6 Commits

Author SHA1 Message Date
Ilya Groshev fa2a124d47 Implement panel missions as static unlock-all
Build and Push Docker images to Docker Hub / build-and-push (push) Has been cancelled
2026-05-16 22:16:31 +03:00
Ilya Groshev 25cbe8635f Implement Tower accumulation reward claiming 2026-05-16 20:51:44 +03:00
Ilya Groshev 1dc5b8fd7c Clear stale side story pointer on mama's room and main-quest transitions 2026-05-16 19:36:07 +03:00
Ilya Groshev c9a1929279 Implement remaining Memoirs preset management RPCs 2026-05-16 19:05:22 +03:00
Ilya Groshev fb111cf1ec Update CHANGELOG with new features and fixes for 2026-05-16 2026-05-16 14:48:29 +03:00
Ilya Groshev 26c10ac429 Implement Recollections of Dusk 2026-05-16 14:35:47 +03:00
29 changed files with 793 additions and 107 deletions
+21
View File
@@ -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
+107 -7
View File
@@ -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)
}
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)
}
firstScene := make(map[int32]int32, len(scenes)/7)
for _, s := range scenes {
if s.SortOrder == 1 {
firstScene[s.SideStoryQuestId] = s.SideStoryQuestSceneId
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
}
}
log.Printf("side story catalog loaded: %d quests", len(firstScene))
return &SideStoryCatalog{FirstSceneByQuestId: firstScene}
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,
}
}
+84
View File
@@ -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}
}
+16 -2
View File
@@ -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
)
+13
View File
@@ -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)
+12 -2
View File
@@ -27,11 +27,21 @@ type QuestHandler struct {
*masterdata.QuestCatalog
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 {
+6
View File
@@ -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
+5 -2
View File
@@ -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
+1
View File
@@ -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.
+31
View File
@@ -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 &rsaquo; 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>`))
+99
View File
@@ -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
}
+6
View File
@@ -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
}
+5
View File
@@ -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
+75 -25
View File
@@ -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
}
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,
scene, ok = sideStoryNextSceneAfterBattle(info, user)
}
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
+49
View File
@@ -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
}
+2
View File
@@ -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)
+2
View File
@@ -124,12 +124,14 @@ func SeedUserState(userId int64, uuid string, nowMillis int64, platform model.Cl
Unlocks: make(map[GimmickKey]GimmickUnlockState),
},
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),
+17
View File
@@ -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
+21
View File
@@ -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}
+2
View File
@@ -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",
+20
View File
@@ -77,11 +77,13 @@ type UserState struct {
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
@@ -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":
+23 -1
View File
@@ -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 {
+22 -1
View File
@@ -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",
+28
View File
@@ -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;