package world import ( "bd2server/internal/server/gamedata" "bd2server/internal/server/player" "bd2server/internal/server/progress" "bd2server/internal/server/wire" "encoding/binary" "fmt" ) func (s *Service) questDifficultyFor(packID, questID int) int { if s.storyCatalog != nil && s.storyCatalog.Packs[packID].Quests[questID].Type != 0 { return 0 } return s.questDifficulty(packID) } func (s *Service) questDifficulty(packID int) int { selection, _ := s.state.Selection(packID) return selection.Difficulty } func (s *Service) CurrentQuestDifficulty() (uint64, error) { pack, err := s.CurrentPackID() if err != nil { return 0, err } return uint64(s.questDifficulty(pack)), nil } func questRewardIdentity(packID, questID, difficulty int) string { if difficulty == 0 { return fmt.Sprintf("pack%d:quest%d", packID, questID) } return fmt.Sprintf("pack%d:difficulty%d:quest%d", packID, difficulty, questID) } func (s *Service) questInfoWire(packID, questID int) []byte { out := wire.AppendVarint(nil, 1, uint64(questID)) selection, _ := s.state.Selection(packID) if s.storyCatalog != nil && s.storyCatalog.Packs[packID].Quests[questID].Type != 0 { selection = progress.QuestSelection{} } if current, ok := s.state.QuestInPack(questID, packID, selection.Difficulty); ok { for _, value := range current.Values { out = wire.AppendVarint(out, 3, uint64(value)) } } if selection.Difficulty != 0 { out = wire.AppendVarint(out, 4, uint64(selection.Difficulty)) } if selection.Option != 0 { out = wire.AppendVarint(out, 5, uint64(selection.Option)) } return wire.AppendVarint(out, 6, uint64(packID)) } func (s *Service) questLevelInfoWire(packID, difficulty int) []byte { out := wire.AppendVarint(nil, 1, uint64(packID)) out = wire.AppendVarint(out, 2, uint64(difficulty)) last := 0 complete := true pack := s.storyCatalog.Packs[packID] for _, id := range pack.MainQuestIDs { if !s.state.QuestCleared(id, packID, difficulty) { complete = false break } last = id } out = wire.AppendVarint(out, 3, uint64(last)) selection, _ := s.state.Selection(packID) if selection.Difficulty == difficulty { out = wire.AppendVarint(out, 4, uint64(selection.Option)) } if complete && len(pack.MainQuestIDs) > 0 { out = wire.AppendVarint(out, 5, 1) } return out } func (s *Service) handleQuestSelection(path string, request []byte) (int, []byte, bool, error) { if path != "/QuestInfo" && s.battleActive != nil && s.battleActive() { return 0, nil, true, fmt.Errorf("%w: active battle", ErrInvalidRequest) } if path == "/QuestInfo" { pack, err := requestPack(request) if err != nil || !s.packUnlocked(pack) { return 0, nil, true, ErrInvalidRequest } var out []byte if active := s.firstUnclearedQuestFor(pack); active != 0 { out = wire.AppendBytes(out, 1, s.questInfoWire(pack, active)) } for _, quest := range s.activeSideQuestWires(pack) { out = wire.AppendBytes(out, 1, quest) } var cleared []byte for _, id := range s.state.ClearedQuests(pack, s.questDifficulty(pack)) { cleared = binary.AppendUvarint(cleared, uint64(id)) } if len(cleared) > 0 { out = wire.AppendBytes(out, 2, cleared) } return 16, out, true, nil } quest, pack, err := requestQuest(request) if err != nil || !s.packUnlocked(pack) { return 0, nil, true, ErrInvalidRequest } designs, known := s.questsFor(pack) design, exists := designs[quest] if !known || !exists { return 0, nil, true, ErrInvalidRequest } if path == "/QuestGiveUp" { if design.Type != 0 { if _, ok := s.state.QuestInPack(quest, pack); !ok { return 0, nil, true, ErrInvalidRequest } if err := s.state.RemoveQuest(quest, pack, 0); err != nil { return 0, nil, true, err } return 20, wire.AppendVarint(nil, 1, uint64(quest)), true, nil } selection, ok := s.state.Selection(pack) if !ok || selection.QuestID != quest || selection.Difficulty == 0 { return 0, nil, true, ErrInvalidRequest } // Abandoning a difficulty retains its independently committed clears. if err := s.state.SelectQuest(pack, progress.QuestSelection{}); err != nil { return 0, nil, true, err } return 20, wire.AppendVarint(nil, 1, uint64(quest)), true, nil } level, _, err := wire.Varint(request, 4) if err != nil || level > 4 { return 0, nil, true, ErrInvalidRequest } opt, _, err := wire.Varint(request, 5) if err != nil || opt != 0 { return 0, nil, true, ErrInvalidRequest } if level > 0 && !s.questDifficulties[pack][int(level)] { return 0, nil, true, ErrInvalidRequest } if design.Type != 0 { if design.Type != 1 { return 0, nil, true, ErrInvalidRequest } if level != 0 || (design.PriorQuestID != 0 && !s.state.QuestCleared(design.PriorQuestID, pack)) || s.state.QuestCleared(quest, pack) { return 0, nil, true, ErrInvalidRequest } if err := s.state.AcceptQuest(quest, pack, 0); err != nil { return 0, nil, true, err } out := wire.AppendBytes(nil, 1, s.questInfoWire(pack, quest)) items, err := s.ensureQuestItems(pack, quest) if err != nil { return 0, nil, true, err } for _, item := range items { out = wire.AppendBytes(out, 4, player.ItemWire(item)) } return 17, out, true, nil } if level > 0 { ids := s.storyCatalog.Packs[pack].MainQuestIDs for _, id := range ids { if !s.state.QuestCleared(id, pack, int(level)-1) { return 0, nil, true, fmt.Errorf("%w: previous difficulty is incomplete", ErrInvalidRequest) } } } // Accept only the first unfinished main quest in this difficulty, using // authoritative links rather than permitting an arbitrary reward jump. first := 0 for _, id := range s.storyCatalog.Packs[pack].MainQuestIDs { if !s.state.QuestCleared(id, pack, int(level)) { first = id break } } if quest != first || first == 0 { return 0, nil, true, ErrInvalidRequest } if err := s.state.SelectQuest(pack, progress.QuestSelection{QuestID: quest, Difficulty: int(level), Option: int(opt)}); err != nil { return 0, nil, true, err } if err := s.state.SetActivePackID(pack); err != nil { return 0, nil, true, err } s.setCurrentPack(pack) chars, decks, err := s.resolveActivePartyWires(pack, quest) if err != nil { return 0, nil, true, err } out := wire.AppendBytes(nil, 1, s.questInfoWire(pack, quest)) for _, char := range chars { out = wire.AppendBytes(out, 2, char) } for _, deck := range decks { out = wire.AppendBytes(out, 3, deck) } items, err := s.ensureQuestItems(pack, quest) if err != nil { return 0, nil, true, err } for _, item := range items { out = wire.AppendBytes(out, 4, player.ItemWire(item)) } return 17, out, true, nil } func (s *Service) activeSideQuestWires(packID int) [][]byte { var out [][]byte if s.storyCatalog == nil { return nil } for _, quest := range s.state.QuestsInPack(packID, 0) { if s.storyCatalog.Packs[packID].Quests[quest.QuestID].Type == 1 && !s.state.QuestCleared(quest.QuestID, packID) { out = append(out, s.questInfoWire(packID, quest.QuestID)) } } return out } func (s *Service) visibleOwnedCharacters(characters []player.Character) []player.Character { earned := s.state.QuestCleared(s.seed.BattleUnlockQuestID, s.seed.PackID) out := make([]player.Character, 0, len(characters)) for _, character := range characters { if !player.IsStoryCharacter(character) && (earned || s.seed.RewardCharacter.InvenIndex == 0 || character.InvenIndex != s.seed.RewardCharacter.InvenIndex) { out = append(out, character) } } return out } func (s *Service) ensureQuestItems(packID, questID int) ([]player.Item, error) { ids := s.storyCatalog.Packs[packID].Quests[questID].GiveQuestItemIDs if len(ids) == 0 { return nil, nil } if s.inventory == nil { return nil, fmt.Errorf("world: quest item inventory unavailable") } var rewards []gamedata.BattleReward for _, id := range ids { rewards = append(rewards, gamedata.BattleReward{Type: 13, ID: id, Count: 1}) } identity := questRewardIdentity(packID, questID, s.questDifficultyFor(packID, questID)) + ":give-items" items, err := s.inventory.GrantOnce(identity, rewards) if err == nil && len(items) == 0 { items = s.inventory.GrantedItems(identity) } return items, err } func (s *Service) AttachBattleActive(provider func() bool) error { if provider == nil { return fmt.Errorf("world: nil battle activity provider") } s.battleActive = provider return nil }