Files
bd2/go/internal/server/world/quest_selection.go
T

261 lines
8.2 KiB
Go

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
}