mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Compare commits
6 Commits
15beefb5b8
...
fa2a124d47
| Author | SHA1 | Date | |
|---|---|---|---|
| fa2a124d47 | |||
| 25cbe8635f | |||
| 1dc5b8fd7c | |||
| c9a1929279 | |||
| fb111cf1ec | |||
| 26c10ac429 |
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-05-16
|
||||
|
||||
### Working
|
||||
|
||||
- `--no-register` flag and `register-account` CLI tool
|
||||
- Database backup support in the wizard
|
||||
- Subjugation Quests — rewards, battle report, and triple-deck preset persistence
|
||||
- `MaterialSaleObtainPossession` item grants when selling materials
|
||||
- Memoir protect / unprotect
|
||||
- Effect item usage (`UseEffectItem`)
|
||||
- Recollections of Dusk
|
||||
|
||||
### Fixed
|
||||
|
||||
- Login bonus
|
||||
- Main quest replay (Map replay)
|
||||
- Weapon awaken level cap
|
||||
- Gacha pool overhaul
|
||||
- Quest mission rewards
|
||||
- Menu-pick quest start — wrong state from replay flow, black screen on quests with no difficulty, and normal-difficulty handling
|
||||
|
||||
## 2026-05-02
|
||||
|
||||
### Working
|
||||
|
||||
@@ -2,11 +2,35 @@ package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type SideStorySceneInfo struct {
|
||||
SceneId int32
|
||||
Type model.SideStorySceneIdType
|
||||
}
|
||||
|
||||
type SideStoryQuestInfo struct {
|
||||
SideStoryQuestId int32
|
||||
Scenes []SideStorySceneInfo // the 7 scenes, one per type
|
||||
Quests []int32 // ordered event quests (the chapter+difficulty sequence)
|
||||
}
|
||||
|
||||
type SideStoryCatalog struct {
|
||||
FirstSceneByQuestId map[int32]int32
|
||||
QuestById map[int32]*SideStoryQuestInfo
|
||||
ChapterByEventQuestId map[int32]int32 // event quest id -> side story chapter id
|
||||
}
|
||||
|
||||
func (q *SideStoryQuestInfo) SceneIdByType(t model.SideStorySceneIdType) (int32, bool) {
|
||||
for _, s := range q.Scenes {
|
||||
if s.Type == t {
|
||||
return s.SceneId, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func LoadSideStoryCatalog() *SideStoryCatalog {
|
||||
@@ -14,14 +38,90 @@ func LoadSideStoryCatalog() *SideStoryCatalog {
|
||||
if err != nil {
|
||||
log.Fatalf("load side story quest scene table: %v", err)
|
||||
}
|
||||
|
||||
firstScene := make(map[int32]int32, len(scenes)/7)
|
||||
for _, s := range scenes {
|
||||
if s.SortOrder == 1 {
|
||||
firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId
|
||||
}
|
||||
limitContents, err := utils.ReadTable[EntityMSideStoryQuestLimitContent]("m_side_story_quest_limit_content")
|
||||
if err != nil {
|
||||
log.Fatalf("load side story quest limit content table: %v", err)
|
||||
}
|
||||
seqGroups, err := utils.ReadTable[EntityMEventQuestSequenceGroup]("m_event_quest_sequence_group")
|
||||
if err != nil {
|
||||
log.Fatalf("load event quest sequence group table: %v", err)
|
||||
}
|
||||
sequences, err := utils.ReadTable[EntityMEventQuestSequence]("m_event_quest_sequence")
|
||||
if err != nil {
|
||||
log.Fatalf("load event quest sequence table: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("side story catalog loaded: %d quests", len(firstScene))
|
||||
return &SideStoryCatalog{FirstSceneByQuestId: firstScene}
|
||||
seqRows := make(map[int32][]EntityMEventQuestSequence)
|
||||
for _, s := range sequences {
|
||||
seqRows[s.EventQuestSequenceId] = append(seqRows[s.EventQuestSequenceId], s)
|
||||
}
|
||||
orderedQuestIds := make(map[int32][]int32, len(seqRows))
|
||||
for seqId, rows := range seqRows {
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder })
|
||||
ids := make([]int32, len(rows))
|
||||
for i, r := range rows {
|
||||
ids[i] = r.QuestId
|
||||
}
|
||||
orderedQuestIds[seqId] = ids
|
||||
}
|
||||
|
||||
// (chapterId, difficulty) -> sequenceId. Sequence group id == chapter id.
|
||||
type chapDiff struct{ chapter, difficulty int32 }
|
||||
sequenceByChapterDiff := make(map[chapDiff]int32, len(seqGroups))
|
||||
for _, g := range seqGroups {
|
||||
sequenceByChapterDiff[chapDiff{g.EventQuestSequenceGroupId, g.DifficultyType}] = g.EventQuestSequenceId
|
||||
}
|
||||
|
||||
// sideStoryQuestId -> limit content row. Limit content id == side story quest id.
|
||||
limitByQuest := make(map[int32]EntityMSideStoryQuestLimitContent, len(limitContents))
|
||||
for _, lc := range limitContents {
|
||||
limitByQuest[lc.SideStoryQuestLimitContentId] = lc
|
||||
}
|
||||
|
||||
// sideStoryQuestId -> scene rows
|
||||
scenesByQuest := make(map[int32][]EntityMSideStoryQuestScene)
|
||||
for _, sc := range scenes {
|
||||
scenesByQuest[sc.SideStoryQuestId] = append(scenesByQuest[sc.SideStoryQuestId], sc)
|
||||
}
|
||||
|
||||
questById := make(map[int32]*SideStoryQuestInfo, len(scenesByQuest))
|
||||
chapterByEventQuest := make(map[int32]int32)
|
||||
|
||||
for ssqId, rows := range scenesByQuest {
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].SortOrder < rows[j].SortOrder })
|
||||
|
||||
var orderedQuests []int32
|
||||
var chapterId, difficulty int32
|
||||
if lc, ok := limitByQuest[ssqId]; ok {
|
||||
chapterId = lc.EventQuestChapterId
|
||||
difficulty = lc.DifficultyType
|
||||
if seqId, ok := sequenceByChapterDiff[chapDiff{chapterId, difficulty}]; ok {
|
||||
orderedQuests = orderedQuestIds[seqId]
|
||||
}
|
||||
}
|
||||
if chapterId != 0 {
|
||||
for _, questId := range orderedQuests {
|
||||
chapterByEventQuest[questId] = chapterId
|
||||
}
|
||||
}
|
||||
|
||||
info := &SideStoryQuestInfo{
|
||||
SideStoryQuestId: ssqId,
|
||||
Scenes: make([]SideStorySceneInfo, 0, len(rows)),
|
||||
Quests: orderedQuests,
|
||||
}
|
||||
for _, sc := range rows {
|
||||
info.Scenes = append(info.Scenes, SideStorySceneInfo{
|
||||
SceneId: sc.SideStoryQuestSceneId,
|
||||
Type: model.SideStorySceneIdType(sc.SortOrder),
|
||||
})
|
||||
}
|
||||
questById[ssqId] = info
|
||||
}
|
||||
|
||||
log.Printf("side story catalog loaded: %d quests, %d scenes", len(questById), len(scenes))
|
||||
return &SideStoryCatalog{
|
||||
QuestById: questById,
|
||||
ChapterByEventQuestId: chapterByEventQuest,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type TowerTier struct {
|
||||
QuestMissionClearCount int32
|
||||
Rewards []RewardItem
|
||||
}
|
||||
|
||||
type TowerCatalog struct {
|
||||
TiersByChapter map[int32][]TowerTier
|
||||
}
|
||||
|
||||
func (c *TowerCatalog) CollectRewards(chapterId, oldCount, targetCount int32) ([]RewardItem, int32) {
|
||||
var items []RewardItem
|
||||
highest := int32(0)
|
||||
for _, t := range c.TiersByChapter[chapterId] {
|
||||
if t.QuestMissionClearCount > oldCount && t.QuestMissionClearCount <= targetCount {
|
||||
items = append(items, t.Rewards...)
|
||||
if t.QuestMissionClearCount > highest {
|
||||
highest = t.QuestMissionClearCount
|
||||
}
|
||||
}
|
||||
}
|
||||
return items, highest
|
||||
}
|
||||
|
||||
func LoadTowerCatalog() *TowerCatalog {
|
||||
// chapterId -> accumulation reward group id
|
||||
accumRewardRows, err := utils.ReadTable[EntityMEventQuestTowerAccumulationReward]("m_event_quest_tower_accumulation_reward")
|
||||
if err != nil {
|
||||
log.Fatalf("load event quest tower accumulation reward table: %v", err)
|
||||
}
|
||||
groupByChapter := make(map[int32]int32, len(accumRewardRows))
|
||||
for _, r := range accumRewardRows {
|
||||
groupByChapter[r.EventQuestChapterId] = r.EventQuestTowerAccumulationRewardGroupId
|
||||
}
|
||||
|
||||
// reward group id -> reward items
|
||||
rewardGroupRows, err := utils.ReadTable[EntityMEventQuestTowerRewardGroup]("m_event_quest_tower_reward_group")
|
||||
if err != nil {
|
||||
log.Fatalf("load event quest tower reward group table: %v", err)
|
||||
}
|
||||
itemsByRewardGroup := make(map[int32][]RewardItem)
|
||||
for _, r := range rewardGroupRows {
|
||||
itemsByRewardGroup[r.EventQuestTowerRewardGroupId] = append(itemsByRewardGroup[r.EventQuestTowerRewardGroupId], RewardItem{
|
||||
PossessionType: r.PossessionType,
|
||||
PossessionId: r.PossessionId,
|
||||
Count: r.Count,
|
||||
})
|
||||
}
|
||||
|
||||
// accumulation group id -> tiers (threshold + resolved reward items)
|
||||
accumGroupRows, err := utils.ReadTable[EntityMEventQuestTowerAccumulationRewardGroup]("m_event_quest_tower_accumulation_reward_group")
|
||||
if err != nil {
|
||||
log.Fatalf("load event quest tower accumulation reward group table: %v", err)
|
||||
}
|
||||
tiersByGroup := make(map[int32][]TowerTier)
|
||||
for _, r := range accumGroupRows {
|
||||
tiersByGroup[r.EventQuestTowerAccumulationRewardGroupId] = append(tiersByGroup[r.EventQuestTowerAccumulationRewardGroupId], TowerTier{
|
||||
QuestMissionClearCount: r.QuestMissionClearCount,
|
||||
Rewards: itemsByRewardGroup[r.EventQuestTowerRewardGroupId],
|
||||
})
|
||||
}
|
||||
|
||||
// resolve per-chapter, sorted ascending by threshold
|
||||
tiersByChapter := make(map[int32][]TowerTier, len(groupByChapter))
|
||||
for chapterId, groupId := range groupByChapter {
|
||||
tiers := tiersByGroup[groupId]
|
||||
sort.Slice(tiers, func(i, j int) bool {
|
||||
return tiers[i].QuestMissionClearCount < tiers[j].QuestMissionClearCount
|
||||
})
|
||||
tiersByChapter[chapterId] = tiers
|
||||
}
|
||||
|
||||
log.Printf("tower catalog loaded: %d chapters", len(tiersByChapter))
|
||||
|
||||
return &TowerCatalog{TiersByChapter: tiersByChapter}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package masterdata
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
type WebviewPanelMissionCatalog struct {
|
||||
PageIds []int32 // every WebviewPanelMissionPageId, sorted ascending
|
||||
}
|
||||
|
||||
func LoadWebviewPanelMissionCatalog() *WebviewPanelMissionCatalog {
|
||||
rows, err := utils.ReadTable[EntityMWebviewPanelMissionPage]("m_webview_panel_mission_page")
|
||||
if err != nil {
|
||||
log.Printf("load webview panel mission page table: %v", err)
|
||||
return &WebviewPanelMissionCatalog{}
|
||||
}
|
||||
ids := make([]int32, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ids = append(ids, r.WebviewPanelMissionPageId)
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
return &WebviewPanelMissionCatalog{PageIds: ids}
|
||||
}
|
||||
@@ -164,6 +164,20 @@ type SideStoryQuestStateType int32
|
||||
|
||||
const (
|
||||
SideStoryQuestStateUnknown SideStoryQuestStateType = 0
|
||||
SideStoryQuestStateActive SideStoryQuestStateType = 1
|
||||
SideStoryQuestStateCleared SideStoryQuestStateType = 2
|
||||
SideStoryQuestStateActive SideStoryQuestStateType = 2
|
||||
SideStoryQuestStateCleared SideStoryQuestStateType = 3
|
||||
)
|
||||
|
||||
type SideStorySceneIdType int32
|
||||
|
||||
// Values mirror SideStoryTypes.SceneIdTypes in the client (dump.cs).
|
||||
const (
|
||||
SideStorySceneInvalid SideStorySceneIdType = 0
|
||||
SideStorySceneIntroduction SideStorySceneIdType = 1
|
||||
SideStoryScenePlayGeneralQuest SideStorySceneIdType = 2
|
||||
SideStorySceneUnlockLastQuest SideStorySceneIdType = 3
|
||||
SideStoryScenePlayLastQuest SideStorySceneIdType = 4
|
||||
SideStorySceneOutroduction SideStorySceneIdType = 5
|
||||
SideStorySceneShowCostumeAcquisition SideStorySceneIdType = 6
|
||||
SideStoryScenePlayFreeQuest SideStorySceneIdType = 7
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
@@ -64,6 +65,18 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
||||
return outcome
|
||||
}
|
||||
|
||||
func (h *QuestHandler) recordSideStoryLimitContentStatus(user *store.UserState, questId int32, nowMillis int64) {
|
||||
chapterId, ok := h.SideStoryChapterByEventQuestId[questId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
st := user.QuestLimitContentStatus[questId]
|
||||
st.LimitContentQuestStatusType = 1
|
||||
st.EventQuestChapterId = chapterId
|
||||
st.LatestVersion = nowMillis
|
||||
user.QuestLimitContentStatus[questId] = st
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuestChapterId, questId int32, nowMillis int64) {
|
||||
h.HandleQuestRestart(user, questId, nowMillis)
|
||||
|
||||
|
||||
@@ -25,13 +25,23 @@ type FinishOutcome struct {
|
||||
|
||||
type QuestHandler struct {
|
||||
*masterdata.QuestCatalog
|
||||
Config *masterdata.GameConfig
|
||||
Granter *store.PossessionGranter
|
||||
Config *masterdata.GameConfig
|
||||
Granter *store.PossessionGranter
|
||||
SideStoryChapterByEventQuestId map[int32]int32
|
||||
}
|
||||
|
||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *QuestHandler {
|
||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler {
|
||||
granter := BuildGranter(catalog)
|
||||
return &QuestHandler{QuestCatalog: catalog, Config: config, Granter: granter}
|
||||
var sideStoryChapters map[int32]int32
|
||||
if sideStory != nil {
|
||||
sideStoryChapters = sideStory.ChapterByEventQuestId
|
||||
}
|
||||
return &QuestHandler{
|
||||
QuestCatalog: catalog,
|
||||
Config: config,
|
||||
Granter: granter,
|
||||
SideStoryChapterByEventQuestId: sideStoryChapters,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
||||
|
||||
@@ -142,6 +142,12 @@ func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, ques
|
||||
user.PortalCageStatus.IsCurrentProgress = false
|
||||
user.PortalCageStatus.LatestVersion = nowMillis
|
||||
|
||||
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
|
||||
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
flowType := h.replayFlowType(user, questSceneId)
|
||||
user.MainQuest.CurrentQuestFlowType = int32(flowType)
|
||||
user.MainQuest.LatestVersion = nowMillis
|
||||
|
||||
@@ -33,7 +33,8 @@ func buildCatalogs() (*Catalogs, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest catalog: %w", err)
|
||||
}
|
||||
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
|
||||
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
||||
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog)
|
||||
|
||||
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
||||
if err != nil {
|
||||
@@ -136,9 +137,10 @@ func buildCatalogs() (*Catalogs, error) {
|
||||
}
|
||||
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
|
||||
|
||||
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
||||
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
|
||||
|
||||
towerCatalog := masterdata.LoadTowerCatalog()
|
||||
|
||||
return &Catalogs{
|
||||
GameConfig: gameConfig,
|
||||
Parts: partsCatalog,
|
||||
@@ -164,6 +166,7 @@ func buildCatalogs() (*Catalogs, error) {
|
||||
Companion: companionCatalog,
|
||||
SideStory: sideStoryCatalog,
|
||||
BigHunt: bigHuntCatalog,
|
||||
Tower: towerCatalog,
|
||||
QuestHandler: questHandler,
|
||||
GachaHandler: gachaHandler,
|
||||
}, nil
|
||||
|
||||
@@ -50,6 +50,7 @@ type Catalogs struct {
|
||||
Companion *masterdata.CompanionCatalog
|
||||
SideStory *masterdata.SideStoryCatalog
|
||||
BigHunt *masterdata.BigHuntCatalog
|
||||
Tower *masterdata.TowerCatalog
|
||||
|
||||
// Catalog-derived handlers must rebuild on every reload because they
|
||||
// embed/cache pointers to specific catalog instances.
|
||||
|
||||
@@ -44,6 +44,30 @@ const informationPage = `<!DOCTYPE html>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const panelMissionPage = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Panel Missions</title>
|
||||
<style>
|
||||
body { margin:0; padding:48px 20px; font-family:"Noto Sans",sans-serif;
|
||||
background:#0a0a0f; color:#d4cfc6; text-align:center; }
|
||||
h1 { font-size:1.3em; letter-spacing:.15em; color:#e8e0d0; margin-bottom:6px; }
|
||||
.sub { font-size:.75em; color:#888; margin-bottom:28px; }
|
||||
.sep { width:60px; border:none; border-top:1px solid #333; margin:24px auto; }
|
||||
p { font-size:.85em; line-height:1.6; color:#999; max-width:340px; margin:0 auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PANEL MISSIONS</h1>
|
||||
<div class="sub">Card Stories</div>
|
||||
<hr class="sep">
|
||||
<p>All panel missions are cleared.</p>
|
||||
<p>Their Card Stories are available in Library › Extra Stories.</p>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// resourcesURLOriginal is the base URL embedded in list.bin; must be replaced with same-length (43 bytes) when rewriting.
|
||||
const resourcesURLOriginal = "https://resources.app.nierreincarnation.com"
|
||||
|
||||
@@ -456,6 +480,13 @@ func (s *OctoHTTPServer) handleWebAPI(w http.ResponseWriter, r *http.Request, pa
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(path, "panelmission") {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(panelMissionPage))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body></body></html>`))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Cont
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||
user.MainQuest.LatestVersion = now
|
||||
}
|
||||
// Returning to Mama's Room also ends any active side story.
|
||||
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
|
||||
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||
LatestVersion: now,
|
||||
}
|
||||
}
|
||||
})
|
||||
return &pb.UpdatePortalCageSceneProgressResponse{}, nil
|
||||
}
|
||||
|
||||
@@ -203,6 +203,11 @@ func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteReque
|
||||
now := gametime.NowMillis()
|
||||
user.PortalCageStatus.IsCurrentProgress = false
|
||||
user.PortalCageStatus.LatestVersion = now
|
||||
if user.SideStoryActiveProgress.CurrentSideStoryQuestId != 0 {
|
||||
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||
LatestVersion: now,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return &pb.SetRouteResponse{}, nil
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/runtime"
|
||||
"lunar-tear/server/internal/store"
|
||||
@@ -22,34 +23,89 @@ func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.S
|
||||
return &SideStoryQuestServiceServer{users: users, sessions: sessions, holder: holder}
|
||||
}
|
||||
|
||||
func sideStoryClearedCount(info *masterdata.SideStoryQuestInfo, user *store.UserState) int {
|
||||
cleared := 0
|
||||
for _, questId := range info.Quests {
|
||||
if user.QuestLimitContentStatus[questId].LimitContentQuestStatusType == 1 {
|
||||
cleared++
|
||||
}
|
||||
}
|
||||
return cleared
|
||||
}
|
||||
|
||||
func sideStoryQuestCleared(info *masterdata.SideStoryQuestInfo, user *store.UserState) bool {
|
||||
return info != nil && len(info.Quests) > 0 && sideStoryClearedCount(info, user) == len(info.Quests)
|
||||
}
|
||||
|
||||
func sideStoryNextSceneAfterBattle(info *masterdata.SideStoryQuestInfo, user *store.UserState) (int32, bool) {
|
||||
cleared := sideStoryClearedCount(info, user)
|
||||
if cleared == 0 {
|
||||
return 0, false
|
||||
}
|
||||
total := len(info.Quests)
|
||||
var sceneType model.SideStorySceneIdType
|
||||
switch {
|
||||
case cleared >= total:
|
||||
sceneType = model.SideStorySceneOutroduction
|
||||
case cleared == total-1:
|
||||
sceneType = model.SideStorySceneUnlockLastQuest
|
||||
default:
|
||||
sceneType = model.SideStoryScenePlayLastQuest
|
||||
}
|
||||
return info.SceneIdByType(sceneType)
|
||||
}
|
||||
|
||||
func applySideStoryProgressState(progress *store.SideStoryQuestProgress, info *masterdata.SideStoryQuestInfo, user *store.UserState) {
|
||||
if sideStoryQuestCleared(info, user) {
|
||||
progress.SideStoryQuestStateType = model.SideStoryQuestStateCleared
|
||||
} else if progress.SideStoryQuestStateType == model.SideStoryQuestStateUnknown {
|
||||
progress.SideStoryQuestStateType = model.SideStoryQuestStateActive
|
||||
}
|
||||
}
|
||||
|
||||
func setSideStoryActive(user *store.UserState, questId, sceneId int32, nowMillis int64) {
|
||||
user.SideStoryActiveProgress = store.SideStoryActiveProgress{
|
||||
CurrentSideStoryQuestId: questId,
|
||||
CurrentSideStoryQuestSceneId: sceneId,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
|
||||
func setSideStoryScene(user *store.UserState, info *masterdata.SideStoryQuestInfo, questId, sceneId int32, nowMillis int64) {
|
||||
progress := user.SideStoryQuests[questId]
|
||||
progress.HeadSideStoryQuestSceneId = sceneId
|
||||
applySideStoryProgressState(&progress, info, user)
|
||||
progress.LatestVersion = nowMillis
|
||||
user.SideStoryQuests[questId] = progress
|
||||
setSideStoryActive(user, questId, sceneId, nowMillis)
|
||||
}
|
||||
|
||||
func (s *SideStoryQuestServiceServer) MoveSideStoryQuestProgress(ctx context.Context, req *pb.MoveSideStoryQuestRequest) (*pb.MoveSideStoryQuestResponse, error) {
|
||||
log.Printf("[SideStoryQuestService] MoveSideStoryQuestProgress: sideStoryQuestId=%d", req.SideStoryQuestId)
|
||||
|
||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
firstSceneId := s.holder.Get().SideStory.FirstSceneByQuestId[req.SideStoryQuestId]
|
||||
info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId]
|
||||
|
||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if info == nil || len(info.Quests) == 0 {
|
||||
log.Printf("[SideStoryQuestService] unknown sideStoryQuestId=%d, skipping", req.SideStoryQuestId)
|
||||
return
|
||||
}
|
||||
|
||||
existing, exists := user.SideStoryQuests[req.SideStoryQuestId]
|
||||
|
||||
var sceneId int32
|
||||
if exists && existing.HeadSideStoryQuestSceneId > 0 {
|
||||
sceneId = existing.HeadSideStoryQuestSceneId
|
||||
var scene int32
|
||||
var ok bool
|
||||
if !exists || existing.HeadSideStoryQuestSceneId == 0 {
|
||||
scene, ok = info.SceneIdByType(model.SideStorySceneIntroduction)
|
||||
} else {
|
||||
sceneId = firstSceneId
|
||||
scene, ok = sideStoryNextSceneAfterBattle(info, user)
|
||||
}
|
||||
|
||||
user.SideStoryActiveProgress.CurrentSideStoryQuestId = req.SideStoryQuestId
|
||||
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = sceneId
|
||||
user.SideStoryActiveProgress.LatestVersion = nowMillis
|
||||
|
||||
if !exists {
|
||||
user.SideStoryQuests[req.SideStoryQuestId] = store.SideStoryQuestProgress{
|
||||
HeadSideStoryQuestSceneId: firstSceneId,
|
||||
SideStoryQuestStateType: model.SideStoryQuestStateActive,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
setSideStoryScene(user, info, req.SideStoryQuestId, scene, nowMillis)
|
||||
})
|
||||
|
||||
return &pb.MoveSideStoryQuestResponse{}, nil
|
||||
@@ -61,16 +117,10 @@ func (s *SideStoryQuestServiceServer) UpdateSideStoryQuestSceneProgress(ctx cont
|
||||
|
||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
user.SideStoryActiveProgress.CurrentSideStoryQuestSceneId = req.SideStoryQuestSceneId
|
||||
user.SideStoryActiveProgress.LatestVersion = nowMillis
|
||||
info := s.holder.Get().SideStory.QuestById[req.SideStoryQuestId]
|
||||
|
||||
progress := user.SideStoryQuests[req.SideStoryQuestId]
|
||||
if req.SideStoryQuestSceneId > progress.HeadSideStoryQuestSceneId {
|
||||
progress.HeadSideStoryQuestSceneId = req.SideStoryQuestSceneId
|
||||
}
|
||||
progress.LatestVersion = nowMillis
|
||||
user.SideStoryQuests[req.SideStoryQuestId] = progress
|
||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
setSideStoryScene(user, info, req.SideStoryQuestId, req.SideStoryQuestSceneId, nowMillis)
|
||||
})
|
||||
|
||||
return &pb.UpdateSideStoryQuestSceneProgressResponse{}, nil
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
func (s *QuestServiceServer) ReceiveTowerAccumulationReward(ctx context.Context, req *pb.ReceiveTowerAccumulationRewardRequest) (*pb.ReceiveTowerAccumulationRewardResponse, error) {
|
||||
log.Printf("[QuestService] ReceiveTowerAccumulationReward: eventQuestChapterId=%d targetMissionClearCount=%d",
|
||||
req.EventQuestChapterId, req.TargetMissionClearCount)
|
||||
|
||||
cat := s.holder.Get()
|
||||
tower := cat.Tower
|
||||
granter := cat.QuestHandler.Granter
|
||||
|
||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
rec := user.TowerAccumulationRewards[req.EventQuestChapterId]
|
||||
old := rec.LatestRewardReceiveQuestMissionClearCount
|
||||
|
||||
items, highest := tower.CollectRewards(req.EventQuestChapterId, old, req.TargetMissionClearCount)
|
||||
if highest <= old {
|
||||
log.Printf("[QuestService] ReceiveTowerAccumulationReward: nothing to grant for chapter=%d (claimed=%d, target=%d)",
|
||||
req.EventQuestChapterId, old, req.TargetMissionClearCount)
|
||||
return
|
||||
}
|
||||
|
||||
for _, it := range items {
|
||||
granter.GrantFull(user, model.PossessionType(it.PossessionType), it.PossessionId, it.Count, nowMillis)
|
||||
}
|
||||
|
||||
rec.EventQuestChapterId = req.EventQuestChapterId
|
||||
rec.LatestRewardReceiveQuestMissionClearCount = highest
|
||||
rec.LatestVersion = nowMillis
|
||||
user.TowerAccumulationRewards[req.EventQuestChapterId] = rec
|
||||
|
||||
log.Printf("[QuestService] ReceiveTowerAccumulationReward: chapter=%d granted %d item(s), claimed %d -> %d",
|
||||
req.EventQuestChapterId, len(items), old, highest)
|
||||
})
|
||||
|
||||
return &pb.ReceiveTowerAccumulationRewardResponse{}, nil
|
||||
}
|
||||
@@ -26,11 +26,13 @@ func CloneUserState(u UserState) UserState {
|
||||
Unlocks: maps.Clone(u.Gimmick.Unlocks),
|
||||
}
|
||||
out.CageOrnamentRewards = maps.Clone(u.CageOrnamentRewards)
|
||||
out.TowerAccumulationRewards = maps.Clone(u.TowerAccumulationRewards)
|
||||
out.ConsumableItems = maps.Clone(u.ConsumableItems)
|
||||
out.Materials = maps.Clone(u.Materials)
|
||||
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)
|
||||
|
||||
@@ -123,29 +123,31 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
|
||||
Sequences: make(map[GimmickSequenceKey]GimmickSequenceState),
|
||||
Unlocks: make(map[GimmickKey]GimmickUnlockState),
|
||||
},
|
||||
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
|
||||
ConsumableItems: make(map[int32]int32),
|
||||
Materials: make(map[int32]int32),
|
||||
Thoughts: make(map[string]ThoughtState),
|
||||
Parts: make(map[string]PartsState),
|
||||
PartsGroupNotes: make(map[int32]PartsGroupNoteState),
|
||||
PartsPresets: make(map[int32]PartsPresetState),
|
||||
PartsStatusSubs: make(map[PartsStatusSubKey]PartsStatusSubState),
|
||||
ImportantItems: make(map[int32]int32),
|
||||
CostumeActiveSkills: make(map[string]CostumeActiveSkillState),
|
||||
WeaponSkills: make(map[string][]WeaponSkillState),
|
||||
WeaponAbilities: make(map[string][]WeaponAbilityState),
|
||||
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
|
||||
WeaponNotes: make(map[int32]WeaponNoteState),
|
||||
NaviCutInPlayed: make(map[int32]bool),
|
||||
ViewedMovies: make(map[int32]int64),
|
||||
ContentsStories: make(map[int32]int64),
|
||||
DrawnOmikuji: make(map[int32]int64),
|
||||
PremiumItems: make(map[int32]int64),
|
||||
DokanConfirmed: make(map[int32]bool),
|
||||
ShopItems: make(map[int32]UserShopItemState),
|
||||
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
|
||||
ExploreScores: make(map[int32]ExploreScoreState),
|
||||
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
|
||||
TowerAccumulationRewards: make(map[int32]TowerAccumulationRewardState),
|
||||
ConsumableItems: make(map[int32]int32),
|
||||
Materials: make(map[int32]int32),
|
||||
Thoughts: make(map[string]ThoughtState),
|
||||
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),
|
||||
WeaponSkills: make(map[string][]WeaponSkillState),
|
||||
WeaponAbilities: make(map[string][]WeaponAbilityState),
|
||||
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
|
||||
WeaponNotes: make(map[int32]WeaponNoteState),
|
||||
NaviCutInPlayed: make(map[int32]bool),
|
||||
ViewedMovies: make(map[int32]int64),
|
||||
ContentsStories: make(map[int32]int64),
|
||||
DrawnOmikuji: make(map[int32]int64),
|
||||
PremiumItems: make(map[int32]int64),
|
||||
DokanConfirmed: make(map[int32]bool),
|
||||
ShopItems: make(map[int32]UserShopItemState),
|
||||
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
|
||||
ExploreScores: make(map[int32]ExploreScoreState),
|
||||
|
||||
CharacterBoards: make(map[int32]CharacterBoardState),
|
||||
CharacterBoardAbilities: make(map[CharacterBoardAbilityKey]CharacterBoardAbilityState),
|
||||
|
||||
@@ -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)
|
||||
@@ -76,6 +77,7 @@ func initMaps(u *store.UserState) {
|
||||
u.ShopReplaceableLineup = make(map[int32]store.UserShopReplaceableLineupState)
|
||||
u.ExploreScores = make(map[int32]store.ExploreScoreState)
|
||||
u.CageOrnamentRewards = make(map[int32]store.CageOrnamentRewardState)
|
||||
u.TowerAccumulationRewards = make(map[int32]store.TowerAccumulationRewardState)
|
||||
u.CharacterBoards = make(map[int32]store.CharacterBoardState)
|
||||
u.CharacterBoardAbilities = make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState)
|
||||
u.CharacterBoardStatusUps = make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState)
|
||||
@@ -492,6 +494,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,
|
||||
@@ -642,6 +652,13 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
||||
u.CageOrnamentRewards[v.CageOrnamentId] = v
|
||||
})
|
||||
|
||||
queryRows(db, `SELECT event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version
|
||||
FROM user_event_quest_tower_accumulation_rewards WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||
var v store.TowerAccumulationRewardState
|
||||
rows.Scan(&v.EventQuestChapterId, &v.LatestRewardReceiveQuestMissionClearCount, &v.LatestVersion)
|
||||
u.TowerAccumulationRewards[v.EventQuestChapterId] = v
|
||||
})
|
||||
|
||||
queryRows(db, `SELECT shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version
|
||||
FROM user_shop_items WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||
var v store.UserShopItemState
|
||||
|
||||
@@ -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 {
|
||||
@@ -448,6 +454,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, v := range u.TowerAccumulationRewards {
|
||||
if err := exec(`INSERT INTO user_event_quest_tower_accumulation_rewards (user_id, event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version) VALUES (?,?,?,?)`,
|
||||
uid, v.EventQuestChapterId, v.LatestRewardReceiveQuestMissionClearCount, v.LatestVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, v := range u.ShopItems {
|
||||
if err := exec(`INSERT INTO user_shop_items (user_id, shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version) VALUES (?,?,?,?,?)`,
|
||||
uid, v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion); err != nil {
|
||||
@@ -862,6 +874,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 {
|
||||
@@ -984,6 +1000,11 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return []any{v.CageOrnamentId, v.AcquisitionDatetime, v.LatestVersion}
|
||||
},
|
||||
"cage_ornament_id, acquisition_datetime, latest_version")
|
||||
diffMapInt32(tx, uid, before.TowerAccumulationRewards, after.TowerAccumulationRewards, "user_event_quest_tower_accumulation_rewards", "event_quest_chapter_id",
|
||||
func(v store.TowerAccumulationRewardState) []any {
|
||||
return []any{v.EventQuestChapterId, v.LatestRewardReceiveQuestMissionClearCount, v.LatestVersion}
|
||||
},
|
||||
"event_quest_chapter_id, latest_reward_receive_quest_mission_clear_count, latest_version")
|
||||
diffMapInt32(tx, uid, before.ShopItems, after.ShopItems, "user_shop_items", "shop_item_id",
|
||||
func(v store.UserShopItemState) []any {
|
||||
return []any{v.ShopItemId, v.BoughtCount, v.LatestBoughtCountChangedDatetime, v.LatestVersion}
|
||||
|
||||
@@ -79,6 +79,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
|
||||
|
||||
// Child tables in reverse-dependency order (matches schema's goose Down).
|
||||
childTables := []string{
|
||||
"user_event_quest_tower_accumulation_rewards",
|
||||
"user_cage_ornament_rewards",
|
||||
"user_shop_replaceable_lineup",
|
||||
"user_shop_items",
|
||||
@@ -119,6 +120,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",
|
||||
|
||||
@@ -63,46 +63,48 @@ type UserState struct {
|
||||
Gacha GachaState
|
||||
Notifications NotificationState
|
||||
|
||||
Characters map[int32]CharacterState
|
||||
Costumes map[string]CostumeState
|
||||
Weapons map[string]WeaponState
|
||||
Companions map[string]CompanionState
|
||||
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
|
||||
WeaponStories map[int32]WeaponStoryState
|
||||
Gimmick GimmickState
|
||||
CageOrnamentRewards map[int32]CageOrnamentRewardState
|
||||
ConsumableItems map[int32]int32
|
||||
Materials map[int32]int32
|
||||
Parts map[string]PartsState
|
||||
PartsGroupNotes map[int32]PartsGroupNoteState
|
||||
PartsPresets map[int32]PartsPresetState
|
||||
PartsStatusSubs map[PartsStatusSubKey]PartsStatusSubState
|
||||
ImportantItems map[int32]int32
|
||||
CostumeActiveSkills map[string]CostumeActiveSkillState
|
||||
WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid
|
||||
WeaponAbilities map[string][]WeaponAbilityState // key: userWeaponUuid
|
||||
WeaponAwakens map[string]WeaponAwakenState // key: userWeaponUuid
|
||||
DeckTypeNotes map[model.DeckType]DeckTypeNoteState
|
||||
WeaponNotes map[int32]WeaponNoteState
|
||||
DeckSubWeapons map[string][]string
|
||||
DeckParts map[string][]string
|
||||
NaviCutInPlayed map[int32]bool
|
||||
ViewedMovies map[int32]int64
|
||||
ContentsStories map[int32]int64
|
||||
DrawnOmikuji map[int32]int64
|
||||
PremiumItems map[int32]int64
|
||||
DokanConfirmed map[int32]bool
|
||||
PortalCageStatus PortalCageStatusState
|
||||
GuerrillaFreeOpen GuerrillaFreeOpenState
|
||||
ShopItems map[int32]UserShopItemState
|
||||
ShopReplaceable UserShopReplaceableState
|
||||
ShopReplaceableLineup map[int32]UserShopReplaceableLineupState
|
||||
Characters map[int32]CharacterState
|
||||
Costumes map[string]CostumeState
|
||||
Weapons map[string]WeaponState
|
||||
Companions map[string]CompanionState
|
||||
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
|
||||
WeaponStories map[int32]WeaponStoryState
|
||||
Gimmick GimmickState
|
||||
CageOrnamentRewards map[int32]CageOrnamentRewardState
|
||||
TowerAccumulationRewards map[int32]TowerAccumulationRewardState
|
||||
ConsumableItems map[int32]int32
|
||||
Materials map[int32]int32
|
||||
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
|
||||
WeaponSkills map[string][]WeaponSkillState // key: userWeaponUuid
|
||||
WeaponAbilities map[string][]WeaponAbilityState // key: userWeaponUuid
|
||||
WeaponAwakens map[string]WeaponAwakenState // key: userWeaponUuid
|
||||
DeckTypeNotes map[model.DeckType]DeckTypeNoteState
|
||||
WeaponNotes map[int32]WeaponNoteState
|
||||
DeckSubWeapons map[string][]string
|
||||
DeckParts map[string][]string
|
||||
NaviCutInPlayed map[int32]bool
|
||||
ViewedMovies map[int32]int64
|
||||
ContentsStories map[int32]int64
|
||||
DrawnOmikuji map[int32]int64
|
||||
PremiumItems map[int32]int64
|
||||
DokanConfirmed map[int32]bool
|
||||
PortalCageStatus PortalCageStatusState
|
||||
GuerrillaFreeOpen GuerrillaFreeOpenState
|
||||
ShopItems map[int32]UserShopItemState
|
||||
ShopReplaceable UserShopReplaceableState
|
||||
ShopReplaceableLineup map[int32]UserShopReplaceableLineupState
|
||||
|
||||
Explore ExploreState
|
||||
ExploreScores map[int32]ExploreScoreState
|
||||
@@ -191,6 +193,9 @@ func (u *UserState) EnsureMaps() {
|
||||
if u.CageOrnamentRewards == nil {
|
||||
u.CageOrnamentRewards = make(map[int32]CageOrnamentRewardState)
|
||||
}
|
||||
if u.TowerAccumulationRewards == nil {
|
||||
u.TowerAccumulationRewards = make(map[int32]TowerAccumulationRewardState)
|
||||
}
|
||||
if u.ConsumableItems == nil {
|
||||
u.ConsumableItems = make(map[int32]int32)
|
||||
}
|
||||
@@ -206,6 +211,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)
|
||||
}
|
||||
@@ -864,6 +872,12 @@ type CageOrnamentRewardState struct {
|
||||
LatestVersion int64
|
||||
}
|
||||
|
||||
type TowerAccumulationRewardState struct {
|
||||
EventQuestChapterId int32
|
||||
LatestRewardReceiveQuestMissionClearCount int32
|
||||
LatestVersion int64
|
||||
}
|
||||
|
||||
type PartsState struct {
|
||||
UserPartsUuid string
|
||||
PartsId int32
|
||||
@@ -890,6 +904,12 @@ type PartsPresetState struct {
|
||||
LatestVersion int64
|
||||
}
|
||||
|
||||
type PartsPresetTagState struct {
|
||||
UserPartsPresetTagNumber int32
|
||||
Name string
|
||||
LatestVersion int64
|
||||
}
|
||||
|
||||
type PartsStatusSubKey struct {
|
||||
UserPartsUuid string
|
||||
StatusIndex int32
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -260,6 +263,9 @@ func ChangedTables(before, after *store.UserState) []string {
|
||||
if !mapsEqualStruct(before.CageOrnamentRewards, after.CageOrnamentRewards) {
|
||||
add("IUserCageOrnamentReward")
|
||||
}
|
||||
if !mapsEqualStruct(before.TowerAccumulationRewards, after.TowerAccumulationRewards) {
|
||||
add("IUserEventQuestTowerAccumulationReward")
|
||||
}
|
||||
|
||||
if !mapsEqualStruct(before.BigHuntMaxScores, after.BigHuntMaxScores) {
|
||||
add("IUserBigHuntMaxScore")
|
||||
@@ -419,8 +425,12 @@ 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 "IUserEventQuestTowerAccumulationReward":
|
||||
return []string{"userId", "eventQuestChapterId"}
|
||||
case "IUserAutoSaleSettingDetail":
|
||||
return []string{"userId", "possessionAutoSaleItemType"}
|
||||
case "IUserCharacterRebirth":
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -219,11 +219,32 @@ func init() {
|
||||
s, _ := utils.EncodeJSONMaps(records...)
|
||||
return s
|
||||
})
|
||||
register("IUserEventQuestTowerAccumulationReward", func(user store.UserState) string {
|
||||
if len(user.TowerAccumulationRewards) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
ids := make([]int, 0, len(user.TowerAccumulationRewards))
|
||||
for id := range user.TowerAccumulationRewards {
|
||||
ids = append(ids, int(id))
|
||||
}
|
||||
sort.Ints(ids)
|
||||
records := make([]map[string]any, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
st := user.TowerAccumulationRewards[int32(id)]
|
||||
records = append(records, map[string]any{
|
||||
"userId": user.UserId,
|
||||
"eventQuestChapterId": st.EventQuestChapterId,
|
||||
"latestRewardReceiveQuestMissionClearCount": st.LatestRewardReceiveQuestMissionClearCount,
|
||||
"latestVersion": st.LatestVersion,
|
||||
})
|
||||
}
|
||||
s, _ := utils.EncodeJSONMaps(records...)
|
||||
return s
|
||||
})
|
||||
registerStatic(
|
||||
"IUserEventQuestDailyGroupCompleteReward",
|
||||
"IUserEventQuestLabyrinthSeason",
|
||||
"IUserEventQuestLabyrinthStage",
|
||||
"IUserEventQuestTowerAccumulationReward",
|
||||
"IUserQuestReplayFlowRewardGroup",
|
||||
"IUserQuestAutoOrbit",
|
||||
"IUserQuestSceneChoice",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/utils"
|
||||
)
|
||||
|
||||
var webviewPanelMissionCatalog = sync.OnceValue(masterdata.LoadWebviewPanelMissionCatalog)
|
||||
|
||||
func init() {
|
||||
register("IUserWebviewPanelMission", func(user store.UserState) string {
|
||||
pageIds := webviewPanelMissionCatalog().PageIds
|
||||
records := make([]map[string]any, 0, len(pageIds))
|
||||
for _, pageId := range pageIds {
|
||||
records = append(records, map[string]any{
|
||||
"userId": user.UserId,
|
||||
"webviewPanelMissionPageId": pageId,
|
||||
"rewardReceiveDatetime": user.GameStartDatetime,
|
||||
"latestVersion": user.GameStartDatetime,
|
||||
})
|
||||
}
|
||||
s, _ := utils.EncodeJSONMaps(records...)
|
||||
return s
|
||||
})
|
||||
}
|
||||
@@ -102,6 +102,7 @@ func FullClientTableMap(user store.UserState) map[string]string {
|
||||
"IUserBigHuntWeeklyStatus": projectTable("IUserBigHuntWeeklyStatus", user),
|
||||
"IUserFacebook": projectTable("IUserFacebook", user),
|
||||
"IUserApple": projectTable("IUserApple", user),
|
||||
"IUserWebviewPanelMission": projectTable("IUserWebviewPanelMission", user),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE user_event_quest_tower_accumulation_rewards (
|
||||
user_id INTEGER NOT NULL REFERENCES users(user_id),
|
||||
event_quest_chapter_id INTEGER NOT NULL,
|
||||
latest_reward_receive_quest_mission_clear_count INTEGER NOT NULL DEFAULT 0,
|
||||
latest_version INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (user_id, event_quest_chapter_id)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE IF EXISTS user_event_quest_tower_accumulation_rewards;
|
||||
Reference in New Issue
Block a user