DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,789 @@
|
||||
package worldmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const tournamentRoundCount = 4
|
||||
|
||||
var (
|
||||
ErrTournamentDefinitionInvalid = errors.New("dnf tournament definition is invalid")
|
||||
ErrTournamentRandomInvalid = errors.New("dnf tournament random source returned an invalid index")
|
||||
ErrTournamentSequenceUnbound = errors.New("dnf tournament actor sequence is not bound")
|
||||
)
|
||||
|
||||
const (
|
||||
tournamentCardTypeCount = 2
|
||||
tournamentCardsPerType = 2
|
||||
)
|
||||
|
||||
// TournamentReward is the frozen face of one Yellow Dragon reward card.
|
||||
// ItemID==0 represents a gold/empty wire tuple; Empty disambiguates the
|
||||
// zero-value empty card from a real gold card with a zero amount.
|
||||
type TournamentReward struct {
|
||||
ItemID uint32
|
||||
Count uint32
|
||||
Gold bool
|
||||
Empty bool
|
||||
}
|
||||
|
||||
// TournamentRewardPlan mirrors the current client card topology: two cards
|
||||
// in the upper group and, for champions only, two cards in the lower group.
|
||||
// The plan is created once after the terminal actor death and then only moves
|
||||
// through reservation/delivery states. It is intentionally independent of
|
||||
// packet encoding and inventory persistence.
|
||||
type TournamentRewardPlan struct {
|
||||
CompletedRounds byte
|
||||
Experience uint32
|
||||
Champion bool
|
||||
Upper [tournamentCardsPerType]TournamentReward
|
||||
Lower [tournamentCardsPerType]TournamentReward
|
||||
selected [tournamentCardTypeCount]int8
|
||||
delivered [tournamentCardTypeCount]bool
|
||||
}
|
||||
|
||||
func (p *TournamentRewardPlan) initSelection() {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
for index := range p.selected {
|
||||
p.selected[index] = -1
|
||||
}
|
||||
}
|
||||
|
||||
func (p TournamentRewardPlan) CardCount(side byte) byte {
|
||||
if side == 0 {
|
||||
return tournamentCardsPerType
|
||||
}
|
||||
if side == 1 && p.Champion {
|
||||
return tournamentCardsPerType
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p TournamentRewardPlan) Card(side, index byte) (TournamentReward, bool) {
|
||||
if index >= tournamentCardsPerType || index >= p.CardCount(side) {
|
||||
return TournamentReward{}, false
|
||||
}
|
||||
if side == 0 {
|
||||
return p.Upper[index], true
|
||||
}
|
||||
return p.Lower[index], true
|
||||
}
|
||||
|
||||
// SelectedCard reports the committed card for one visible reward group.
|
||||
// A reserved but not yet delivered selection is intentionally not projected
|
||||
// to the client as owned.
|
||||
func (p TournamentRewardPlan) SelectedCard(side byte) (byte, bool) {
|
||||
if side >= tournamentCardTypeCount || !p.delivered[side] || p.selected[side] < 0 {
|
||||
return 0, false
|
||||
}
|
||||
index := byte(p.selected[side])
|
||||
if index >= p.CardCount(side) {
|
||||
return 0, false
|
||||
}
|
||||
return index, true
|
||||
}
|
||||
|
||||
// ReserveSelection performs the S4A12-style two-phase selection transition.
|
||||
// The boolean replay result is true only for an already delivered identical
|
||||
// selection, allowing a retry to be acknowledged without a second grant.
|
||||
func (p *TournamentRewardPlan) ReserveSelection(side, index byte) (TournamentReward, bool, bool) {
|
||||
if p == nil || index >= p.CardCount(side) || side >= tournamentCardTypeCount {
|
||||
return TournamentReward{}, false, false
|
||||
}
|
||||
if p.delivered[side] {
|
||||
// A retry is idempotent only when it repeats the exact card that was
|
||||
// committed. A different card index must be rejected instead of being
|
||||
// acknowledged with a mismatched reward row.
|
||||
if p.selected[side] != int8(index) {
|
||||
return TournamentReward{}, false, false
|
||||
}
|
||||
reward, ok := p.Card(side, index)
|
||||
return reward, false, ok
|
||||
}
|
||||
if p.selected[side] >= 0 {
|
||||
return TournamentReward{}, false, false
|
||||
}
|
||||
p.selected[side] = int8(index)
|
||||
reward, _ := p.Card(side, index)
|
||||
return reward, true, false
|
||||
}
|
||||
|
||||
func (p *TournamentRewardPlan) CommitSelection(side, index byte) bool {
|
||||
if p == nil || side >= tournamentCardTypeCount || index >= p.CardCount(side) || p.selected[side] != int8(index) || p.delivered[side] {
|
||||
return false
|
||||
}
|
||||
p.delivered[side] = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *TournamentRewardPlan) RollbackSelection(side, index byte) bool {
|
||||
if p == nil || side >= tournamentCardTypeCount || index >= p.CardCount(side) || p.selected[side] != int8(index) || p.delivered[side] {
|
||||
return false
|
||||
}
|
||||
p.selected[side] = -1
|
||||
return true
|
||||
}
|
||||
|
||||
func (p TournamentRewardPlan) IsSelectionComplete() bool {
|
||||
if p.selected[0] < 0 || !p.delivered[0] {
|
||||
return false
|
||||
}
|
||||
return !p.Champion || (p.selected[1] >= 0 && p.delivered[1])
|
||||
}
|
||||
|
||||
func (p TournamentRewardPlan) Cardless() bool {
|
||||
for side := byte(0); side < tournamentCardTypeCount; side++ {
|
||||
for index := byte(0); index < p.CardCount(side); index++ {
|
||||
reward, ok := p.Card(side, index)
|
||||
if !ok || !reward.Empty {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *TournamentRewardPlan) commitCardless() bool {
|
||||
if p == nil || !p.Cardless() {
|
||||
return false
|
||||
}
|
||||
if p.IsSelectionComplete() {
|
||||
return true
|
||||
}
|
||||
for side := byte(0); side < tournamentCardTypeCount; side++ {
|
||||
if p.CardCount(side) == 0 {
|
||||
continue
|
||||
}
|
||||
p.selected[side] = 0
|
||||
p.delivered[side] = true
|
||||
}
|
||||
return p.IsSelectionComplete()
|
||||
}
|
||||
|
||||
type TournamentActorKind byte
|
||||
|
||||
const (
|
||||
TournamentActorMonster TournamentActorKind = iota
|
||||
TournamentActorAICharacter
|
||||
)
|
||||
|
||||
type TournamentActor struct {
|
||||
Kind TournamentActorKind `json:"kind"`
|
||||
Code uint32 `json:"code"`
|
||||
Strength uint16 `json:"strength"`
|
||||
Name string `json:"name"`
|
||||
Level byte `json:"level"`
|
||||
Type byte `json:"type"`
|
||||
}
|
||||
|
||||
type TournamentTeam struct {
|
||||
Position byte `json:"position"`
|
||||
Members []TournamentActor `json:"members"`
|
||||
Player bool `json:"player"`
|
||||
}
|
||||
|
||||
type TournamentRound struct {
|
||||
Number byte `json:"number"`
|
||||
Teams []TournamentTeam `json:"teams"`
|
||||
}
|
||||
|
||||
type TournamentDefinition struct {
|
||||
DungeonID uint32
|
||||
MapID uint32
|
||||
DifficultyLevel byte
|
||||
PartyCount byte
|
||||
RoundFatigue int64
|
||||
GoldRate int64
|
||||
Experience map[byte]uint32
|
||||
ResultCards map[byte]TournamentResultCard
|
||||
RewardItems []TournamentRewardItemRate
|
||||
Candidates []TournamentActor
|
||||
StartAreas []TournamentStartArea
|
||||
}
|
||||
|
||||
type TournamentActorProjector func(candidate TournamentEnemyCandidate, basisLevel byte) (level byte, actorType byte, err error)
|
||||
|
||||
func (t *Table) TournamentDefinition(dungeonID, mapID int64, partyCount int, projector TournamentActorProjector) (TournamentDefinition, error) {
|
||||
dungeon, ok := t.FindDungeon(dungeonID)
|
||||
if !ok {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: dungeon=%d is missing", ErrTournamentDefinitionInvalid, dungeonID)
|
||||
}
|
||||
tournamentMap, ok := t.FindMap(mapID)
|
||||
if !ok {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: map=%d is missing", ErrTournamentDefinitionInvalid, mapID)
|
||||
}
|
||||
return buildTournamentDefinition(dungeon, tournamentMap, partyCount, projector)
|
||||
}
|
||||
|
||||
func buildTournamentDefinition(dungeon Dungeon, tournamentMap Map, partyCount int, projector TournamentActorProjector) (TournamentDefinition, error) {
|
||||
if dungeon.Metadata.Flags == nil || !dungeon.Metadata.Flags["tournament dungeon"] {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: dungeon=%d is not marked tournament", ErrTournamentDefinitionInvalid, dungeon.ID)
|
||||
}
|
||||
if dungeon.ID <= 0 || dungeon.ID > int64(^uint32(0)) || tournamentMap.ID <= 0 || tournamentMap.ID > int64(^uint32(0)) {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: dungeon=%d map=%d exceeds wire range", ErrTournamentDefinitionInvalid, dungeon.ID, tournamentMap.ID)
|
||||
}
|
||||
if !tournamentMap.DungeonID.Set || tournamentMap.DungeonID.Value != dungeon.ID {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: map=%d belongs to dungeon=%d", ErrTournamentDefinitionInvalid, tournamentMap.ID, tournamentMap.DungeonID.Value)
|
||||
}
|
||||
if tournamentMap.TournamentMalformed || partyCount < 1 || partyCount > 2 {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: party_count=%d malformed_map=%t", ErrTournamentDefinitionInvalid, partyCount, tournamentMap.TournamentMalformed)
|
||||
}
|
||||
if !dungeon.Metadata.LimitPartyCount.Set || dungeon.Metadata.LimitPartyCount.Value != int64(partyCount) {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: party_count=%d limit=%d", ErrTournamentDefinitionInvalid, partyCount, dungeon.Metadata.LimitPartyCount.Value)
|
||||
}
|
||||
if !dungeon.Metadata.BasisLevel.Set || dungeon.Metadata.BasisLevel.Value < 1 || dungeon.Metadata.BasisLevel.Value > 0xff {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: basis_level=%d", ErrTournamentDefinitionInvalid, dungeon.Metadata.BasisLevel.Value)
|
||||
}
|
||||
basisLevel := byte(dungeon.Metadata.BasisLevel.Value)
|
||||
definition := TournamentDefinition{
|
||||
DungeonID: uint32(dungeon.ID), MapID: uint32(tournamentMap.ID), PartyCount: byte(partyCount),
|
||||
DifficultyLevel: basisLevel, RoundFatigue: dungeon.Metadata.TournamentRoundFatigue,
|
||||
GoldRate: dungeon.Metadata.TournamentGoldRate, Experience: make(map[byte]uint32, 5),
|
||||
ResultCards: make(map[byte]TournamentResultCard, 6),
|
||||
RewardItems: append([]TournamentRewardItemRate(nil), dungeon.Metadata.TournamentRewardItemRates...),
|
||||
StartAreas: append([]TournamentStartArea(nil), tournamentMap.TournamentStartAreas...),
|
||||
}
|
||||
for _, row := range dungeon.Metadata.TournamentClearRewardExp {
|
||||
if row.CompletedRounds < 0 || row.CompletedRounds > tournamentRoundCount || row.Experience < 0 || row.Experience > int64(^uint32(0)) {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: reward experience row=%+v", ErrTournamentDefinitionInvalid, row)
|
||||
}
|
||||
key := byte(row.CompletedRounds)
|
||||
if _, exists := definition.Experience[key]; exists {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: duplicate reward experience round=%d", ErrTournamentDefinitionInvalid, key)
|
||||
}
|
||||
definition.Experience[key] = uint32(row.Experience)
|
||||
}
|
||||
for round := byte(0); round <= tournamentRoundCount; round++ {
|
||||
if _, ok := definition.Experience[round]; !ok {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: reward experience round=%d is missing", ErrTournamentDefinitionInvalid, round)
|
||||
}
|
||||
}
|
||||
for _, card := range dungeon.Metadata.TournamentResultCards {
|
||||
if card.ResultKey < 0 || card.ResultKey > 5 || card.GoldWeight < 0 || card.ItemWeight < 0 || card.EmptyWeight < 0 || card.GoldWeight+card.ItemWeight+card.EmptyWeight <= 0 {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: result card=%+v", ErrTournamentDefinitionInvalid, card)
|
||||
}
|
||||
key := byte(card.ResultKey)
|
||||
if _, exists := definition.ResultCards[key]; exists {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: duplicate result card=%d", ErrTournamentDefinitionInvalid, key)
|
||||
}
|
||||
definition.ResultCards[key] = card
|
||||
}
|
||||
for key := byte(0); key <= 5; key++ {
|
||||
if _, ok := definition.ResultCards[key]; !ok {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: result card=%d is missing", ErrTournamentDefinitionInvalid, key)
|
||||
}
|
||||
}
|
||||
if len(definition.RewardItems) == 0 || definition.RoundFatigue < 0 || definition.GoldRate <= 0 {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: reward metadata is incomplete", ErrTournamentDefinitionInvalid)
|
||||
}
|
||||
for _, reward := range definition.RewardItems {
|
||||
if reward.ItemID <= 0 || reward.Weight <= 0 || reward.Count <= 0 {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: reward item=%+v", ErrTournamentDefinitionInvalid, reward)
|
||||
}
|
||||
}
|
||||
for _, candidate := range tournamentMap.TournamentEnemies {
|
||||
if candidate.PartyCount != int64(partyCount) {
|
||||
continue
|
||||
}
|
||||
if candidate.Code <= 0 || candidate.Code > int64(^uint32(0)) || candidate.Strength < 0 || candidate.Strength > 0xffff || strings.TrimSpace(candidate.Name) == "" {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: actor=%+v", ErrTournamentDefinitionInvalid, candidate)
|
||||
}
|
||||
actor := TournamentActor{Code: uint32(candidate.Code), Strength: uint16(candidate.Strength), Name: candidate.Name, Level: basisLevel}
|
||||
switch strings.ToLower(strings.TrimSpace(candidate.Kind)) {
|
||||
case "[monster]":
|
||||
actor.Kind = TournamentActorMonster
|
||||
case "[apc]":
|
||||
if projector == nil {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: APC code=%d requires a projector", ErrTournamentDefinitionInvalid, candidate.Code)
|
||||
}
|
||||
level, actorType, err := projector(candidate, basisLevel)
|
||||
if err != nil {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: project APC code=%d: %v", ErrTournamentDefinitionInvalid, candidate.Code, err)
|
||||
}
|
||||
actor.Kind, actor.Level, actor.Type = TournamentActorAICharacter, level, actorType
|
||||
default:
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: actor kind=%q", ErrTournamentDefinitionInvalid, candidate.Kind)
|
||||
}
|
||||
definition.Candidates = append(definition.Candidates, actor)
|
||||
}
|
||||
if len(definition.Candidates) < 15*partyCount {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: candidates=%d required=%d", ErrTournamentDefinitionInvalid, len(definition.Candidates), 15*partyCount)
|
||||
}
|
||||
startCount := 0
|
||||
for _, area := range definition.StartAreas {
|
||||
if area.PartyCount == int64(partyCount) {
|
||||
startCount++
|
||||
}
|
||||
}
|
||||
if startCount < 2 {
|
||||
return TournamentDefinition{}, fmt.Errorf("%w: start areas=%d required=2", ErrTournamentDefinitionInvalid, startCount)
|
||||
}
|
||||
return definition, nil
|
||||
}
|
||||
|
||||
type TournamentRandom func(exclusiveMaximum int) (int, error)
|
||||
|
||||
type TournamentTerminalResult byte
|
||||
|
||||
const (
|
||||
TournamentActive TournamentTerminalResult = iota
|
||||
TournamentChampion
|
||||
TournamentEliminated
|
||||
)
|
||||
|
||||
type TournamentDeathDisposition byte
|
||||
|
||||
const (
|
||||
TournamentDeathRejected TournamentDeathDisposition = iota
|
||||
TournamentDeathDuplicate
|
||||
TournamentDeathAccepted
|
||||
TournamentRoundCompleted
|
||||
TournamentCompleted
|
||||
)
|
||||
|
||||
type TournamentDeathTransition struct {
|
||||
Disposition TournamentDeathDisposition
|
||||
CompletedRound byte
|
||||
CurrentRound byte
|
||||
}
|
||||
|
||||
type TournamentRuntime struct {
|
||||
mu sync.Mutex
|
||||
|
||||
Definition TournamentDefinition
|
||||
Rounds []TournamentRound
|
||||
PathActors []TournamentActor
|
||||
|
||||
firstSequence uint16
|
||||
currentRound byte
|
||||
completedRounds byte
|
||||
terminal TournamentTerminalResult
|
||||
acceptedEvents map[string]uint16
|
||||
acceptedSequences map[uint16]struct{}
|
||||
rewardPlan *TournamentRewardPlan
|
||||
}
|
||||
|
||||
// PrepareRewardPlan freezes the PVF-selected card faces exactly once after a
|
||||
// tournament becomes terminal. The caller supplies the already-authoritative
|
||||
// gold/experience amounts because those values belong to the bridge's reward
|
||||
// and persistence layers, not to the world-map parser.
|
||||
func (r *TournamentRuntime) PrepareRewardPlan(experience, gold uint32, random TournamentRandom) (TournamentRewardPlan, bool, error) {
|
||||
if r == nil || random == nil {
|
||||
return TournamentRewardPlan{}, false, fmt.Errorf("%w: reward random source is missing", ErrTournamentDefinitionInvalid)
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.terminal == TournamentActive {
|
||||
return TournamentRewardPlan{}, false, fmt.Errorf("%w: tournament is not terminal", ErrTournamentDefinitionInvalid)
|
||||
}
|
||||
if r.rewardPlan != nil {
|
||||
return *r.rewardPlan, false, nil
|
||||
}
|
||||
plan := TournamentRewardPlan{
|
||||
CompletedRounds: r.completedRounds,
|
||||
Experience: experience,
|
||||
Champion: r.terminal == TournamentChampion,
|
||||
}
|
||||
plan.initSelection()
|
||||
upperKey := r.completedRounds
|
||||
for index := byte(0); index < tournamentCardsPerType; index++ {
|
||||
reward, err := tournamentRollReward(r.Definition, upperKey, gold, random)
|
||||
if err != nil {
|
||||
return TournamentRewardPlan{}, false, err
|
||||
}
|
||||
plan.Upper[index] = reward
|
||||
}
|
||||
if plan.Champion {
|
||||
for index := byte(0); index < tournamentCardsPerType; index++ {
|
||||
reward, err := tournamentRollReward(r.Definition, 5, gold, random)
|
||||
if err != nil {
|
||||
return TournamentRewardPlan{}, false, err
|
||||
}
|
||||
plan.Lower[index] = reward
|
||||
}
|
||||
}
|
||||
r.rewardPlan = &plan
|
||||
return plan, true, nil
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) RewardPlanSnapshot() (TournamentRewardPlan, bool) {
|
||||
if r == nil {
|
||||
return TournamentRewardPlan{}, false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.rewardPlan == nil {
|
||||
return TournamentRewardPlan{}, false
|
||||
}
|
||||
return *r.rewardPlan, true
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) ReserveRewardSelection(side, index byte) (TournamentReward, bool, bool) {
|
||||
if r == nil {
|
||||
return TournamentReward{}, false, false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.rewardPlan == nil {
|
||||
return TournamentReward{}, false, false
|
||||
}
|
||||
return r.rewardPlan.ReserveSelection(side, index)
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) CommitRewardSelection(side, index byte) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.rewardPlan != nil && r.rewardPlan.CommitSelection(side, index)
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) RollbackRewardSelection(side, index byte) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.rewardPlan != nil && r.rewardPlan.RollbackSelection(side, index)
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) RewardSelectionComplete() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.rewardPlan != nil && r.rewardPlan.IsSelectionComplete()
|
||||
}
|
||||
|
||||
// CommitCardlessRewardPlan closes a PVF reward plan whose visible faces are
|
||||
// all empty. The current client represents this as zero card groups in op388,
|
||||
// so there is no op458 request that could otherwise cross the delivery gate.
|
||||
func (r *TournamentRuntime) CommitCardlessRewardPlan() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.rewardPlan != nil && r.rewardPlan.commitCardless()
|
||||
}
|
||||
|
||||
// RewardSideSelectionComplete reports whether one visible card group has
|
||||
// already crossed the durable delivery boundary. It is used by timeout
|
||||
// coordinators to fill only client selections that never arrived.
|
||||
func (r *TournamentRuntime) RewardSideSelectionComplete(side byte) bool {
|
||||
if r == nil || side >= tournamentCardTypeCount {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.rewardPlan != nil && r.rewardPlan.delivered[side]
|
||||
}
|
||||
|
||||
func tournamentRollReward(definition TournamentDefinition, resultKey byte, gold uint32, random TournamentRandom) (TournamentReward, error) {
|
||||
card, ok := definition.ResultCards[resultKey]
|
||||
if !ok {
|
||||
return TournamentReward{}, fmt.Errorf("%w: reward result key=%d is missing", ErrTournamentDefinitionInvalid, resultKey)
|
||||
}
|
||||
total := card.GoldWeight + card.ItemWeight + card.EmptyWeight
|
||||
if total <= 0 {
|
||||
return TournamentReward{}, fmt.Errorf("%w: reward result key=%d has no weight", ErrTournamentDefinitionInvalid, resultKey)
|
||||
}
|
||||
roll, err := tournamentRandomIndex(random, int(total))
|
||||
if err != nil {
|
||||
return TournamentReward{}, err
|
||||
}
|
||||
if int64(roll) < card.GoldWeight {
|
||||
return TournamentReward{Gold: true, Count: gold}, nil
|
||||
}
|
||||
if int64(roll) < card.GoldWeight+card.ItemWeight {
|
||||
reward, err := tournamentRollRewardItem(definition.RewardItems, random)
|
||||
if err != nil {
|
||||
return TournamentReward{}, err
|
||||
}
|
||||
return reward, nil
|
||||
}
|
||||
return TournamentReward{Empty: true}, nil
|
||||
}
|
||||
|
||||
func tournamentRollRewardItem(items []TournamentRewardItemRate, random TournamentRandom) (TournamentReward, error) {
|
||||
var total int64
|
||||
for _, item := range items {
|
||||
if item.ItemID <= 0 || item.Weight <= 0 || item.Count <= 0 {
|
||||
continue
|
||||
}
|
||||
total += item.Weight
|
||||
}
|
||||
if total <= 0 || total > int64(^uint(0)>>1) {
|
||||
return TournamentReward{}, fmt.Errorf("%w: reward item rates are empty", ErrTournamentDefinitionInvalid)
|
||||
}
|
||||
roll, err := tournamentRandomIndex(random, int(total))
|
||||
if err != nil {
|
||||
return TournamentReward{}, err
|
||||
}
|
||||
var cumulative int64
|
||||
for _, item := range items {
|
||||
if item.ItemID <= 0 || item.Weight <= 0 || item.Count <= 0 {
|
||||
continue
|
||||
}
|
||||
cumulative += item.Weight
|
||||
if int64(roll) < cumulative {
|
||||
return TournamentReward{ItemID: uint32(item.ItemID), Count: uint32(item.Count)}, nil
|
||||
}
|
||||
}
|
||||
return TournamentReward{}, fmt.Errorf("%w: reward item roll did not resolve", ErrTournamentDefinitionInvalid)
|
||||
}
|
||||
|
||||
func NewTournamentRuntime(definition TournamentDefinition, random TournamentRandom) (*TournamentRuntime, error) {
|
||||
if random == nil {
|
||||
return nil, fmt.Errorf("%w: random source is nil", ErrTournamentDefinitionInvalid)
|
||||
}
|
||||
partyCount := int(definition.PartyCount)
|
||||
if partyCount < 1 || partyCount > 2 || len(definition.Candidates) < 15*partyCount {
|
||||
return nil, fmt.Errorf("%w: party=%d candidates=%d", ErrTournamentDefinitionInvalid, partyCount, len(definition.Candidates))
|
||||
}
|
||||
candidates := append([]TournamentActor(nil), definition.Candidates...)
|
||||
if err := shuffleTournamentActors(candidates, random); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
npcTeams := make([]TournamentTeam, 15)
|
||||
for index := range npcTeams {
|
||||
members := append([]TournamentActor(nil), candidates[index*partyCount:(index+1)*partyCount]...)
|
||||
npcTeams[index] = TournamentTeam{Members: members}
|
||||
}
|
||||
for i := 1; i < len(npcTeams); i++ {
|
||||
for j := i; j > 0 && tournamentTeamStrength(npcTeams[j]) < tournamentTeamStrength(npcTeams[j-1]); j-- {
|
||||
npcTeams[j], npcTeams[j-1] = npcTeams[j-1], npcTeams[j]
|
||||
}
|
||||
}
|
||||
playerPosition, err := tournamentRandomIndex(random, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opponentPosition := playerPosition ^ 1
|
||||
firstRound := make([]TournamentTeam, 16)
|
||||
firstRound[playerPosition] = TournamentTeam{Position: byte(playerPosition), Members: make([]TournamentActor, partyCount), Player: true}
|
||||
firstRound[opponentPosition] = tournamentTeamAt(npcTeams[0], opponentPosition)
|
||||
remaining := append([]TournamentTeam(nil), npcTeams[1:]...)
|
||||
if err := shuffleTournamentTeams(remaining, random); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remainingIndex := 0
|
||||
for position := range firstRound {
|
||||
if position == playerPosition || position == opponentPosition {
|
||||
continue
|
||||
}
|
||||
firstRound[position] = tournamentTeamAt(remaining[remainingIndex], position)
|
||||
remainingIndex++
|
||||
}
|
||||
rounds := []TournamentRound{{Number: 1, Teams: firstRound}}
|
||||
previous := firstRound
|
||||
for round := byte(2); round <= tournamentRoundCount; round++ {
|
||||
nextRound := make([]TournamentTeam, len(previous)/2)
|
||||
for match := range nextRound {
|
||||
left, right := previous[match*2], previous[match*2+1]
|
||||
winner := left
|
||||
if !left.Player && right.Player {
|
||||
winner = right
|
||||
} else if !left.Player && !right.Player {
|
||||
leftStrength, rightStrength := tournamentTeamStrength(left), tournamentTeamStrength(right)
|
||||
if rightStrength > leftStrength {
|
||||
winner = right
|
||||
} else if rightStrength == leftStrength {
|
||||
pick, pickErr := tournamentRandomIndex(random, 2)
|
||||
if pickErr != nil {
|
||||
return nil, pickErr
|
||||
}
|
||||
if pick == 1 {
|
||||
winner = right
|
||||
}
|
||||
}
|
||||
}
|
||||
nextRound[match] = tournamentTeamAt(winner, match)
|
||||
}
|
||||
rounds = append(rounds, TournamentRound{Number: round, Teams: nextRound})
|
||||
previous = nextRound
|
||||
}
|
||||
pathActors := make([]TournamentActor, 0, tournamentRoundCount*partyCount)
|
||||
for _, round := range rounds {
|
||||
playerIndex := -1
|
||||
for index, team := range round.Teams {
|
||||
if team.Player {
|
||||
playerIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if playerIndex < 0 {
|
||||
return nil, fmt.Errorf("%w: round=%d has no player", ErrTournamentDefinitionInvalid, round.Number)
|
||||
}
|
||||
for _, actor := range round.Teams[playerIndex^1].Members {
|
||||
if round.Number == tournamentRoundCount {
|
||||
if actor.Kind == TournamentActorAICharacter {
|
||||
actor.Type = 8
|
||||
} else {
|
||||
actor.Type = 3
|
||||
}
|
||||
}
|
||||
pathActors = append(pathActors, actor)
|
||||
}
|
||||
}
|
||||
return &TournamentRuntime{
|
||||
Definition: definition, Rounds: rounds, PathActors: pathActors, currentRound: 1,
|
||||
acceptedEvents: make(map[string]uint16), acceptedSequences: make(map[uint16]struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) BindFirstActorSequence(sequence uint16) bool {
|
||||
if r == nil || sequence == 0 {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if int(sequence)+len(r.PathActors)-1 > int(^uint16(0)) {
|
||||
return false
|
||||
}
|
||||
if r.firstSequence == 0 {
|
||||
r.firstSequence = sequence
|
||||
}
|
||||
return r.firstSequence == sequence
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) FirstActorSequence() uint16 {
|
||||
if r == nil {
|
||||
return 0
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.firstSequence
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) ActorForSequence(sequence uint16) (TournamentActor, bool) {
|
||||
if r == nil || sequence == 0 {
|
||||
return TournamentActor{}, false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.firstSequence == 0 || sequence < r.firstSequence {
|
||||
return TournamentActor{}, false
|
||||
}
|
||||
index := int(sequence - r.firstSequence)
|
||||
if index < 0 || index >= len(r.PathActors) {
|
||||
return TournamentActor{}, false
|
||||
}
|
||||
return r.PathActors[index], true
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) Snapshot() (currentRound, completedRounds byte, terminal TournamentTerminalResult) {
|
||||
if r == nil {
|
||||
return 0, 0, TournamentEliminated
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.currentRound, r.completedRounds, r.terminal
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) ApplyActorDeath(eventID string, sequence uint16) TournamentDeathTransition {
|
||||
if r == nil || strings.TrimSpace(eventID) == "" {
|
||||
return TournamentDeathTransition{Disposition: TournamentDeathRejected}
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if accepted, ok := r.acceptedEvents[eventID]; ok {
|
||||
disposition := TournamentDeathRejected
|
||||
if accepted == sequence {
|
||||
disposition = TournamentDeathDuplicate
|
||||
}
|
||||
return TournamentDeathTransition{Disposition: disposition, CurrentRound: r.currentRound}
|
||||
}
|
||||
if _, ok := r.acceptedSequences[sequence]; ok {
|
||||
return TournamentDeathTransition{Disposition: TournamentDeathDuplicate, CurrentRound: r.currentRound}
|
||||
}
|
||||
if r.firstSequence == 0 || r.terminal != TournamentActive {
|
||||
return TournamentDeathTransition{Disposition: TournamentDeathRejected, CurrentRound: r.currentRound}
|
||||
}
|
||||
first := int(r.firstSequence) + (int(r.currentRound)-1)*int(r.Definition.PartyCount)
|
||||
end := first + int(r.Definition.PartyCount)
|
||||
if int(sequence) < first || int(sequence) >= end {
|
||||
return TournamentDeathTransition{Disposition: TournamentDeathRejected, CurrentRound: r.currentRound}
|
||||
}
|
||||
r.acceptedEvents[eventID] = sequence
|
||||
r.acceptedSequences[sequence] = struct{}{}
|
||||
for current := first; current < end; current++ {
|
||||
if _, ok := r.acceptedSequences[uint16(current)]; !ok {
|
||||
return TournamentDeathTransition{Disposition: TournamentDeathAccepted, CurrentRound: r.currentRound}
|
||||
}
|
||||
}
|
||||
completed := r.currentRound
|
||||
r.completedRounds = completed
|
||||
if completed == tournamentRoundCount {
|
||||
r.terminal = TournamentChampion
|
||||
return TournamentDeathTransition{Disposition: TournamentCompleted, CompletedRound: completed, CurrentRound: completed}
|
||||
}
|
||||
r.currentRound++
|
||||
return TournamentDeathTransition{Disposition: TournamentRoundCompleted, CompletedRound: completed, CurrentRound: r.currentRound}
|
||||
}
|
||||
|
||||
func (r *TournamentRuntime) Eliminate() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.terminal != TournamentActive {
|
||||
return false
|
||||
}
|
||||
r.completedRounds = r.currentRound - 1
|
||||
r.terminal = TournamentEliminated
|
||||
return true
|
||||
}
|
||||
|
||||
func tournamentTeamAt(team TournamentTeam, position int) TournamentTeam {
|
||||
return TournamentTeam{Position: byte(position), Members: append([]TournamentActor(nil), team.Members...), Player: team.Player}
|
||||
}
|
||||
|
||||
func tournamentTeamStrength(team TournamentTeam) uint64 {
|
||||
var total uint64
|
||||
for _, actor := range team.Members {
|
||||
total += uint64(actor.Strength)
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func tournamentRandomIndex(random TournamentRandom, maximum int) (int, error) {
|
||||
value, err := random(maximum)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if value < 0 || value >= maximum {
|
||||
return 0, fmt.Errorf("%w: value=%d maximum=%d", ErrTournamentRandomInvalid, value, maximum)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func shuffleTournamentActors(values []TournamentActor, random TournamentRandom) error {
|
||||
for index := len(values) - 1; index > 0; index-- {
|
||||
swap, err := tournamentRandomIndex(random, index+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values[index], values[swap] = values[swap], values[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shuffleTournamentTeams(values []TournamentTeam, random TournamentRandom) error {
|
||||
for index := len(values) - 1; index > 0; index-- {
|
||||
swap, err := tournamentRandomIndex(random, index+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values[index], values[swap] = values[swap], values[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user