feat(all): implement SkyWay progression, battles and dispatch; fix field reward position ordering
This commit is contained in:
@@ -3,7 +3,11 @@ package app
|
|||||||
import (
|
import (
|
||||||
"bd2server/internal/server/design/gamedata"
|
"bd2server/internal/server/design/gamedata"
|
||||||
"bd2server/internal/server/domain/battle/monsterhunt"
|
"bd2server/internal/server/domain/battle/monsterhunt"
|
||||||
|
"bd2server/internal/server/domain/command"
|
||||||
"bd2server/internal/server/domain/commerce"
|
"bd2server/internal/server/domain/commerce"
|
||||||
|
"bd2server/internal/server/domain/world"
|
||||||
|
"bd2server/internal/server/protocol/wire"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +19,8 @@ type designCatalog struct {
|
|||||||
fieldBuffs map[uint64]gamedata.FieldBuffDesign
|
fieldBuffs map[uint64]gamedata.FieldBuffDesign
|
||||||
recovery *gamedata.PackRecoveryPolicy
|
recovery *gamedata.PackRecoveryPolicy
|
||||||
researchCharacters map[uint64]bool
|
researchCharacters map[uint64]bool
|
||||||
overwhelmSky []gamedata.SkyWayOverwhelmRule
|
skyway *gamedata.SkyWayDesign
|
||||||
|
skywaySchedules []world.SkyWaySchedule
|
||||||
eventPlay *gamedata.EventPlayCatalog
|
eventPlay *gamedata.EventPlayCatalog
|
||||||
monsterHunt *monsterhunt.Rules
|
monsterHunt *monsterhunt.Rules
|
||||||
presetDesign *gamedata.PresetDesign
|
presetDesign *gamedata.PresetDesign
|
||||||
@@ -345,7 +350,50 @@ func loadDesign(c *configuration, seeds *seedCatalog) (*designCatalog, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
d.overwhelmSky, err = gamedata.LoadSkyWayOverwhelm(gameData, gameDataVersion)
|
d.skyway, err = gamedata.LoadSkyWayDesign(gameData, gameDataVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, schedule, handled, err := seeds.defaults.Handle(command.Context{}, "/SkyWayScheduleInfo", wire.AppendVarint(nil, 1, 1))
|
||||||
|
if err != nil || !handled {
|
||||||
|
return nil, fmt.Errorf("load SkyWay schedule policy: %w", err)
|
||||||
|
}
|
||||||
|
err = wire.Walk(schedule, func(f wire.Field) error {
|
||||||
|
if f.Number != 1 || f.Type != 2 {
|
||||||
|
return fmt.Errorf("invalid SkyWay schedule seed")
|
||||||
|
}
|
||||||
|
var row world.SkyWaySchedule
|
||||||
|
row.Group, _, err = wire.Varint(f.Value, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
row.Bonus, _, err = wire.Varint(f.Value, 3)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = wire.Walk(f.Value, func(day wire.Field) error {
|
||||||
|
if day.Number != 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if day.Type != 0 && day.Type != 2 {
|
||||||
|
return fmt.Errorf("invalid SkyWay day seed")
|
||||||
|
}
|
||||||
|
for rest := day.Value; len(rest) > 0; {
|
||||||
|
v, n := binary.Uvarint(rest)
|
||||||
|
if n <= 0 {
|
||||||
|
return wire.ErrMalformed
|
||||||
|
}
|
||||||
|
row.Days = append(row.Days, v)
|
||||||
|
rest = rest[n:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.skywaySchedules = append(d.skywaySchedules, row)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,5 +180,11 @@ func (p *playerAssembly) events(ctx command.Context) error {
|
|||||||
return fmt.Errorf("restore recorded achievement history: %w", err)
|
return fmt.Errorf("restore recorded achievement history: %w", err)
|
||||||
}
|
}
|
||||||
p.eventTasksService.AttachInventoryProvider(&eventtasks.InventoryProjection{Items: p.ownedItems, Equipment: p.ownedEquipment, Costumes: p.collection, StateVersion: p.stateRepository.ObservationVersion})
|
p.eventTasksService.AttachInventoryProvider(&eventtasks.InventoryProjection{Items: p.ownedItems, Equipment: p.ownedEquipment, Costumes: p.collection, StateVersion: p.stateRepository.ObservationVersion})
|
||||||
|
p.worldService.AttachSkyWayProgress(p.eventTasksService.RecordSkyWayClear, func(ctx command.Context, group uint64) error {
|
||||||
|
return p.missionService.RecordEvent(ctx, 227, group, 1, p.worldService.MissionsUnlocked)
|
||||||
|
})
|
||||||
|
p.huntingService.AttachDispatchProgress(func(ctx command.Context, count uint64) error {
|
||||||
|
return p.missionService.RecordEvent(ctx, 23, 0, count, p.worldService.MissionsUnlocked)
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ func (p *playerAssembly) gameplay(ctx command.Context) error {
|
|||||||
return fmt.Errorf("load event economy: %w", err)
|
return fmt.Errorf("load event economy: %w", err)
|
||||||
}
|
}
|
||||||
p.eventEconomy.AttachHuntingAP(p.huntingService)
|
p.eventEconomy.AttachHuntingAP(p.huntingService)
|
||||||
|
if err = p.worldService.AttachSkyWay(ctx, p.design.skyway, p.gameplayStore, p.eventEconomy, p.huntingService, p.design.skywaySchedules, p.design.rewardGraph); err != nil {
|
||||||
|
return fmt.Errorf("attach SkyWay: %w", err)
|
||||||
|
}
|
||||||
|
p.battleService.AttachSkyWay(p.worldService)
|
||||||
|
p.huntingService.AttachDispatchEligibility(p.worldService.SkyWayDispatchEligibility)
|
||||||
|
p.huntingService.AttachSkyWayDispatch(p.worldService.SkyWayDispatchCosts, p.worldService.SkyWayDispatchExchange, p.worldService.SkyWayDispatchBonus)
|
||||||
p.talentUseService, err = roster.NewTalentUseService(p.design.talentUseDesign, p.gameplayStore, p.worldService.CharacterService(), p.ownedItems, p.wallet, p.eventEconomy)
|
p.talentUseService, err = roster.NewTalentUseService(p.design.talentUseDesign, p.gameplayStore, p.worldService.CharacterService(), p.ownedItems, p.wallet, p.eventEconomy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load field talent state: %w", err)
|
return fmt.Errorf("load field talent state: %w", err)
|
||||||
@@ -91,7 +97,7 @@ func (p *playerAssembly) gameplay(ctx command.Context) error {
|
|||||||
p.worldService.AttachTalentPackInfo(p.talentUseService.PackInfo)
|
p.worldService.AttachTalentPackInfo(p.talentUseService.PackInfo)
|
||||||
p.worldService.AttachOverwhelmAuthorization(p.talentUseService.ConsumeOverwhelm)
|
p.worldService.AttachOverwhelmAuthorization(p.talentUseService.ConsumeOverwhelm)
|
||||||
p.worldService.AttachOverwhelmHunting(p.huntingService)
|
p.worldService.AttachOverwhelmHunting(p.huntingService)
|
||||||
if err := p.worldService.AttachOverwhelmDesign(p.design.source, p.design.overwhelmSky); err != nil {
|
if err := p.worldService.AttachOverwhelmDesign(p.design.source); err != nil {
|
||||||
return fmt.Errorf("attach overwhelm design: %w", err)
|
return fmt.Errorf("attach overwhelm design: %w", err)
|
||||||
}
|
}
|
||||||
p.talentUseService.AttachEffect(4, p.worldService.ApplyTalentFieldAbsorb)
|
p.talentUseService.AttachEffect(4, p.worldService.ApplyTalentFieldAbsorb)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
func (p *playerAssembly) session(ctx command.Context) error {
|
func (p *playerAssembly) session(ctx command.Context) error {
|
||||||
p.handlers = []session.Handler{
|
p.handlers = []session.Handler{
|
||||||
|
p.worldService,
|
||||||
p.progressState,
|
p.progressState,
|
||||||
p.cashService,
|
p.cashService,
|
||||||
p.cashBonuses,
|
p.cashBonuses,
|
||||||
@@ -35,7 +36,6 @@ func (p *playerAssembly) session(ctx command.Context) error {
|
|||||||
p.battleService,
|
p.battleService,
|
||||||
p.huntingService,
|
p.huntingService,
|
||||||
p.monsterHuntService,
|
p.monsterHuntService,
|
||||||
p.worldService,
|
|
||||||
p.worldService.CharacterService(),
|
p.worldService.CharacterService(),
|
||||||
p.progressState,
|
p.progressState,
|
||||||
p.deckStateStore,
|
p.deckStateStore,
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"bd2server/internal/server/design/gamedata"
|
||||||
|
"bd2server/internal/server/domain/command"
|
||||||
|
"bd2server/internal/server/protocol/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type skyWayPlayerScenario struct {
|
||||||
|
factory *PlayerFactory
|
||||||
|
owner *playerInstance
|
||||||
|
account string
|
||||||
|
sequence uint64
|
||||||
|
blue []byte
|
||||||
|
}
|
||||||
|
type skyWayAssets struct{ gold, rice, torch, crystal uint64 }
|
||||||
|
|
||||||
|
func newSkyWayPlayerScenario(t *testing.T) *skyWayPlayerScenario {
|
||||||
|
t.Helper()
|
||||||
|
factory, accounts := newIntegrationFactory(t)
|
||||||
|
for i := range factory.design.skywaySchedules {
|
||||||
|
if factory.design.skywaySchedules[i].Group == 1 {
|
||||||
|
factory.design.skywaySchedules[i].Bonus = 100
|
||||||
|
factory.design.skywaySchedules[i].Days = []uint64{0, 1, 2, 3, 4, 5, 6}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owner, err := factory.open(accounts[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := &skyWayPlayerScenario{factory: factory, owner: owner, account: accounts[0]}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if s.owner != nil {
|
||||||
|
if err := s.owner.Close(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tx, err := owner.repository.BeginCommand(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
ctx := command.Context{Identity: command.Identity{AccountID: s.account, SessionID: "skyway-client"}, State: tx}
|
||||||
|
for _, character := range owner.assembly.worldService.CharacterService().RawAll() {
|
||||||
|
costume, owned := owner.assembly.collection.CostumeByIndex(character.UseCostume)
|
||||||
|
if !owned || costume.UseChar != character.InvenIndex || character.HP == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.blue = wire.AppendVarint(nil, 1, 1)
|
||||||
|
for field, value := range map[int]uint64{2: character.InvenIndex, 3: character.ID, 4: character.HP, 5: character.Level, 6: character.UseCostume, 7: costume.ID} {
|
||||||
|
s.blue = wire.AppendVarint(s.blue, field, value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if s.blue == nil {
|
||||||
|
t.Fatal("SkyWay scenario requires an owned living battle participant")
|
||||||
|
}
|
||||||
|
if _, err := owner.assembly.ownedItems.GrantOnce(ctx, "skyway-entry-ticket", []gamedata.BattleReward{{Type: 19, ID: 13004, Count: 1}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := owner.assembly.eventEconomy.Apply(ctx, "skyway-test-ap", nil, []gamedata.Reward{{Type: 21, Count: 200}, {Type: 32, Count: 200}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
beforePurchase := s.assets(t)
|
||||||
|
purchase := s.request(3004)
|
||||||
|
s.call(t, "/PackBuy", purchase)
|
||||||
|
bought := beforePurchase
|
||||||
|
bought.rice += 10
|
||||||
|
if s.assets(t) != bought {
|
||||||
|
t.Fatal("the first SkyWay pack purchase did not credit its ten rice")
|
||||||
|
}
|
||||||
|
s.call(t, "/PackBuy", purchase)
|
||||||
|
if s.assets(t) != bought {
|
||||||
|
t.Fatal("pack purchase retry duplicated rice")
|
||||||
|
}
|
||||||
|
s.call(t, "/PackInGameInfo", s.request(3004))
|
||||||
|
for _, step := range []struct {
|
||||||
|
quest uint64
|
||||||
|
talks []uint64
|
||||||
|
}{{1, []uint64{11}}, {2, []uint64{21, 22}}, {3, []uint64{31}}} {
|
||||||
|
update := s.request(step.quest, 3004)
|
||||||
|
for _, talk := range step.talks {
|
||||||
|
update = wire.AppendVarint(update, 4, talk)
|
||||||
|
}
|
||||||
|
s.call(t, "/QuestUpdate", update)
|
||||||
|
clear := s.request(step.quest, 3004)
|
||||||
|
response := s.call(t, "/QuestClear", clear)
|
||||||
|
if step.quest == 3 {
|
||||||
|
complete := false
|
||||||
|
for _, row := range appRows(response, 11) {
|
||||||
|
if appValue(row, 1) == 3004 && appValue(row, 3) == 1 {
|
||||||
|
complete = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !complete {
|
||||||
|
t.Fatal("final dialogue did not unlock dispatch in the client's pack cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bought.gold += 500
|
||||||
|
if s.assets(t) != bought {
|
||||||
|
t.Fatal("introductory dialogue did not settle its authored gold")
|
||||||
|
}
|
||||||
|
s.call(t, "/QuestClear", clear)
|
||||||
|
if s.assets(t) != bought {
|
||||||
|
t.Fatal("introductory dialogue retry duplicated rewards")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) request(values ...uint64) []byte {
|
||||||
|
s.sequence++
|
||||||
|
raw := wire.AppendVarint(nil, 1, s.sequence)
|
||||||
|
for i, value := range values {
|
||||||
|
raw = wire.AppendVarint(raw, i+2, value)
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) execute(path string, raw []byte) ([]byte, error) {
|
||||||
|
c := registryCommand(s.account, fmt.Sprintf("skyway-%s-%x", path, raw), path, raw)
|
||||||
|
c.Identity.SessionID = "skyway-client"
|
||||||
|
reply, err := s.owner.Execute(context.Background(), c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(reply.Responses) != 1 {
|
||||||
|
return nil, fmt.Errorf("%s has no response", path)
|
||||||
|
}
|
||||||
|
return reply.Responses[0].Body, nil
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) call(t *testing.T, path string, raw []byte) []byte {
|
||||||
|
t.Helper()
|
||||||
|
body, err := s.execute(path, raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", path, err)
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) assets(t *testing.T) skyWayAssets {
|
||||||
|
t.Helper()
|
||||||
|
tx, err := s.owner.repository.BeginCommand(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
ctx := command.Context{Identity: command.Identity{AccountID: s.account, SessionID: "skyway-client"}, State: tx}
|
||||||
|
free, bonus, err := s.owner.assembly.huntingService.HuntingAP(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
extra, err := s.owner.assembly.eventEconomy.AdditionalCurrencies(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out := skyWayAssets{gold: s.owner.assembly.wallet.Snapshot(ctx).Gold, rice: free + bonus, torch: extra[36] + extra[37]}
|
||||||
|
for _, item := range s.owner.assembly.ownedItems.All(ctx) {
|
||||||
|
if item.Type == 8 && item.ID == 111 {
|
||||||
|
out.crystal += item.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) reject(t *testing.T, path string, raw []byte) {
|
||||||
|
t.Helper()
|
||||||
|
before := s.assets(t)
|
||||||
|
if _, err := s.execute(path, raw); err == nil {
|
||||||
|
t.Fatalf("%s accepted an unavailable encounter", path)
|
||||||
|
}
|
||||||
|
if after := s.assets(t); after != before {
|
||||||
|
t.Fatalf("rejected %s changed assets: before=%+v after=%+v", path, before, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) battle(t *testing.T, monster, mode, result uint64) ([]byte, []byte) {
|
||||||
|
t.Helper()
|
||||||
|
s.call(t, "/BattleEnter", s.request(0, monster, monster, mode))
|
||||||
|
s.call(t, "/BattleStart", wire.AppendBytes(s.request(monster), 5, s.blue))
|
||||||
|
end := s.request(result)
|
||||||
|
return s.call(t, "/BattleEnd", end), end
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) maximum(t *testing.T, group uint64) int64 {
|
||||||
|
t.Helper()
|
||||||
|
info := s.call(t, "/SkyWayInfo", s.request(3004))
|
||||||
|
for _, row := range appRows(info, 1) {
|
||||||
|
if appValue(row, 2) == group {
|
||||||
|
return int64(appValue(row, 4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("SkyWayInfo omitted selected group %d", group)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
func (s *skyWayPlayerScenario) reopen(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if err := s.owner.repository.RequireDomains("bootstrap", "characters", "collection", "deck", "equipment", "items", "mail", "missions", "progress", "wallet"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
problems, err := s.owner.repository.Validate()
|
||||||
|
if err != nil || len(problems) != 0 {
|
||||||
|
t.Fatalf("persisted player validation: %v %v", problems, err)
|
||||||
|
}
|
||||||
|
if err := s.owner.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s.owner = nil
|
||||||
|
s.owner, err = s.factory.open(s.account)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GameData group1/10 has five 1-rice encounters (1000 gold each) and a
|
||||||
|
// 1-rice boss (1500 gold). The configured 100% policy doubles concrete payouts.
|
||||||
|
func TestSkyWayGoblinBossUnlockAndRepeatedRunsConserveRewards(t *testing.T) {
|
||||||
|
s := newSkyWayPlayerScenario(t)
|
||||||
|
initial := s.assets(t)
|
||||||
|
if s.maximum(t, 1) != -1 {
|
||||||
|
t.Fatal("unplayed dungeon was advertised as cleared")
|
||||||
|
}
|
||||||
|
s.reject(t, "/SkyWayEnter", s.request(3004, 1, 11, 0))
|
||||||
|
enter := s.request(3004, 1, 10, 0)
|
||||||
|
enterReply := s.call(t, "/SkyWayEnter", enter)
|
||||||
|
if s.assets(t) != initial {
|
||||||
|
t.Fatal("entering a dungeon spent battle AP")
|
||||||
|
}
|
||||||
|
s.reject(t, "/BattleEnter", s.request(0, 2005, 2005, 9))
|
||||||
|
s.battle(t, 2001, 9, 2)
|
||||||
|
if s.assets(t) != initial {
|
||||||
|
t.Fatal("losing the encounter charged AP or granted gold")
|
||||||
|
}
|
||||||
|
for i, monster := range []uint64{2001, 2002, 2003, 2004, 2006} {
|
||||||
|
var response, end []byte
|
||||||
|
if i == 0 {
|
||||||
|
s.call(t, "/BattleRetry", s.request(monster))
|
||||||
|
if s.assets(t) != initial {
|
||||||
|
t.Fatal("retrying a lost battle charged AP before victory")
|
||||||
|
}
|
||||||
|
s.call(t, "/BattleStart", wire.AppendBytes(s.request(monster), 5, s.blue))
|
||||||
|
end = s.request(1)
|
||||||
|
response = s.call(t, "/BattleEnd", end)
|
||||||
|
} else {
|
||||||
|
response, end = s.battle(t, monster, 9, 1)
|
||||||
|
}
|
||||||
|
expected := initial
|
||||||
|
expected.gold += uint64(i+1) * 2000
|
||||||
|
expected.rice -= uint64(i + 1)
|
||||||
|
if after := s.assets(t); after != expected {
|
||||||
|
t.Fatalf("victory assets: got %+v want %+v", after, expected)
|
||||||
|
}
|
||||||
|
if retry := s.call(t, "/BattleEnd", end); !bytes.Equal(response, retry) || s.assets(t) != expected {
|
||||||
|
t.Fatal("settlement retry charged or rewarded the player twice")
|
||||||
|
}
|
||||||
|
if i == 1 {
|
||||||
|
s.reopen(t)
|
||||||
|
command := registryCommand(s.account, "uncached-enter-retry", "/SkyWayEnter", enter)
|
||||||
|
command.Identity.SessionID = "skyway-client"
|
||||||
|
replay, err := s.owner.Execute(context.Background(), command)
|
||||||
|
if err != nil || len(replay.Responses) != 1 || !bytes.Equal(replay.Responses[0].Body, enterReply) || s.assets(t) != expected {
|
||||||
|
t.Fatalf("persisted native entry retry reset or charged the dungeon: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.maximum(t, 1) != -1 {
|
||||||
|
t.Fatal("normal encounters unlocked the next difficulty before the boss")
|
||||||
|
}
|
||||||
|
s.battle(t, 2005, 9, 1)
|
||||||
|
afterCycle := initial
|
||||||
|
afterCycle.gold += 13000
|
||||||
|
afterCycle.rice -= 6
|
||||||
|
if s.assets(t) != afterCycle || s.maximum(t, 1) != 0 {
|
||||||
|
t.Fatal("boss did not settle one complete run or unlock hard difficulty")
|
||||||
|
}
|
||||||
|
s.reopen(t)
|
||||||
|
if s.assets(t) != afterCycle || s.maximum(t, 1) != 0 {
|
||||||
|
t.Fatal("reopen lost completed dungeon assets or difficulty")
|
||||||
|
}
|
||||||
|
s.battle(t, 2001, 9, 1)
|
||||||
|
repeated := afterCycle
|
||||||
|
repeated.gold += 2000
|
||||||
|
repeated.rice--
|
||||||
|
if s.assets(t) != repeated {
|
||||||
|
t.Fatal("the new run reused the preceding run's reward identity")
|
||||||
|
}
|
||||||
|
s.call(t, "/SkyWayEnter", s.request(3004, 1, 11, 0))
|
||||||
|
if s.assets(t) != repeated {
|
||||||
|
t.Fatal("selecting the unlocked hard dungeon spent battle AP")
|
||||||
|
}
|
||||||
|
safe := wire.AppendBytes(s.request(3004), 3, []byte(`{"MapId":30021,"PlayerPosition":{"x":0,"y":0,"z":0},"ColleaguePositions":[]}`))
|
||||||
|
s.call(t, "/SaveUserPosition", safe)
|
||||||
|
s.reject(t, "/BattleEnter", s.request(0, 2011, 2011, 9))
|
||||||
|
s.reopen(t)
|
||||||
|
if s.maximum(t, 1) != 0 || s.assets(t) != repeated {
|
||||||
|
t.Fatal("leaving the dungeon discarded permanent progress or assets")
|
||||||
|
}
|
||||||
|
s.call(t, "/SkyWayEnter", s.request(3004, 1, 11, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire normal encounters and boss each grant resource111 x2 and cost one torch;
|
||||||
|
// the instant two-run dispatch costs twelve torches and grants twenty-four.
|
||||||
|
func TestSkyWayCrystalDispatchUsesTorchesAndSurvivesRetryAndReopen(t *testing.T) {
|
||||||
|
s := newSkyWayPlayerScenario(t)
|
||||||
|
initial := s.assets(t)
|
||||||
|
dispatch := s.request(2, 7, 2)
|
||||||
|
s.reject(t, "/HuntDispatch", dispatch)
|
||||||
|
s.call(t, "/SkyWayEnter", s.request(3004, 3, 30, 0))
|
||||||
|
s.battle(t, 4001, 11, 2)
|
||||||
|
if s.assets(t) != initial {
|
||||||
|
t.Fatal("lost crystal battle spent torches or awarded crystals")
|
||||||
|
}
|
||||||
|
for _, monster := range []uint64{4001, 4002, 4003, 4004, 4006, 4005} {
|
||||||
|
s.battle(t, monster, 11, 1)
|
||||||
|
}
|
||||||
|
afterCycle := initial
|
||||||
|
afterCycle.torch -= 6
|
||||||
|
afterCycle.crystal += 12
|
||||||
|
if s.assets(t) != afterCycle || s.maximum(t, 3) != 0 {
|
||||||
|
t.Fatal("crystal dungeon charged rice or failed to persist its rewards")
|
||||||
|
}
|
||||||
|
dispatch = s.request(2, 7, 2)
|
||||||
|
reply := s.call(t, "/HuntDispatch", dispatch)
|
||||||
|
afterDispatch := afterCycle
|
||||||
|
afterDispatch.torch -= 12
|
||||||
|
afterDispatch.crystal += 24
|
||||||
|
if s.assets(t) != afterDispatch {
|
||||||
|
t.Fatalf("instant dispatch charged the wrong currency or quantity: %+v want %+v", s.assets(t), afterDispatch)
|
||||||
|
}
|
||||||
|
if repeated := s.call(t, "/HuntDispatch", dispatch); !bytes.Equal(reply, repeated) || s.assets(t) != afterDispatch {
|
||||||
|
t.Fatal("dispatch retry duplicated rewards or torch charges")
|
||||||
|
}
|
||||||
|
s.reopen(t)
|
||||||
|
if s.assets(t) != afterDispatch || s.maximum(t, 3) != 0 {
|
||||||
|
t.Fatal("reopen lost crystal rewards, torch balance or dispatch qualification")
|
||||||
|
}
|
||||||
|
if repeated := s.call(t, "/HuntDispatch", dispatch); !bytes.Equal(reply, repeated) || s.assets(t) != afterDispatch {
|
||||||
|
t.Fatal("reopened dispatch duplicated assets")
|
||||||
|
}
|
||||||
|
s.call(t, "/HuntDispatchStart", s.request(2, 7, 2))
|
||||||
|
reserved := afterDispatch
|
||||||
|
reserved.torch -= 12
|
||||||
|
if s.assets(t) != reserved {
|
||||||
|
t.Fatal("timed dispatch did not reserve the correct torch cost")
|
||||||
|
}
|
||||||
|
preview := s.call(t, "/HuntDispatchRewardPreview", s.request(2, 7))
|
||||||
|
if appValue(preview, 1) != 0 || s.assets(t) != reserved {
|
||||||
|
t.Fatal("preview completed an unplayed dispatch or issued assets")
|
||||||
|
}
|
||||||
|
cancel := s.request(2, 7)
|
||||||
|
cancelReply := s.call(t, "/HuntDispatchEnd", cancel)
|
||||||
|
if s.assets(t) != afterDispatch {
|
||||||
|
t.Fatal("cancelling unplayed runs did not refund exactly the reserved torches")
|
||||||
|
}
|
||||||
|
s.reopen(t)
|
||||||
|
if retry := s.call(t, "/HuntDispatchEnd", cancel); !bytes.Equal(retry, cancelReply) || s.assets(t) != afterDispatch {
|
||||||
|
t.Fatal("reopened cancellation refunded torches twice")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
type DispatchDesign struct {
|
type DispatchDesign struct {
|
||||||
Group, ID, Pack, Difficulty, TypeGroup, TypeID, AP, ClearTime, Growth uint64
|
Group, ID, Pack, Difficulty, TypeGroup, TypeID, AP, ClearTime, Growth uint64
|
||||||
GroundID uint64
|
GroundID, APType uint64
|
||||||
Rewards []BattleReward
|
Rewards []BattleReward
|
||||||
boxes map[uint64]*dispatchGroup
|
boxes map[uint64]*dispatchGroup
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,7 @@ func LoadDispatchDesign(root, version string, group, id uint64) (*DispatchDesign
|
|||||||
}
|
}
|
||||||
ids, err = packedInts(raw, 15)
|
ids, err = packedInts(raw, 15)
|
||||||
boss, _ = optionalScalar(raw, 4)
|
boss, _ = optionalScalar(raw, 4)
|
||||||
|
d.APType, _ = optionalScalar(raw, 1)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func loadFieldPacks(db *sql.DB) (map[int]FieldPack, error) {
|
|||||||
values[field] = v[0]
|
values[field] = v[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if values[55] != 3 && values[55] != 10 {
|
if values[55] != 3 && values[55] != 5 && values[55] != 10 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if id <= 0 || values[25] != uint64(id) {
|
if id <= 0 || values[25] != uint64(id) {
|
||||||
|
|||||||
@@ -4,51 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SkyWayOverwhelmRule struct {
|
|
||||||
Group, Map, APType, Boss, BossAP uint64
|
|
||||||
Monsters, AP []uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadSkyWayOverwhelm(root, version string) ([]SkyWayOverwhelmRule, error) {
|
|
||||||
db, done, e := openStatDatabase(root, version)
|
|
||||||
if e != nil {
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
defer done()
|
|
||||||
rows, e := db.Query("SELECT ProtoBuf FROM SkyWayFieldTable")
|
|
||||||
if e != nil {
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
var out []SkyWayOverwhelmRule
|
|
||||||
for rows.Next() {
|
|
||||||
var b []byte
|
|
||||||
if e = rows.Scan(&b); e != nil {
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
r := SkyWayOverwhelmRule{}
|
|
||||||
for f, p := range map[int]*uint64{10: &r.Group, 13: &r.Map, 1: &r.APType, 4: &r.Boss, 3: &r.BossAP} {
|
|
||||||
*p, e = optionalScalar(b, f)
|
|
||||||
if e != nil {
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.Monsters, e = packedInts(b, 15)
|
|
||||||
if e != nil {
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
r.AP, e = packedInts(b, 14)
|
|
||||||
if e != nil {
|
|
||||||
return nil, e
|
|
||||||
}
|
|
||||||
if len(r.Monsters) != len(r.AP) {
|
|
||||||
return nil, fmt.Errorf("gamedata: malformed skyway AP mapping")
|
|
||||||
}
|
|
||||||
out = append(out, r)
|
|
||||||
}
|
|
||||||
return out, rows.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
type OverwhelmQuestRule struct {
|
type OverwhelmQuestRule struct {
|
||||||
Type, Count uint64
|
Type, Count uint64
|
||||||
Targets []uint64
|
Targets []uint64
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package gamedata
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type SkyWayStage struct {
|
||||||
|
Group, ID, Difficulty, Map, Point, APType, Boss, BossAP, PositionGroup uint64
|
||||||
|
Monsters, AP []uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SkyWayDesign struct {
|
||||||
|
Pack int
|
||||||
|
Stages []SkyWayStage
|
||||||
|
Monsters map[uint64]HuntingMonster
|
||||||
|
SafeMaps map[int]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSkyWayDesign(root, version string) (*SkyWayDesign, error) {
|
||||||
|
db, done, err := openStatDatabase(root, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer done()
|
||||||
|
d := &SkyWayDesign{Monsters: map[uint64]HuntingMonster{}, SafeMaps: map[int]bool{}}
|
||||||
|
rows, err := db.Query("SELECT ProtoBuf FROM SkyWayFieldTable ORDER BY groupId,difficulty")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var raw []byte
|
||||||
|
if err = rows.Scan(&raw); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var s SkyWayStage
|
||||||
|
for f, p := range map[int]*uint64{1: &s.APType, 3: &s.BossAP, 4: &s.Boss, 6: &s.Difficulty, 10: &s.Group, 12: &s.ID, 13: &s.Map, 17: &s.Point, 18: &s.PositionGroup} {
|
||||||
|
*p, err = optionalScalar(raw, f)
|
||||||
|
if err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Monsters, err = packedInts(raw, 15)
|
||||||
|
if err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.AP, err = packedInts(raw, 14)
|
||||||
|
if err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.Group == 0 || s.Group > 7 || s.ID == 0 || s.Boss == 0 || s.Map == 0 || len(s.AP) != len(s.Monsters) || (s.APType != 1 && s.APType != 2) {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, fmt.Errorf("gamedata: invalid SkyWay stage")
|
||||||
|
}
|
||||||
|
d.Stages = append(d.Stages, s)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, s := range d.Stages {
|
||||||
|
var pack int
|
||||||
|
if err = db.QueryRow("SELECT packId FROM MapTable WHERE id=?", s.Map).Scan(&pack); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if d.Pack != 0 && d.Pack != pack {
|
||||||
|
return nil, fmt.Errorf("gamedata: SkyWay maps span packs")
|
||||||
|
}
|
||||||
|
d.Pack = pack
|
||||||
|
}
|
||||||
|
if d.Pack == 0 {
|
||||||
|
return nil, fmt.Errorf("gamedata: no SkyWay pack")
|
||||||
|
}
|
||||||
|
rows, err = db.Query("SELECT id,ProtoBuf FROM MapTable WHERE packId=?", d.Pack)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var raw []byte
|
||||||
|
if err = rows.Scan(&id, &raw); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
typ, e := optionalScalar(raw, 22)
|
||||||
|
if e != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
if typ == 0 {
|
||||||
|
d.SafeMaps[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pdb, release, err := openPackDatabase(root, version, d.Pack)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer release()
|
||||||
|
for _, s := range d.Stages {
|
||||||
|
for _, id := range append(append([]uint64(nil), s.Monsters...), s.Boss) {
|
||||||
|
if _, ok := d.Monsters[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var raw []byte
|
||||||
|
if err = pdb.QueryRow("SELECT ProtoBuf FROM FieldMonsterTable WHERE id=?", id).Scan(&raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := HuntingMonster{ID: id, Rewards: map[uint64][]BattleReward{}}
|
||||||
|
m.Type, err = optionalScalar(raw, 30)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.Decks, err = packedInts(raw, 2)
|
||||||
|
if err != nil || len(m.Decks) == 0 {
|
||||||
|
return nil, fmt.Errorf("gamedata: SkyWay monster missing decks")
|
||||||
|
}
|
||||||
|
for _, deck := range m.Decks {
|
||||||
|
if err = pdb.QueryRow("SELECT ProtoBuf FROM BattleDeckTable WHERE id=?", deck).Scan(&raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rs, e := parallelRewards(raw, 36, 34, 33)
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
for _, r := range rs {
|
||||||
|
m.Rewards[deck] = append(m.Rewards[deck], BattleReward(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.Monsters[id] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SkyWayDesign) Stage(group, id uint64) (SkyWayStage, bool) {
|
||||||
|
for _, s := range d.Stages {
|
||||||
|
if s.Group == group && s.ID == id {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SkyWayStage{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battle modes are the current protocol's contiguous SKY_WAY enum family.
|
||||||
|
func SkyWayMode(group uint64) uint64 { return 8 + group }
|
||||||
|
func IsSkyWayMode(mode uint64) bool { return mode >= 9 && mode <= 15 }
|
||||||
@@ -70,7 +70,7 @@ func loadStoryCatalog(db *sql.DB) (*StoryCatalog, error) {
|
|||||||
_ = rows.Close()
|
_ = rows.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if kind != 0 && kind != 1000 {
|
if kind != 0 && kind != 5 && kind != 1000 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
protoID, err := read(25)
|
protoID, err := read(25)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type Service struct {
|
|||||||
onMonsterWin func(ctx command.Context) error
|
onMonsterWin func(ctx command.Context) error
|
||||||
commitHealth func(ctx command.Context, _ map[uint64]uint64) error
|
commitHealth func(ctx command.Context, _ map[uint64]uint64) error
|
||||||
hunting HuntingRuntime
|
hunting HuntingRuntime
|
||||||
|
skyway SkyWayRuntime
|
||||||
monsterHunt MonsterHuntRuntime
|
monsterHunt MonsterHuntRuntime
|
||||||
eventBattles []EventBattleRuntime
|
eventBattles []EventBattleRuntime
|
||||||
fieldMonsters FieldMonsterRuntime
|
fieldMonsters FieldMonsterRuntime
|
||||||
@@ -83,6 +84,14 @@ type HuntingRuntime interface {
|
|||||||
CompleteBattle(ctx command.Context, pack int, mode, monster, deck uint64, receipt string) ([]byte, [][]byte, error)
|
CompleteBattle(ctx command.Context, pack int, mode, monster, deck uint64, receipt string) ([]byte, [][]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SkyWayRuntime interface {
|
||||||
|
SkyWayBeginBattle(command.Context, int, uint64, uint64, uint64) (string, error)
|
||||||
|
SkyWayCompleteBattle(command.Context, int, uint64, uint64, uint64, string, string) ([]byte, []byte, [][]byte, error)
|
||||||
|
SkyWayBattleStarted(command.Context, uint64, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) AttachSkyWay(runtime SkyWayRuntime) { s.skyway = runtime }
|
||||||
|
|
||||||
const huntingGroundMode = 5
|
const huntingGroundMode = 5
|
||||||
|
|
||||||
func (s *Service) AttachHunting(runtime HuntingRuntime) {
|
func (s *Service) AttachHunting(runtime HuntingRuntime) {
|
||||||
@@ -91,6 +100,7 @@ func (s *Service) AttachHunting(runtime HuntingRuntime) {
|
|||||||
|
|
||||||
type battleState struct {
|
type battleState struct {
|
||||||
entered bool
|
entered bool
|
||||||
|
retryable bool
|
||||||
index uint64
|
index uint64
|
||||||
round uint64
|
round uint64
|
||||||
monster uint64
|
monster uint64
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package hunting
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bd2server/internal/server/design/gamedata"
|
"bd2server/internal/server/design/gamedata"
|
||||||
|
"bd2server/internal/server/domain/command"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,11 +21,18 @@ type dispatchState struct {
|
|||||||
Receipts map[string]dispatchReceipt `json:"receipts"`
|
Receipts map[string]dispatchReceipt `json:"receipts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AttachDispatchEligibility(check func(*gamedata.DispatchDesign) error) {
|
func (s *Service) AttachDispatchEligibility(check func(command.Context, *gamedata.DispatchDesign) error) {
|
||||||
|
|
||||||
s.dispatchEligibility = check
|
s.dispatchEligibility = check
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) AttachSkyWayDispatch(costs func(command.Context, *gamedata.DispatchDesign, uint64) ([]gamedata.Reward, error), exchange func(command.Context, string, []gamedata.Reward, []gamedata.Reward) ([]byte, error), bonus func(*gamedata.DispatchDesign, []gamedata.BattleReward) ([]gamedata.BattleReward, error)) {
|
||||||
|
s.dispatchCosts, s.dispatchExchange, s.dispatchBonus = costs, exchange, bonus
|
||||||
|
}
|
||||||
|
func (s *Service) AttachDispatchProgress(progress func(command.Context, uint64) error) {
|
||||||
|
s.dispatchProgress = progress
|
||||||
|
}
|
||||||
|
|
||||||
func dispatchPlayed(j dispatchJob, seconds uint64) uint64 {
|
func dispatchPlayed(j dispatchJob, seconds uint64) uint64 {
|
||||||
if seconds == 0 {
|
if seconds == 0 {
|
||||||
return j.Count
|
return j.Count
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ type Service struct {
|
|||||||
currentPack func(command.Context) (int, error)
|
currentPack func(command.Context) (int, error)
|
||||||
load func(int) (*gamedata.HuntingPack, error)
|
load func(int) (*gamedata.HuntingPack, error)
|
||||||
dispatchLoad func(uint64, uint64) (*gamedata.DispatchDesign, error)
|
dispatchLoad func(uint64, uint64) (*gamedata.DispatchDesign, error)
|
||||||
dispatchEligibility func(*gamedata.DispatchDesign) error
|
dispatchEligibility func(command.Context, *gamedata.DispatchDesign) error
|
||||||
|
dispatchCosts func(command.Context, *gamedata.DispatchDesign, uint64) ([]gamedata.Reward, error)
|
||||||
|
dispatchExchange func(command.Context, string, []gamedata.Reward, []gamedata.Reward) ([]byte, error)
|
||||||
|
dispatchBonus func(*gamedata.DispatchDesign, []gamedata.BattleReward) ([]gamedata.BattleReward, error)
|
||||||
|
dispatchProgress func(command.Context, uint64) error
|
||||||
eligibility func(ctx command.Context, _ int, _ uint64) error
|
eligibility func(ctx command.Context, _ int, _ uint64) error
|
||||||
grant func(command.Context, string, []gamedata.Reward) ([]byte, error)
|
grant func(command.Context, string, []gamedata.Reward) ([]byte, error)
|
||||||
apDesign *gamedata.HuntingAPDesign
|
apDesign *gamedata.HuntingAPDesign
|
||||||
|
|||||||
@@ -399,6 +399,14 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return code, nil, true, err
|
return code, nil, true, err
|
||||||
}
|
}
|
||||||
|
sky := d.TypeGroup != 0
|
||||||
|
if sky && (s.dispatchCosts == nil || s.dispatchExchange == nil || s.dispatchBonus == nil) {
|
||||||
|
return code, nil, true, fmt.Errorf("hunting: SkyWay dispatch runtime missing")
|
||||||
|
}
|
||||||
|
freeType, bonusType := uint64(21), uint64(23)
|
||||||
|
if sky && d.APType == 2 {
|
||||||
|
freeType, bonusType = 32, 33
|
||||||
|
}
|
||||||
var out []byte
|
var out []byte
|
||||||
if path == "/HuntDispatchRewardPreview" {
|
if path == "/HuntDispatchRewardPreview" {
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -411,10 +419,10 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
bonus := min(remaining, job.Bonus)
|
bonus := min(remaining, job.Bonus)
|
||||||
free := remaining - bonus
|
free := remaining - bonus
|
||||||
if free > 0 {
|
if free > 0 {
|
||||||
preview = append(preview, gamedata.BattleReward{Type: 21, Count: free})
|
preview = append(preview, gamedata.BattleReward{Type: freeType, Count: free})
|
||||||
}
|
}
|
||||||
if bonus > 0 {
|
if bonus > 0 {
|
||||||
preview = append(preview, gamedata.BattleReward{Type: 23, Count: bonus})
|
preview = append(preview, gamedata.BattleReward{Type: bonusType, Count: bonus})
|
||||||
}
|
}
|
||||||
bundle := dispatchPreview(preview)
|
bundle := dispatchPreview(preview)
|
||||||
for _, r := range preview {
|
for _, r := range preview {
|
||||||
@@ -435,23 +443,40 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
remaining := d.AP * (job.Count - played)
|
remaining := d.AP * (job.Count - played)
|
||||||
refundBonus := min(remaining, job.Bonus)
|
refundBonus := min(remaining, job.Bonus)
|
||||||
refundFree := remaining - refundBonus
|
refundFree := remaining - refundBonus
|
||||||
next := s.clone()
|
if !sky {
|
||||||
next.Free += refundFree
|
next := s.clone()
|
||||||
next.Bonus += refundBonus
|
next.Free += refundFree
|
||||||
if e := s.persist(ctx, next); e != nil {
|
next.Bonus += refundBonus
|
||||||
return code, nil, true, e
|
if e := s.persist(ctx, next); e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if refundFree > 0 {
|
if refundFree > 0 {
|
||||||
completed = append(completed, gamedata.BattleReward{Type: 21, Count: refundFree})
|
completed = append(completed, gamedata.BattleReward{Type: freeType, Count: refundFree})
|
||||||
}
|
}
|
||||||
if refundBonus > 0 {
|
if refundBonus > 0 {
|
||||||
completed = append(completed, gamedata.BattleReward{Type: 23, Count: refundBonus})
|
completed = append(completed, gamedata.BattleReward{Type: bonusType, Count: refundBonus})
|
||||||
|
}
|
||||||
|
var bundle []byte
|
||||||
|
var e error
|
||||||
|
if sky {
|
||||||
|
var rewards []gamedata.Reward
|
||||||
|
for _, row := range completed {
|
||||||
|
rewards = append(rewards, gamedata.Reward(row))
|
||||||
|
}
|
||||||
|
bundle, e = s.dispatchExchange(ctx, "dispatch:"+key, nil, rewards)
|
||||||
|
} else {
|
||||||
|
bundle, e = s.dispatchGrant(ctx, "dispatch:"+key, completed)
|
||||||
}
|
}
|
||||||
bundle, e := s.dispatchGrant(ctx, "dispatch:"+key, completed)
|
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return code, nil, true, e
|
return code, nil, true, e
|
||||||
}
|
}
|
||||||
out = wire.AppendBytes(out, 1, bundle)
|
out = wire.AppendBytes(out, 1, bundle)
|
||||||
|
if played > 0 && s.dispatchProgress != nil {
|
||||||
|
if e = s.dispatchProgress(ctx, played); e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
delete(ds.Jobs, jobKey)
|
delete(ds.Jobs, jobKey)
|
||||||
} else {
|
} else {
|
||||||
count, _, e := wire.Varint(req, 4)
|
count, _, e := wire.Varint(req, 4)
|
||||||
@@ -470,8 +495,8 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
return code, nil, true, fmt.Errorf("hunting: dispatch category already running")
|
return code, nil, true, fmt.Errorf("hunting: dispatch category already running")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.dispatchEligibility != nil {
|
if d.TypeGroup != 0 && s.dispatchEligibility != nil {
|
||||||
if e = s.dispatchEligibility(d); e != nil {
|
if e = s.dispatchEligibility(ctx, d); e != nil {
|
||||||
return code, nil, true, e
|
return code, nil, true, e
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -487,7 +512,14 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
return code, nil, true, fmt.Errorf("hunting: dispatch AP overflow")
|
return code, nil, true, fmt.Errorf("hunting: dispatch AP overflow")
|
||||||
}
|
}
|
||||||
cost := d.AP * count
|
cost := d.AP * count
|
||||||
if s.state.Free < cost && s.state.Bonus < cost-s.state.Free {
|
var skyCosts []gamedata.Reward
|
||||||
|
if sky {
|
||||||
|
skyCosts, e = s.dispatchCosts(ctx, d, count)
|
||||||
|
if e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sky && s.state.Free < cost && s.state.Bonus < cost-s.state.Free {
|
||||||
return code, nil, true, fmt.Errorf("hunting: insufficient hunting AP")
|
return code, nil, true, fmt.Errorf("hunting: insufficient hunting AP")
|
||||||
}
|
}
|
||||||
rewards, e := d.Roll(count, func(n uint64) (uint64, error) {
|
rewards, e := d.Roll(count, func(n uint64) (uint64, error) {
|
||||||
@@ -500,11 +532,29 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
if e != nil {
|
if e != nil {
|
||||||
return code, nil, true, e
|
return code, nil, true, e
|
||||||
}
|
}
|
||||||
|
if sky {
|
||||||
|
rewards, e = s.dispatchBonus(d, rewards)
|
||||||
|
if e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
next := s.clone()
|
next := s.clone()
|
||||||
free := min(cost, next.Free)
|
free := min(cost, next.Free)
|
||||||
bonus := cost - free
|
bonus := cost - free
|
||||||
next.Free -= free
|
if sky {
|
||||||
next.Bonus -= bonus
|
free, bonus = 0, 0
|
||||||
|
for _, c := range skyCosts {
|
||||||
|
switch c.Type {
|
||||||
|
case freeType:
|
||||||
|
free += c.Count
|
||||||
|
case bonusType:
|
||||||
|
bonus += c.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next.Free -= free
|
||||||
|
next.Bonus -= bonus
|
||||||
|
}
|
||||||
if path == "/HuntDispatchStart" {
|
if path == "/HuntDispatchStart" {
|
||||||
now := uint64(time.Now().UnixMilli())
|
now := uint64(time.Now().UnixMilli())
|
||||||
runs := make([][]gamedata.BattleReward, count)
|
runs := make([][]gamedata.BattleReward, count)
|
||||||
@@ -520,9 +570,30 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
return code, nil, true, e
|
return code, nil, true, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if sky {
|
||||||
|
for i, run := range runs {
|
||||||
|
runs[i], e = s.dispatchBonus(d, run)
|
||||||
|
if e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, e = s.dispatchExchange(ctx, "dispatch-charge:"+key, skyCosts, nil); e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
job = dispatchJob{Group: group, ID: id, Count: count, Start: now, End: now + d.ClearTime*count*1000, Free: free, Bonus: bonus, Rewards: rewards, Runs: runs}
|
job = dispatchJob{Group: group, ID: id, Count: count, Start: now, End: now + d.ClearTime*count*1000, Free: free, Bonus: bonus, Rewards: rewards, Runs: runs}
|
||||||
ds.Jobs[jobKey] = job
|
ds.Jobs[jobKey] = job
|
||||||
out = wire.AppendBytes(out, 1, dispatchJobWire(job))
|
out = wire.AppendBytes(out, 1, dispatchJobWire(job))
|
||||||
|
} else if sky {
|
||||||
|
var grants []gamedata.Reward
|
||||||
|
for _, row := range rewards {
|
||||||
|
grants = append(grants, gamedata.Reward(row))
|
||||||
|
}
|
||||||
|
bundle, e := s.dispatchExchange(ctx, "dispatch:"+key, skyCosts, grants)
|
||||||
|
if e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
|
out = wire.AppendBytes(out, 1, bundle)
|
||||||
} else {
|
} else {
|
||||||
beforeFree, beforeBonus := s.state.Free, s.state.Bonus
|
beforeFree, beforeBonus := s.state.Free, s.state.Bonus
|
||||||
bundle, e := s.dispatchGrant(ctx, "dispatch:"+key, rewards)
|
bundle, e := s.dispatchGrant(ctx, "dispatch:"+key, rewards)
|
||||||
@@ -549,8 +620,15 @@ func (s *Service) handleDispatch(ctx command.Context, path string, req []byte, s
|
|||||||
next.Bonus -= delta
|
next.Bonus -= delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e = s.persist(ctx, next); e != nil {
|
if !sky {
|
||||||
return code, nil, true, e
|
if e = s.persist(ctx, next); e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path == "/HuntDispatch" && s.dispatchProgress != nil {
|
||||||
|
if e = s.dispatchProgress(ctx, count); e != nil {
|
||||||
|
return code, nil, true, e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ds.Receipts[key] = dispatchReceipt{append([]byte(path), req...), out, code}
|
ds.Receipts[key] = dispatchReceipt{append([]byte(path), req...), out, code}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func checkSeq(request []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Handle(ctx command.Context, path string, request []byte) (int, []byte, bool, error) {
|
func (s *Service) Handle(ctx command.Context, path string, request []byte) (int, []byte, bool, error) {
|
||||||
if path != "/BattleEnter" && path != "/BattleStart" && path != "/BattleRetry" && path != "/BattleVerifyState" && path != "/BattleEnd" && path != "/BattleExit" && path != "/BattlePhaseChange" {
|
if path != "/BattleEnter" && path != "/BattleStart" && path != "/BattleRetry" && path != "/BattleVerifyState" && path != "/BattleEnd" && path != "/BattleEndTest" && path != "/BattleExit" && path != "/BattlePhaseChange" {
|
||||||
return 0, nil, false, nil
|
return 0, nil, false, nil
|
||||||
}
|
}
|
||||||
if err := checkSeq(request); err != nil {
|
if err := checkSeq(request); err != nil {
|
||||||
@@ -27,6 +27,11 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
state := s.stateLocked(ctx)
|
state := s.stateLocked(ctx)
|
||||||
|
endCode := 15
|
||||||
|
if path == "/BattleEndTest" {
|
||||||
|
path = "/BattleEnd"
|
||||||
|
endCode = 181
|
||||||
|
}
|
||||||
switch path {
|
switch path {
|
||||||
case "/BattlePhaseChange":
|
case "/BattlePhaseChange":
|
||||||
if !state.entered {
|
if !state.entered {
|
||||||
@@ -144,6 +149,15 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
}
|
}
|
||||||
var phases []gamedata.BattlePhase
|
var phases []gamedata.BattlePhase
|
||||||
fieldInstance := ""
|
fieldInstance := ""
|
||||||
|
if gamedata.IsSkyWayMode(mode) {
|
||||||
|
if s.skyway == nil {
|
||||||
|
return 0, nil, true, errors.New("battle: SkyWay runtime unavailable")
|
||||||
|
}
|
||||||
|
fieldInstance, err = s.skyway.SkyWayBeginBattle(ctx, packID, mode, monster, deck)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if mode == 2 && eventRuntime == nil && s.fieldMonsters != nil && monster != 0 {
|
if mode == 2 && eventRuntime == nil && s.fieldMonsters != nil && monster != 0 {
|
||||||
instance, _, e := s.fieldMonsters.BeginFieldMonsterBattle(ctx, packID, monster, deck)
|
instance, _, e := s.fieldMonsters.BeginFieldMonsterBattle(ctx, packID, monster, deck)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
@@ -190,6 +204,7 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.entered, state.index, state.round, state.initialBlue = true, 0, 0, nil
|
state.entered, state.index, state.round, state.initialBlue = true, 0, 0, nil
|
||||||
|
state.retryable = false
|
||||||
state.monster, state.deck, state.pack = monster, deck, packID
|
state.monster, state.deck, state.pack = monster, deck, packID
|
||||||
state.mode = mode
|
state.mode = mode
|
||||||
state.enterReceipt = identity
|
state.enterReceipt = identity
|
||||||
@@ -198,7 +213,7 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
s.transientVersion++
|
s.transientVersion++
|
||||||
return 52, response, true, nil
|
return 52, response, true, nil
|
||||||
case "/BattleRetry":
|
case "/BattleRetry":
|
||||||
if !state.entered {
|
if !state.entered && !state.retryable {
|
||||||
return 0, nil, true, errors.New("battle: retry before enter")
|
return 0, nil, true, errors.New("battle: retry before enter")
|
||||||
}
|
}
|
||||||
index, found, err := wire.Varint(request, 2)
|
index, found, err := wire.Varint(request, 2)
|
||||||
@@ -208,12 +223,29 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
if len(state.initialBlue) == 0 {
|
if len(state.initialBlue) == 0 {
|
||||||
return 0, nil, true, errors.New("battle: retry before initial battle state")
|
return 0, nil, true, errors.New("battle: retry before initial battle state")
|
||||||
}
|
}
|
||||||
|
if index != state.index {
|
||||||
|
return 0, nil, true, errors.New("battle: retry index does not match current battle")
|
||||||
|
}
|
||||||
|
if state.retryable {
|
||||||
|
if s.currentPack == nil {
|
||||||
|
return 0, nil, true, errors.New("battle: retry pack resolver unavailable")
|
||||||
|
}
|
||||||
|
pack, err := s.currentPack(ctx)
|
||||||
|
if err != nil || pack != state.pack {
|
||||||
|
return 0, nil, true, errors.New("battle: retry outside failed encounter pack")
|
||||||
|
}
|
||||||
|
firstDeck := state.deck
|
||||||
|
if len(state.phases) > 0 {
|
||||||
|
firstDeck = state.phases[0].DeckID
|
||||||
|
}
|
||||||
|
instance, err := s.skyway.SkyWayBeginBattle(ctx, state.pack, state.mode, state.monster, firstDeck)
|
||||||
|
if err != nil || instance != state.fieldInstance {
|
||||||
|
return 0, nil, true, errors.New("battle: failed SkyWay encounter no longer available")
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(state.phases) != 0 {
|
if len(state.phases) != 0 {
|
||||||
// Retry requests carry the current deck; the response must restore
|
// Retry requests carry the current deck; the response must restore
|
||||||
// the first phase's deck, as PhaseBattleManager.ApplyRetryResponse does.
|
// the first phase's deck, as PhaseBattleManager.ApplyRetryResponse does.
|
||||||
if index != state.index {
|
|
||||||
return 0, nil, true, errors.New("battle: retry index does not match current phase")
|
|
||||||
}
|
|
||||||
index = state.phases[0].DeckID
|
index = state.phases[0].DeckID
|
||||||
}
|
}
|
||||||
var response []byte
|
var response []byte
|
||||||
@@ -226,6 +258,7 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
state.deck = state.phases[0].DeckID
|
state.deck = state.phases[0].DeckID
|
||||||
}
|
}
|
||||||
state.phase, state.phaseStarted, state.phaseSeq, state.phaseReply = 0, false, 0, nil
|
state.phase, state.phaseStarted, state.phaseSeq, state.phaseReply = 0, false, 0, nil
|
||||||
|
state.entered, state.retryable = true, false
|
||||||
s.transientVersion++
|
s.transientVersion++
|
||||||
return 58, response, true, nil
|
return 58, response, true, nil
|
||||||
case "/BattleStart":
|
case "/BattleStart":
|
||||||
@@ -243,6 +276,11 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
return 0, nil, true, errors.New("battle: start index does not match current phase")
|
return 0, nil, true, errors.New("battle: start index does not match current phase")
|
||||||
}
|
}
|
||||||
nextRound := state.round + 1
|
nextRound := state.round + 1
|
||||||
|
if nextRound == 1 && gamedata.IsSkyWayMode(state.mode) {
|
||||||
|
if err := s.skyway.SkyWayBattleStarted(ctx, state.mode, state.enterReceipt); err != nil {
|
||||||
|
return 0, nil, true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var initialBlue [][]byte
|
var initialBlue [][]byte
|
||||||
var response []byte
|
var response []byte
|
||||||
err = wire.Walk(request, func(field wire.Field) error {
|
err = wire.Walk(request, func(field wire.Field) error {
|
||||||
@@ -277,7 +315,7 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
if !bytes.Equal(state.endRequest, request) {
|
if !bytes.Equal(state.endRequest, request) {
|
||||||
return 0, nil, true, errors.New("battle: changed settlement retry")
|
return 0, nil, true, errors.New("battle: changed settlement retry")
|
||||||
}
|
}
|
||||||
return 15, append([]byte(nil), state.endReply...), true, nil
|
return endCode, append([]byte(nil), state.endReply...), true, nil
|
||||||
}
|
}
|
||||||
if !state.entered {
|
if !state.entered {
|
||||||
return 0, nil, true, errors.New("battle: end before enter")
|
return 0, nil, true, errors.New("battle: end before enter")
|
||||||
@@ -367,6 +405,21 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
response = wire.AppendBytes(response, 3, character)
|
response = wire.AppendBytes(response, 3, character)
|
||||||
}
|
}
|
||||||
rewardBundle := false
|
rewardBundle := false
|
||||||
|
if result == 1 && gamedata.IsSkyWayMode(state.mode) {
|
||||||
|
seq, _, _ := wire.Varint(request, 1)
|
||||||
|
bundle, bonus, monsters, err := s.skyway.SkyWayCompleteBattle(ctx, state.pack, state.mode, state.monster, state.deck, state.fieldInstance, fmt.Sprintf("%s:%d", ctx.SessionID, seq))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, true, err
|
||||||
|
}
|
||||||
|
for _, monster := range monsters {
|
||||||
|
response = wire.AppendBytes(response, 4, monster)
|
||||||
|
}
|
||||||
|
response = wire.AppendBytes(response, 5, bundle)
|
||||||
|
if len(bonus) > 0 {
|
||||||
|
response = wire.AppendBytes(response, 6, bonus)
|
||||||
|
}
|
||||||
|
rewardBundle = true
|
||||||
|
}
|
||||||
if result == 1 && state.mode == huntingGroundMode {
|
if result == 1 && state.mode == huntingGroundMode {
|
||||||
seq, _, _ := wire.Varint(request, 1)
|
seq, _, _ := wire.Varint(request, 1)
|
||||||
bundle, monsters, err := s.hunting.CompleteBattle(ctx, state.pack, state.mode, state.monster, state.deck,
|
bundle, monsters, err := s.hunting.CompleteBattle(ctx, state.pack, state.mode, state.monster, state.deck,
|
||||||
@@ -380,7 +433,7 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
response = wire.AppendBytes(response, 5, bundle)
|
response = wire.AppendBytes(response, 5, bundle)
|
||||||
rewardBundle = true
|
rewardBundle = true
|
||||||
}
|
}
|
||||||
if result == 1 && state.mode != huntingGroundMode && state.monster != 0 && s.gameDataRoot != "" {
|
if result == 1 && state.mode != huntingGroundMode && !gamedata.IsSkyWayMode(state.mode) && state.monster != 0 && s.gameDataRoot != "" {
|
||||||
if state.pack <= 0 {
|
if state.pack <= 0 {
|
||||||
return 0, nil, true, errors.New("battle: victory has no locked pack")
|
return 0, nil, true, errors.New("battle: victory has no locked pack")
|
||||||
}
|
}
|
||||||
@@ -433,13 +486,21 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
if field == 5 && rewardBundle {
|
if field == 5 && rewardBundle {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if field == 6 && gamedata.IsSkyWayMode(state.mode) && result == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
response = wire.AppendBytes(response, field, nil)
|
response = wire.AppendBytes(response, field, nil)
|
||||||
}
|
}
|
||||||
state.rememberEnd(request, response)
|
state.rememberEnd(request, response)
|
||||||
state.entered, state.deck, state.pack, state.initialBlue = false, 0, 0, nil
|
state.entered = false
|
||||||
|
state.retryable = (result == 2 || result == 3) && gamedata.IsSkyWayMode(state.mode)
|
||||||
|
if !state.retryable {
|
||||||
|
state.deck, state.pack, state.initialBlue = 0, 0, nil
|
||||||
|
}
|
||||||
s.transientVersion++
|
s.transientVersion++
|
||||||
return 15, response, true, nil
|
return endCode, response, true, nil
|
||||||
case "/BattleExit":
|
case "/BattleExit":
|
||||||
|
state.retryable = false
|
||||||
state.entered, state.index, state.round, state.deck, state.pack, state.initialBlue = false, 0, 0, 0, 0, nil
|
state.entered, state.index, state.round, state.deck, state.pack, state.initialBlue = false, 0, 0, 0, 0, nil
|
||||||
s.transientVersion++
|
s.transientVersion++
|
||||||
return 388, nil, true, nil
|
return 388, nil, true, nil
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package eventtasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bd2server/internal/server/domain/command"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) RecordSkyWayClear(ctx command.Context, positionGroup, difficulty uint64) error {
|
||||||
|
changed := false
|
||||||
|
for _, schedule := range s.taskSchedules() {
|
||||||
|
if schedule.Type != 4 || !s.active(schedule) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, task := range s.design.Missions {
|
||||||
|
if task.Type != 24 || task.SubType != positionGroup || !slices.Contains(task.Params, difficulty) || !s.availableTask(ctx, schedule, task) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
previous := s.state.Missions[scheduleKey(schedule)+"/"+key(task.ID)]
|
||||||
|
var prior mission
|
||||||
|
if previous != nil {
|
||||||
|
prior = *previous
|
||||||
|
}
|
||||||
|
current := s.mission(schedule, task.ID)
|
||||||
|
if previous == nil || prior != *current {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if !current.Claimed && current.Value < task.Target {
|
||||||
|
current.Value++
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.save(ctx)
|
||||||
|
}
|
||||||
@@ -25,8 +25,11 @@ func (s *Service) validateFieldObjectMap(ctx command.Context, pack, mapID int) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
current, err := s.currentFieldMap(ctx, pack)
|
current, err := s.currentFieldMap(ctx, pack)
|
||||||
if err != nil || current != mapID {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: field object outside current map", ErrInvalidRequest)
|
return fmt.Errorf("%w: field object map unavailable (pack=%d object_map=%d): %v", ErrInvalidRequest, pack, mapID, err)
|
||||||
|
}
|
||||||
|
if current != mapID {
|
||||||
|
return fmt.Errorf("%w: field object outside current map (pack=%d object_map=%d current_map=%d)", ErrInvalidRequest, pack, mapID, current)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ func (s *Service) LastPlayedPackID(ctx command.Context) (uint64, error) {
|
|||||||
return 0, errors.New("world: saved login pack is unavailable")
|
return 0, errors.New("world: saved login pack is unavailable")
|
||||||
}
|
}
|
||||||
if pack, field := s.fieldPacks[int(id)]; field {
|
if pack, field := s.fieldPacks[int(id)]; field {
|
||||||
|
if pack.Type == 5 {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
saved, _ := s.state.Position()
|
saved, _ := s.state.Position()
|
||||||
if !pack.MapIDs[saved.Position.MapID] {
|
if !pack.MapIDs[saved.Position.MapID] {
|
||||||
return 0, errors.New("world: saved arena map does not belong to pack")
|
return 0, errors.New("world: saved arena map does not belong to pack")
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ type overwhelmedMonster struct {
|
|||||||
Costs []gamedata.Reward
|
Costs []gamedata.Reward
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AttachOverwhelmDesign(source *gamedata.Source, rows []gamedata.SkyWayOverwhelmRule) error {
|
func (s *Service) AttachOverwhelmDesign(source *gamedata.Source) error {
|
||||||
s.overwhelmSky = rows
|
|
||||||
s.overwhelmQuest = func(pack, quest int) (gamedata.OverwhelmQuestRule, error) {
|
s.overwhelmQuest = func(pack, quest int) (gamedata.OverwhelmQuestRule, error) {
|
||||||
return source.OverwhelmQuest(pack, quest)
|
return source.OverwhelmQuest(pack, quest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,18 @@ func (s *Service) handleOverwhelm(ctx command.Context, request []byte) (int, []b
|
|||||||
if e = s.overwhelmHunting.ValidateBattle(ctx, pack, 5, m.ID, m.Deck); e != nil {
|
if e = s.overwhelmHunting.ValidateBattle(ctx, pack, 5, m.ID, m.Deck); e != nil {
|
||||||
return 275, nil, true, e
|
return 275, nil, true, e
|
||||||
}
|
}
|
||||||
|
} else if gamedata.IsSkyWayMode(m.Mode) {
|
||||||
|
definition, found, e := s.findFieldMonster(pack, int(m.ID))
|
||||||
|
if e != nil || !found || definition.UseBattleSkip != 1 {
|
||||||
|
return 275, nil, true, fmt.Errorf("world: SkyWay monster cannot be overwhelmed")
|
||||||
|
}
|
||||||
|
m.Instance, e = s.SkyWayBeginBattle(ctx, pack, m.Mode, m.ID, m.Deck)
|
||||||
|
if e != nil {
|
||||||
|
return 275, nil, true, e
|
||||||
|
}
|
||||||
|
m.Definition = definition
|
||||||
} else {
|
} else {
|
||||||
if m.Mode != 1 && m.Mode != 2 && m.Mode != 4 && (m.Mode < 9 || m.Mode > 15) {
|
if m.Mode != 1 && m.Mode != 2 && m.Mode != 4 {
|
||||||
return 275, nil, true, fmt.Errorf("world: unavailable overwhelm battle mode %d", m.Mode)
|
return 275, nil, true, fmt.Errorf("world: unavailable overwhelm battle mode %d", m.Mode)
|
||||||
}
|
}
|
||||||
definition, found, e := s.findFieldMonster(pack, int(m.ID))
|
definition, found, e := s.findFieldMonster(pack, int(m.ID))
|
||||||
@@ -104,44 +114,7 @@ func (s *Service) handleOverwhelm(ctx command.Context, request []byte) (int, []b
|
|||||||
if !validDeck {
|
if !validDeck {
|
||||||
return 275, nil, true, fmt.Errorf("world: overwhelm deck mismatch")
|
return 275, nil, true, fmt.Errorf("world: overwhelm deck mismatch")
|
||||||
}
|
}
|
||||||
if m.Mode >= 9 && m.Mode <= 15 {
|
if m.Mode == 2 {
|
||||||
mapID, e := s.currentFieldMap(ctx, pack)
|
|
||||||
if e != nil {
|
|
||||||
return 275, nil, true, e
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, rule := range s.overwhelmSky {
|
|
||||||
if rule.Map != uint64(mapID) || rule.Group+8 != m.Mode {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cost := uint64(0)
|
|
||||||
if rule.Boss == m.ID {
|
|
||||||
found = true
|
|
||||||
cost = rule.BossAP
|
|
||||||
} else {
|
|
||||||
for i, id := range rule.Monsters {
|
|
||||||
if id == m.ID {
|
|
||||||
found = true
|
|
||||||
cost = rule.AP[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
typ := uint64(21)
|
|
||||||
if rule.APType == 2 {
|
|
||||||
typ = 23
|
|
||||||
}
|
|
||||||
if cost > 0 {
|
|
||||||
m.Costs = []gamedata.Reward{{Type: typ, Count: cost}}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return 275, nil, true, fmt.Errorf("world: skyway monster does not match current dungeon")
|
|
||||||
}
|
|
||||||
m.Instance = fmt.Sprintf("skyway:%s:%d", identity, m.ID)
|
|
||||||
} else if m.Mode == 2 {
|
|
||||||
if int(m.Group) != definition.GroupID || definition.GroupID == 0 || !s.monsterEligible(pack, definition) {
|
if int(m.Group) != definition.GroupID || definition.GroupID == 0 || !s.monsterEligible(pack, definition) {
|
||||||
return 275, nil, true, ErrInvalidRequest
|
return 275, nil, true, ErrInvalidRequest
|
||||||
}
|
}
|
||||||
@@ -211,6 +184,16 @@ func (s *Service) handleOverwhelm(ctx command.Context, request []byte) (int, []b
|
|||||||
for _, row := range monsters {
|
for _, row := range monsters {
|
||||||
response = wire.AppendBytes(response, 1, row)
|
response = wire.AppendBytes(response, 1, row)
|
||||||
}
|
}
|
||||||
|
} else if gamedata.IsSkyWayMode(m.Mode) {
|
||||||
|
reward, bonus, monsters, e := s.SkyWayCompleteBattle(ctx, pack, m.Mode, m.ID, m.Deck, m.Instance, fmt.Sprintf("%s:%d", identity, m.ID))
|
||||||
|
if e != nil {
|
||||||
|
return 275, nil, true, e
|
||||||
|
}
|
||||||
|
bundle = append(bundle, reward...)
|
||||||
|
bundle = append(bundle, bonus...)
|
||||||
|
for _, row := range monsters {
|
||||||
|
response = wire.AppendBytes(response, 1, row)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if !v.Claims[m.Instance] && m.Definition.Type != 3 {
|
if !v.Claims[m.Instance] && m.Definition.Type != 3 {
|
||||||
definition := m.Definition
|
definition := m.Definition
|
||||||
@@ -350,6 +333,21 @@ func (s *Service) handleMonsterInfo(ctx command.Context, request []byte) (int, [
|
|||||||
if e != nil || !s.packUnlocked(ctx, pack) || s.monsterLoader == nil {
|
if e != nil || !s.packUnlocked(ctx, pack) || s.monsterLoader == nil {
|
||||||
return 0, nil, true, ErrInvalidRequest
|
return 0, nil, true, ErrInvalidRequest
|
||||||
}
|
}
|
||||||
|
if s.skyway != nil && pack == s.skyway.design.Pack {
|
||||||
|
v, err := s.skyway.load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 51, nil, true, err
|
||||||
|
}
|
||||||
|
stage, ok := s.skyway.design.Stage(v.Run.Group, v.Run.ID)
|
||||||
|
if !ok {
|
||||||
|
return 51, nil, true, nil
|
||||||
|
}
|
||||||
|
var out []byte
|
||||||
|
for _, row := range s.skyway.monsters(stage, v.Run) {
|
||||||
|
out = wire.AppendBytes(out, 1, row)
|
||||||
|
}
|
||||||
|
return 51, out, true, nil
|
||||||
|
}
|
||||||
design, e := s.monsterLoader(pack)
|
design, e := s.monsterLoader(pack)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return 0, nil, true, e
|
return 0, nil, true, e
|
||||||
@@ -861,7 +859,7 @@ func (s *Service) handleFieldObjectReward(ctx command.Context, request []byte) (
|
|||||||
}
|
}
|
||||||
response, err := s.openFieldObjectResponse(ctx, pack, int(group), int(id))
|
response, err := s.openFieldObjectResponse(ctx, pack, int(group), int(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, true, err
|
return 0, nil, true, fmt.Errorf("field object reward pack=%d group=%d object=%d: %w", pack, group, id, err)
|
||||||
}
|
}
|
||||||
return 29, response, true, nil
|
return 29, response, true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,6 +362,14 @@ func (s *Service) resolveActivePartyWires(ctx command.Context, packID, questID i
|
|||||||
func (s *Service) storyPackDBInfoRows(ctx command.Context) [][]byte {
|
func (s *Service) storyPackDBInfoRows(ctx command.Context) [][]byte {
|
||||||
ids := make([]int, 0, len(s.storyCatalog.Packs))
|
ids := make([]int, 0, len(s.storyCatalog.Packs))
|
||||||
for id := range s.storyCatalog.Packs {
|
for id := range s.storyCatalog.Packs {
|
||||||
|
if s.storyCatalog.Packs[id].Type == 5 {
|
||||||
|
if s.collection == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, owned := s.collection.Grant(packPurchaseIdentity(id)); !owned {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.storyPackUnlocked(ctx, id) {
|
if s.storyPackUnlocked(ctx, id) {
|
||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
@@ -400,7 +408,7 @@ func (s *Service) storyPackDBInfoRows(ctx command.Context) [][]byte {
|
|||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
if saved, found := s.state.Position(); found {
|
if saved, found := s.state.Position(); found {
|
||||||
if pack, arena := s.fieldPacks[saved.PackID]; arena && pack.MapIDs[saved.Position.MapID] {
|
if pack, arena := s.fieldPacks[saved.PackID]; arena && pack.Type != 5 && pack.MapIDs[saved.Position.MapID] {
|
||||||
rows = append(rows, wire.AppendVarint(wire.AppendVarint(nil, 1, uint64(saved.PackID)), 8, 1))
|
rows = append(rows, wire.AppendVarint(wire.AppendVarint(nil, 1, uint64(saved.PackID)), 8, 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,6 +727,27 @@ func (s *Service) purchaseStoryPack(ctx command.Context, id int, initial bool) (
|
|||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("%w: unknown purchase pack %d", ErrInvalidRequest, id)
|
return nil, fmt.Errorf("%w: unknown purchase pack %d", ErrInvalidRequest, id)
|
||||||
}
|
}
|
||||||
|
if pack.Type == 5 {
|
||||||
|
if s.skyway == nil || s.collection == nil {
|
||||||
|
return nil, fmt.Errorf("world: SkyWay purchase runtime unavailable")
|
||||||
|
}
|
||||||
|
identity := packPurchaseIdentity(id)
|
||||||
|
if _, owned := s.collection.Grant(identity); owned {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
var costs []gamedata.Reward
|
||||||
|
if !initial && pack.BuyPrice > 0 {
|
||||||
|
costs = []gamedata.Reward{{Type: pack.BuyType, Count: pack.BuyPrice}}
|
||||||
|
}
|
||||||
|
bundle, err := s.skyway.economy.Apply(ctx, identity, costs, pack.BuyRewards)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = s.collection.RecordGrantMarker(ctx, identity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bundle, nil
|
||||||
|
}
|
||||||
return s.purchasePack(ctx, id, pack.BuyType, pack.BuyPrice, pack.BuyRewards, initial)
|
return s.purchasePack(ctx, id, pack.BuyType, pack.BuyPrice, pack.BuyRewards, initial)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,6 +953,9 @@ func (s *Service) enterEventFieldPack(ctx command.Context, pack gamedata.EventFi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Handle(ctx command.Context, path string, request []byte) (int, []byte, bool, error) {
|
func (s *Service) Handle(ctx command.Context, path string, request []byte) (int, []byte, bool, error) {
|
||||||
|
if code, out, handled, err := s.handleSkyWay(ctx, path, request); handled {
|
||||||
|
return code, out, handled, err
|
||||||
|
}
|
||||||
if s.todayQuests != nil {
|
if s.todayQuests != nil {
|
||||||
if code, body, handled, err := s.todayQuests.Handle(ctx, path, request); handled {
|
if code, body, handled, err := s.todayQuests.Handle(ctx, path, request); handled {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -1090,6 +1122,19 @@ func (s *Service) Handle(ctx command.Context, path string, request []byte) (int,
|
|||||||
if !s.packUnlocked(ctx, pack) {
|
if !s.packUnlocked(ctx, pack) {
|
||||||
return 0, nil, true, fmt.Errorf("%w: unsupported pack %d", ErrInvalidRequest, pack)
|
return 0, nil, true, fmt.Errorf("%w: unsupported pack %d", ErrInvalidRequest, pack)
|
||||||
}
|
}
|
||||||
|
if s.skyway != nil && pack == s.skyway.design.Pack {
|
||||||
|
if _, purchased := s.collection.Grant(packPurchaseIdentity(pack)); !purchased {
|
||||||
|
return 0, nil, true, fmt.Errorf("world: SkyWay pack is not purchased")
|
||||||
|
}
|
||||||
|
v, e := s.skyway.load(ctx)
|
||||||
|
if e != nil {
|
||||||
|
return 0, nil, true, e
|
||||||
|
}
|
||||||
|
v.Run = skyWayRun{Generation: v.Run.Generation}
|
||||||
|
if e = s.skyway.save(ctx, v); e != nil {
|
||||||
|
return 0, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
if active := s.firstUnclearedQuestFor(pack); active != 0 {
|
if active := s.firstUnclearedQuestFor(pack); active != 0 {
|
||||||
if _, err := s.ensureQuestItems(ctx, pack, active); err != nil {
|
if _, err := s.ensureQuestItems(ctx, pack, active); err != nil {
|
||||||
return 0, nil, true, err
|
return 0, nil, true, err
|
||||||
|
|||||||
@@ -0,0 +1,554 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bd2server/internal/server/design/gamedata"
|
||||||
|
"bd2server/internal/server/domain/command"
|
||||||
|
"bd2server/internal/server/domain/world/progress"
|
||||||
|
"bd2server/internal/server/platform/versionconfig"
|
||||||
|
"bd2server/internal/server/protocol/wire"
|
||||||
|
"bd2server/internal/server/storage/stateio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SkyWaySchedule struct {
|
||||||
|
Group, Bonus uint64
|
||||||
|
Days []uint64
|
||||||
|
}
|
||||||
|
type skyWayEconomy interface {
|
||||||
|
CanApply(command.Context, []gamedata.Reward) error
|
||||||
|
Apply(command.Context, string, []gamedata.Reward, []gamedata.Reward) ([]byte, error)
|
||||||
|
AdditionalCurrencies(command.Context) (map[int]uint64, error)
|
||||||
|
}
|
||||||
|
type skyWayAP interface {
|
||||||
|
HuntingAP(command.Context) (uint64, uint64, error)
|
||||||
|
}
|
||||||
|
type skyWayRuntime struct {
|
||||||
|
design *gamedata.SkyWayDesign
|
||||||
|
store stateio.Store
|
||||||
|
economy skyWayEconomy
|
||||||
|
ap skyWayAP
|
||||||
|
schedules []SkyWaySchedule
|
||||||
|
now func() time.Time
|
||||||
|
graph *gamedata.RewardGraph
|
||||||
|
bossProgress func(command.Context, uint64, uint64) error
|
||||||
|
battleAttempt func(command.Context, uint64) error
|
||||||
|
}
|
||||||
|
type skyWayGroup struct {
|
||||||
|
Current uint64
|
||||||
|
Maximum int
|
||||||
|
Auto bool
|
||||||
|
}
|
||||||
|
type skyWayRun struct {
|
||||||
|
Group, ID, Generation uint64
|
||||||
|
Defeated []uint64
|
||||||
|
}
|
||||||
|
type skyWayReceipt struct {
|
||||||
|
Request []byte
|
||||||
|
Response, Bonus []byte
|
||||||
|
Monsters [][]byte
|
||||||
|
}
|
||||||
|
type skyWayState struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Groups map[string]skyWayGroup `json:"groups"`
|
||||||
|
Run skyWayRun `json:"run"`
|
||||||
|
Receipts map[string]skyWayReceipt `json:"receipts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) AttachSkyWay(ctx command.Context, d *gamedata.SkyWayDesign, store stateio.Store, economy skyWayEconomy, ap skyWayAP, schedules []SkyWaySchedule, graph *gamedata.RewardGraph) error {
|
||||||
|
if d == nil || store == nil || economy == nil || ap == nil || graph == nil {
|
||||||
|
return fmt.Errorf("world: invalid SkyWay runtime")
|
||||||
|
}
|
||||||
|
seen := map[uint64]bool{}
|
||||||
|
for _, r := range schedules {
|
||||||
|
if r.Group == 0 || r.Group > 7 || seen[r.Group] || r.Bonus > math.MaxInt32 {
|
||||||
|
return fmt.Errorf("world: invalid SkyWay schedule")
|
||||||
|
}
|
||||||
|
seen[r.Group] = true
|
||||||
|
days := map[uint64]bool{}
|
||||||
|
for _, day := range r.Days {
|
||||||
|
if day > 6 || days[day] {
|
||||||
|
return fmt.Errorf("world: invalid SkyWay bonus day")
|
||||||
|
}
|
||||||
|
days[day] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range d.Stages {
|
||||||
|
if !seen[row.Group] {
|
||||||
|
return fmt.Errorf("world: missing SkyWay group schedule")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.skyway = &skyWayRuntime{design: d, store: store, economy: economy, ap: ap, schedules: schedules, now: time.Now, graph: graph}
|
||||||
|
_, err := s.skyway.load(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
func (r *skyWayRuntime) load(ctx command.Context) (skyWayState, error) {
|
||||||
|
v := skyWayState{Version: versionconfig.State(), Groups: map[string]skyWayGroup{}, Receipts: map[string]skyWayReceipt{}}
|
||||||
|
b, err := r.store.Load(ctx.State, "skyway")
|
||||||
|
if err != nil || b == nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
if err = stateio.RequireExactJSONObject(b, "version", "groups", "run", "receipts"); err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
d := json.NewDecoder(bytes.NewReader(b))
|
||||||
|
d.DisallowUnknownFields()
|
||||||
|
if err = d.Decode(&v); err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
if v.Version != versionconfig.State() || v.Groups == nil || v.Receipts == nil {
|
||||||
|
return v, fmt.Errorf("world: incompatible SkyWay state")
|
||||||
|
}
|
||||||
|
for k, st := range v.Groups {
|
||||||
|
g, e := strconv.ParseUint(k, 10, 64)
|
||||||
|
stage, ok := r.design.Stage(g, st.Current)
|
||||||
|
maximum := -1
|
||||||
|
for _, row := range r.design.Stages {
|
||||||
|
if row.Group == g {
|
||||||
|
maximum = max(maximum, int(row.Difficulty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e != nil || strconv.FormatUint(g, 10) != k || !ok || st.Maximum < -1 || st.Maximum > maximum || int(stage.Difficulty) > st.Maximum+1 {
|
||||||
|
return v, fmt.Errorf("world: invalid SkyWay group progression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v.Run.Group != 0 {
|
||||||
|
stage, ok := r.design.Stage(v.Run.Group, v.Run.ID)
|
||||||
|
if !ok || v.Run.Generation == 0 {
|
||||||
|
return v, fmt.Errorf("world: invalid SkyWay run")
|
||||||
|
}
|
||||||
|
seen := map[uint64]bool{}
|
||||||
|
for _, id := range v.Run.Defeated {
|
||||||
|
if !slices.Contains(stage.Monsters, id) || seen[id] {
|
||||||
|
return v, fmt.Errorf("world: invalid SkyWay defeated monster")
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
func (r *skyWayRuntime) save(ctx command.Context, v skyWayState) error {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.store.Save(ctx.State, "skyway", b)
|
||||||
|
}
|
||||||
|
func (s *Service) skyWayPack(ctx command.Context, pack int) error {
|
||||||
|
if s.skyway == nil || pack != s.skyway.design.Pack || !s.packUnlocked(ctx, pack) {
|
||||||
|
return fmt.Errorf("world: SkyWay pack unavailable")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (r *skyWayRuntime) costs(ctx command.Context, stage gamedata.SkyWayStage, count uint64) ([]gamedata.Reward, error) {
|
||||||
|
if count == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var free, bonus uint64
|
||||||
|
var err error
|
||||||
|
freeType, bonusType := uint64(21), uint64(23)
|
||||||
|
if stage.APType == 1 {
|
||||||
|
free, bonus, err = r.ap.HuntingAP(ctx)
|
||||||
|
} else {
|
||||||
|
freeType, bonusType = 32, 33
|
||||||
|
var values map[int]uint64
|
||||||
|
values, err = r.economy.AdditionalCurrencies(ctx)
|
||||||
|
free, bonus = values[36], values[37]
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if free < count && bonus < count-free {
|
||||||
|
return nil, fmt.Errorf("world: insufficient SkyWay AP")
|
||||||
|
}
|
||||||
|
n := min(free, count)
|
||||||
|
var out []gamedata.Reward
|
||||||
|
if n > 0 {
|
||||||
|
out = append(out, gamedata.Reward{Type: freeType, Count: n})
|
||||||
|
}
|
||||||
|
if n < count {
|
||||||
|
out = append(out, gamedata.Reward{Type: bonusType, Count: count - n})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
func (s *Service) handleSkyWay(ctx command.Context, path string, request []byte) (int, []byte, bool, error) {
|
||||||
|
if path == "/SaveUserPosition" && s.skyway != nil {
|
||||||
|
pack, err := requestPack(request)
|
||||||
|
if err != nil {
|
||||||
|
return 7, nil, true, err
|
||||||
|
}
|
||||||
|
if pack == s.skyway.design.Pack {
|
||||||
|
raw, found, e := wire.Bytes(request, 3)
|
||||||
|
var position progress.Position
|
||||||
|
if e != nil || !found || json.Unmarshal(raw, &position) != nil || !s.skyway.design.SafeMaps[position.MapID] || !s.packUnlocked(ctx, pack) {
|
||||||
|
return 7, nil, true, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
if e = s.state.SaveUserPosition(ctx, request); e != nil {
|
||||||
|
return 7, nil, true, e
|
||||||
|
}
|
||||||
|
v, e := s.skyway.load(ctx)
|
||||||
|
if e != nil {
|
||||||
|
return 7, nil, true, e
|
||||||
|
}
|
||||||
|
v.Run = skyWayRun{Generation: v.Run.Generation}
|
||||||
|
e = s.skyway.save(ctx, v)
|
||||||
|
return 7, nil, true, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
code := map[string]int{"/SkyWayInfo": 160, "/SkyWayEnter": 161, "/SkyWayScheduleInfo": 162}[path]
|
||||||
|
if code == 0 {
|
||||||
|
return 0, nil, false, nil
|
||||||
|
}
|
||||||
|
if s.skyway == nil {
|
||||||
|
return code, nil, true, fmt.Errorf("world: SkyWay runtime missing")
|
||||||
|
}
|
||||||
|
seq, present, err := wire.Varint(request, 1)
|
||||||
|
if err != nil || !present || seq == 0 || seq > math.MaxInt32 {
|
||||||
|
return code, nil, true, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
r := s.skyway
|
||||||
|
if path == "/SkyWayScheduleInfo" {
|
||||||
|
var out []byte
|
||||||
|
for _, schedule := range r.schedules {
|
||||||
|
row := wire.AppendVarint(nil, 1, schedule.Group)
|
||||||
|
for _, day := range schedule.Days {
|
||||||
|
row = wire.AppendVarint(row, 2, day)
|
||||||
|
}
|
||||||
|
if schedule.Bonus > 0 {
|
||||||
|
row = wire.AppendVarint(row, 3, schedule.Bonus)
|
||||||
|
}
|
||||||
|
out = wire.AppendBytes(out, 1, row)
|
||||||
|
}
|
||||||
|
return code, out, true, nil
|
||||||
|
}
|
||||||
|
pack, err := requestPack(request)
|
||||||
|
if err != nil {
|
||||||
|
return code, nil, true, err
|
||||||
|
}
|
||||||
|
if err = s.skyWayPack(ctx, pack); err != nil {
|
||||||
|
return code, nil, true, err
|
||||||
|
}
|
||||||
|
v, err := r.load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return code, nil, true, err
|
||||||
|
}
|
||||||
|
if path == "/SkyWayInfo" {
|
||||||
|
var out []byte
|
||||||
|
seen := map[uint64]bool{}
|
||||||
|
for _, stage := range r.design.Stages {
|
||||||
|
if seen[stage.Group] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[stage.Group] = true
|
||||||
|
st, ok := v.Groups[strconv.FormatUint(stage.Group, 10)]
|
||||||
|
if !ok {
|
||||||
|
st = skyWayGroup{Current: stage.ID, Maximum: -1}
|
||||||
|
}
|
||||||
|
row := wire.AppendVarint(nil, 2, stage.Group)
|
||||||
|
row = wire.AppendVarint(row, 3, st.Current)
|
||||||
|
row = wire.AppendVarint(row, 4, uint64(int64(st.Maximum)))
|
||||||
|
if st.Auto {
|
||||||
|
row = wire.AppendVarint(row, 1, 1)
|
||||||
|
}
|
||||||
|
out = wire.AppendBytes(out, 1, row)
|
||||||
|
}
|
||||||
|
return code, out, true, nil
|
||||||
|
}
|
||||||
|
if ctx.SessionID == "" {
|
||||||
|
return code, nil, true, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
key := "enter:" + ctx.SessionID + ":" + strconv.FormatUint(seq, 10)
|
||||||
|
if prior, ok := v.Receipts[key]; ok {
|
||||||
|
if !bytes.Equal(prior.Request, request) {
|
||||||
|
return code, nil, true, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
return code, prior.Response, true, nil
|
||||||
|
}
|
||||||
|
group, _, err := wire.Varint(request, 3)
|
||||||
|
if err != nil {
|
||||||
|
return code, nil, true, err
|
||||||
|
}
|
||||||
|
id, _, err := wire.Varint(request, 4)
|
||||||
|
if err != nil {
|
||||||
|
return code, nil, true, err
|
||||||
|
}
|
||||||
|
auto, _, err := wire.Varint(request, 5)
|
||||||
|
if err != nil || auto > 1 {
|
||||||
|
return code, nil, true, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
stage, ok := r.design.Stage(group, id)
|
||||||
|
if !ok {
|
||||||
|
return code, nil, true, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
current, err := s.CurrentPackID(ctx)
|
||||||
|
if err != nil || current != pack || s.battleActive != nil && s.battleActive(ctx) {
|
||||||
|
return code, nil, true, fmt.Errorf("world: SkyWay enter outside current pack or during battle")
|
||||||
|
}
|
||||||
|
st, exists := v.Groups[strconv.FormatUint(group, 10)]
|
||||||
|
if !exists {
|
||||||
|
st.Maximum = -1
|
||||||
|
}
|
||||||
|
if int(stage.Difficulty) > st.Maximum+1 {
|
||||||
|
return code, nil, true, fmt.Errorf("world: SkyWay difficulty requires previous boss clear")
|
||||||
|
}
|
||||||
|
if _, err = r.costs(ctx, stage, 1); err != nil {
|
||||||
|
return code, nil, true, err
|
||||||
|
}
|
||||||
|
if v.Run.Generation == math.MaxUint64 {
|
||||||
|
return code, nil, true, fmt.Errorf("world: SkyWay generation overflow")
|
||||||
|
}
|
||||||
|
v.Run = skyWayRun{Group: group, ID: id, Generation: v.Run.Generation + 1}
|
||||||
|
st.Current, st.Auto = id, auto == 1
|
||||||
|
v.Groups[strconv.FormatUint(group, 10)] = st
|
||||||
|
var out []byte
|
||||||
|
for _, row := range r.monsters(stage, v.Run) {
|
||||||
|
out = wire.AppendBytes(out, 1, row)
|
||||||
|
}
|
||||||
|
v.Receipts[key] = skyWayReceipt{Request: slices.Clone(request), Response: out}
|
||||||
|
err = r.save(ctx, v)
|
||||||
|
return code, out, true, err
|
||||||
|
}
|
||||||
|
func (r *skyWayRuntime) monsters(stage gamedata.SkyWayStage, run skyWayRun) [][]byte {
|
||||||
|
var out [][]byte
|
||||||
|
for _, id := range stage.Monsters {
|
||||||
|
out = append(out, r.monster(id, !slices.Contains(run.Defeated, id)))
|
||||||
|
}
|
||||||
|
if len(run.Defeated) == len(stage.Monsters) {
|
||||||
|
out = append(out, r.monster(stage.Boss, true))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
func (r *skyWayRuntime) monster(id uint64, active bool) []byte {
|
||||||
|
m := r.design.Monsters[id]
|
||||||
|
out := wire.AppendVarint(nil, 1, id)
|
||||||
|
out = wire.AppendVarint(out, 2, m.Decks[0])
|
||||||
|
if active {
|
||||||
|
out = wire.AppendVarint(out, 6, 1)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
func (s *Service) SkyWayBeginBattle(ctx command.Context, pack int, mode, monster, deck uint64) (string, error) {
|
||||||
|
if err := s.skyWayPack(ctx, pack); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
v, err := s.skyway.load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
stage, ok := s.skyway.design.Stage(v.Run.Group, v.Run.ID)
|
||||||
|
if !ok || mode != gamedata.SkyWayMode(stage.Group) || slices.Contains(v.Run.Defeated, monster) {
|
||||||
|
return "", fmt.Errorf("world: invalid SkyWay encounter")
|
||||||
|
}
|
||||||
|
cost := uint64(0)
|
||||||
|
member := false
|
||||||
|
for i, id := range stage.Monsters {
|
||||||
|
if id == monster {
|
||||||
|
member = true
|
||||||
|
cost = stage.AP[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if monster == stage.Boss {
|
||||||
|
member = len(v.Run.Defeated) == len(stage.Monsters)
|
||||||
|
cost = stage.BossAP
|
||||||
|
}
|
||||||
|
if !member || !slices.Contains(s.skyway.design.Monsters[monster].Decks, deck) {
|
||||||
|
return "", fmt.Errorf("world: SkyWay monster/deck unavailable")
|
||||||
|
}
|
||||||
|
if _, err = s.skyway.costs(ctx, stage, cost); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("skyway:%d:%d:%d:%d", stage.Group, stage.ID, v.Run.Generation, monster), nil
|
||||||
|
}
|
||||||
|
func (s *Service) SkyWayCompleteBattle(ctx command.Context, pack int, mode, monster, deck uint64, instance, receipt string) ([]byte, []byte, [][]byte, error) {
|
||||||
|
r := s.skyway
|
||||||
|
if r == nil || receipt == "" {
|
||||||
|
return nil, nil, nil, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
v, err := r.load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
fingerprint := fmt.Sprintf("%d:%d:%d:%d:%s", pack, mode, monster, deck, instance)
|
||||||
|
key := "win:" + receipt
|
||||||
|
if old, ok := v.Receipts[key]; ok {
|
||||||
|
if !bytes.Equal(old.Request, []byte(fingerprint)) {
|
||||||
|
return nil, nil, nil, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
return old.Response, old.Bonus, old.Monsters, nil
|
||||||
|
}
|
||||||
|
current, err := s.SkyWayBeginBattle(ctx, pack, mode, monster, deck)
|
||||||
|
if err != nil || current != instance {
|
||||||
|
return nil, nil, nil, fmt.Errorf("world: stale SkyWay battle: %w", err)
|
||||||
|
}
|
||||||
|
stage, _ := r.design.Stage(v.Run.Group, v.Run.ID)
|
||||||
|
cost := stage.BossAP
|
||||||
|
if monster == stage.Boss && v.Run.Generation == math.MaxUint64 {
|
||||||
|
return nil, nil, nil, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
for i, id := range stage.Monsters {
|
||||||
|
if id == monster {
|
||||||
|
cost = stage.AP[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
costs, err := r.costs(ctx, stage, cost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
resolved, err := r.graph.Resolve(r.design.Monsters[monster].Rewards[deck])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
rewards := []gamedata.Reward{}
|
||||||
|
for _, reward := range resolved {
|
||||||
|
rewards = append(rewards, gamedata.Reward(reward))
|
||||||
|
}
|
||||||
|
var extra []gamedata.Reward
|
||||||
|
rate := r.bonusRate(stage.Group)
|
||||||
|
if rate > 0 {
|
||||||
|
for _, reward := range rewards {
|
||||||
|
if reward.Count > math.MaxInt32/rate {
|
||||||
|
return nil, nil, nil, fmt.Errorf("world: SkyWay bonus overflow")
|
||||||
|
}
|
||||||
|
reward.Count = reward.Count * rate / 100
|
||||||
|
if reward.Count > 0 {
|
||||||
|
extra = append(extra, reward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bundle, err := r.economy.Apply(ctx, instance, costs, rewards)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
var bonus []byte
|
||||||
|
if len(extra) > 0 {
|
||||||
|
bonus, err = r.economy.Apply(ctx, instance+":bonus", nil, extra)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updates := [][]byte{r.monster(monster, false)}
|
||||||
|
if monster == stage.Boss {
|
||||||
|
if r.bossProgress != nil {
|
||||||
|
if err = r.bossProgress(ctx, stage.PositionGroup, stage.Difficulty); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st := v.Groups[strconv.FormatUint(stage.Group, 10)]
|
||||||
|
st.Maximum = max(st.Maximum, int(stage.Difficulty))
|
||||||
|
v.Groups[strconv.FormatUint(stage.Group, 10)] = st
|
||||||
|
v.Run.Generation++
|
||||||
|
v.Run.Defeated = nil
|
||||||
|
for _, id := range stage.Monsters {
|
||||||
|
updates = append(updates, r.monster(id, true))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v.Run.Defeated = append(v.Run.Defeated, monster)
|
||||||
|
if len(v.Run.Defeated) == len(stage.Monsters) {
|
||||||
|
updates = append(updates, r.monster(stage.Boss, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Receipts[key] = skyWayReceipt{Request: []byte(fingerprint), Response: bundle, Bonus: bonus, Monsters: updates}
|
||||||
|
if err = r.save(ctx, v); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return bundle, bonus, updates, nil
|
||||||
|
}
|
||||||
|
func (r *skyWayRuntime) bonusRate(group uint64) uint64 {
|
||||||
|
day := uint64(r.now().UTC().Weekday())
|
||||||
|
for _, schedule := range r.schedules {
|
||||||
|
if schedule.Group == group && slices.Contains(schedule.Days, day) {
|
||||||
|
return schedule.Bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
func (s *Service) SkyWayDispatchEligibility(ctx command.Context, d *gamedata.DispatchDesign) error {
|
||||||
|
if err := s.skyWayPack(ctx, int(d.Pack)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !s.packCompleteFor(int(d.Pack)) {
|
||||||
|
return fmt.Errorf("world: SkyWay dispatch requires completed introductory quests")
|
||||||
|
}
|
||||||
|
stage, ok := s.skyway.design.Stage(d.TypeGroup, d.TypeID)
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidRequest
|
||||||
|
}
|
||||||
|
v, err := s.skyway.load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
st, ok := v.Groups[strconv.FormatUint(stage.Group, 10)]
|
||||||
|
if !ok || st.Maximum < int(stage.Difficulty) {
|
||||||
|
return fmt.Errorf("world: SkyWay dispatch requires selected boss clear")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *Service) skyWayMap(ctx command.Context, pack int) (int, bool, error) {
|
||||||
|
if s.skyway == nil || pack != s.skyway.design.Pack {
|
||||||
|
return 0, false, nil
|
||||||
|
}
|
||||||
|
v, err := s.skyway.load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, true, err
|
||||||
|
}
|
||||||
|
stage, ok := s.skyway.design.Stage(v.Run.Group, v.Run.ID)
|
||||||
|
return int(stage.Map), ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SkyWayDispatchCosts(ctx command.Context, d *gamedata.DispatchDesign, count uint64) ([]gamedata.Reward, error) {
|
||||||
|
if s.skyway == nil {
|
||||||
|
return nil, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
stage, ok := s.skyway.design.Stage(d.TypeGroup, d.TypeID)
|
||||||
|
if !ok || count > math.MaxInt32/d.AP {
|
||||||
|
return nil, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
return s.skyway.costs(ctx, stage, d.AP*count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) AttachSkyWayProgress(boss func(command.Context, uint64, uint64) error, attempt func(command.Context, uint64) error) {
|
||||||
|
s.skyway.bossProgress, s.skyway.battleAttempt = boss, attempt
|
||||||
|
}
|
||||||
|
func (s *Service) SkyWayBattleStarted(ctx command.Context, mode uint64, receipt string) error {
|
||||||
|
if !gamedata.IsSkyWayMode(mode) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := s.skyway
|
||||||
|
if r == nil || receipt == "" {
|
||||||
|
return ErrInvalidRequest
|
||||||
|
}
|
||||||
|
v, err := r.load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := "attempt:" + receipt
|
||||||
|
if _, ok := v.Receipts[key]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.battleAttempt != nil {
|
||||||
|
if err = r.battleAttempt(ctx, mode-8); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Receipts[key] = skyWayReceipt{Request: []byte(strconv.FormatUint(mode, 10))}
|
||||||
|
return r.save(ctx, v)
|
||||||
|
}
|
||||||
|
func (s *Service) SkyWayDispatchExchange(ctx command.Context, identity string, costs, rewards []gamedata.Reward) ([]byte, error) {
|
||||||
|
return s.skyway.economy.Apply(ctx, identity, costs, rewards)
|
||||||
|
}
|
||||||
|
func (s *Service) SkyWayDispatchBonus(d *gamedata.DispatchDesign, rewards []gamedata.BattleReward) ([]gamedata.BattleReward, error) {
|
||||||
|
rate := s.skyway.bonusRate(d.TypeGroup)
|
||||||
|
out := append([]gamedata.BattleReward(nil), rewards...)
|
||||||
|
for i, r := range out {
|
||||||
|
if r.Count > math.MaxInt32/(100+rate) {
|
||||||
|
return nil, ErrInvalidRequest
|
||||||
|
}
|
||||||
|
out[i].Count = r.Count * (100 + rate) / 100
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ func (s *Service) currentFieldMap(ctx command.Context, pack int) (int, error) {
|
|||||||
if !s.packUnlocked(ctx, pack) {
|
if !s.packUnlocked(ctx, pack) {
|
||||||
return 0, ErrInvalidRequest
|
return 0, ErrInvalidRequest
|
||||||
}
|
}
|
||||||
|
if mapID, found, err := s.skyWayMap(ctx, pack); found || err != nil {
|
||||||
|
return mapID, err
|
||||||
|
}
|
||||||
if saved, ok := s.state.Position(); ok && saved.PackID == pack && saved.Difficulty == s.questDifficulty(pack) && saved.Position.MapID > 0 {
|
if saved, ok := s.state.Position(); ok && saved.PackID == pack && saved.Difficulty == s.questDifficulty(pack) && saved.Position.MapID > 0 {
|
||||||
return saved.Position.MapID, nil
|
return saved.Position.MapID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ type Service struct {
|
|||||||
fieldBuffs map[uint64]gamedata.FieldBuffDesign
|
fieldBuffs map[uint64]gamedata.FieldBuffDesign
|
||||||
talentPackInfo func(ctx command.Context, _ int) ([]byte, error)
|
talentPackInfo func(ctx command.Context, _ int) ([]byte, error)
|
||||||
overwhelmAuthorize func(ctx command.Context, _ string, _ uint64) error
|
overwhelmAuthorize func(ctx command.Context, _ string, _ uint64) error
|
||||||
overwhelmSky []gamedata.SkyWayOverwhelmRule
|
skyway *skyWayRuntime
|
||||||
overwhelmQuest func(int, int) (gamedata.OverwhelmQuestRule, error)
|
overwhelmQuest func(int, int) (gamedata.OverwhelmQuestRule, error)
|
||||||
overwhelmHunting interface {
|
overwhelmHunting interface {
|
||||||
ValidateBattle(ctx command.Context, _ int, _ uint64, _ uint64, _ uint64) error
|
ValidateBattle(ctx command.Context, _ int, _ uint64, _ uint64, _ uint64) error
|
||||||
@@ -231,12 +231,14 @@ func (s *Service) questsFor(ctx command.Context, packID int) (map[int]gamedata.Q
|
|||||||
if _, found, err := s.resolveEventFieldPack(ctx, packID); err == nil && found {
|
if _, found, err := s.resolveEventFieldPack(ctx, packID); err == nil && found {
|
||||||
return map[int]gamedata.QuestDesign{}, true
|
return map[int]gamedata.QuestDesign{}, true
|
||||||
}
|
}
|
||||||
if _, exists := s.fieldPacks[packID]; exists {
|
|
||||||
return map[int]gamedata.QuestDesign{}, true
|
|
||||||
}
|
|
||||||
if s.storyCatalog != nil {
|
if s.storyCatalog != nil {
|
||||||
pack, found := s.storyCatalog.Packs[packID]
|
pack, found := s.storyCatalog.Packs[packID]
|
||||||
return pack.Quests, found
|
if found {
|
||||||
|
return pack.Quests, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, exists := s.fieldPacks[packID]; exists {
|
||||||
|
return map[int]gamedata.QuestDesign{}, true
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BD2.GameNames;
|
||||||
|
using BepInEx.Logging;
|
||||||
|
using gamfs;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Proto.Net;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
|
||||||
|
namespace Bd2LocalIdentity;
|
||||||
|
|
||||||
|
internal static class FieldRewardPosition
|
||||||
|
{
|
||||||
|
private static readonly HashSet<FieldRewardObjectController> Pending = [];
|
||||||
|
private static ManualLogSource Log;
|
||||||
|
|
||||||
|
internal static void Install(Harmony harmony, ManualLogSource logger)
|
||||||
|
{
|
||||||
|
Log = logger;
|
||||||
|
harmony.Patch(Game.Method(() => FieldPacket.SendFieldObjectRewardRequest(null, true)),
|
||||||
|
prefix: new HarmonyMethod(typeof(FieldRewardPosition), nameof(SaveBeforeReward)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SaveBeforeReward(FieldRewardObjectController __0, bool __1)
|
||||||
|
{
|
||||||
|
if (__0 == null || MiniGameHubSupporter.IsPlayEventHubOrMiniEventHub() ||
|
||||||
|
(FieldRewardObjectController.EFieldRewardObjectGroupType)__0.FieldRewardObjectGroupDTO.Type ==
|
||||||
|
FieldRewardObjectController.EFieldRewardObjectGroupType.LostCoin)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Pending.Contains(__0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GameFieldManager field = GameFieldManager.Instance;
|
||||||
|
if (UsesDedicatedPosition(field))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int packId = SG.Pack?.PlayingPackID ?? 0;
|
||||||
|
Scene scene = __0.gameObject.scene;
|
||||||
|
if (IsReady(field, packId, scene))
|
||||||
|
{
|
||||||
|
FieldPacket.SendSaveUserPositionRequest(packId, false, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (field == null || packId <= 0 || !scene.IsValid() || !scene.isLoaded)
|
||||||
|
{
|
||||||
|
Log.LogWarning("Field reward skipped without a loaded field: pack=" + packId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Pending.Add(__0);
|
||||||
|
field.StartCoroutine(WaitForPosition(field, __0, __1, packId, scene));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UsesDedicatedPosition(GameFieldManager field)
|
||||||
|
{
|
||||||
|
var packType = (Define_PackType)(SG.Pack?.EnterPackDTO?.PackType ?? -1);
|
||||||
|
return packType is Define_PackType.PackHidden or Define_PackType.PackIb ||
|
||||||
|
(packType == Define_PackType.PackSkyWay && field?.CurrentMapInfo?.MapType == 1) ||
|
||||||
|
(packType == Define_PackType.PackEvilCastle && EvilCastleManager.Instance != null &&
|
||||||
|
EvilCastleInfo.IsEvilCastleBType(EvilCastleManager.Instance.CurrentEvilCastleType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReady(GameFieldManager field, int packId, Scene scene)
|
||||||
|
{
|
||||||
|
return field != null && field.PlayerController != null && field.IsLoadedField &&
|
||||||
|
!field.IsMoveMapProcessing && field.CurrentMapInfo != null &&
|
||||||
|
packId > 0 && field.CurrentMapInfo.PackId == packId && SG.Net != null &&
|
||||||
|
scene.IsValid() && scene.isLoaded && scene == UnityEngine.SceneManagement.SceneManager.GetActiveScene() &&
|
||||||
|
string.Equals(field.CurrentMapScenePath, scene.name, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerator WaitForPosition(GameFieldManager field,
|
||||||
|
FieldRewardObjectController reward, bool showIndicator, int packId, Scene scene)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// The caller finishes its interaction state after the patched method returns.
|
||||||
|
yield return null;
|
||||||
|
while (field != null && reward != null && scene.isLoaded &&
|
||||||
|
GameFieldManager.Instance == field && SG.Pack != null &&
|
||||||
|
SG.Pack.PlayingPackID == packId && reward.gameObject.scene == scene)
|
||||||
|
{
|
||||||
|
if (IsReady(field, packId, scene))
|
||||||
|
{
|
||||||
|
Pending.Remove(reward);
|
||||||
|
FieldPacket.SendFieldObjectRewardRequest(reward, showIndicator);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
Log.LogWarning("Deferred field reward cancelled after its field changed: pack=" + packId);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Pending.Remove(reward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ public sealed class Plugin : BaseUnityPlugin
|
|||||||
TryInstall("age-gate persistence", () => LocalAccountPolicy.InstallAgeGatePersistence(harmony));
|
TryInstall("age-gate persistence", () => LocalAccountPolicy.InstallAgeGatePersistence(harmony));
|
||||||
TryInstall("database diagnostics", () => ClientDiagnostics.InstallDatabaseDiagnostics(harmony));
|
TryInstall("database diagnostics", () => ClientDiagnostics.InstallDatabaseDiagnostics(harmony));
|
||||||
TryInstall("commission navigation completion", () => CommissionNavigation.Install(harmony, Logger));
|
TryInstall("commission navigation completion", () => CommissionNavigation.Install(harmony, Logger));
|
||||||
|
TryInstall("field reward position ordering", () => FieldRewardPosition.Install(harmony, Logger));
|
||||||
Logger.LogInfo("Local identity active: AppManager.IsPlatformLogin => false");
|
Logger.LogInfo("Local identity active: AppManager.IsPlatformLogin => false");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user