Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

125 lines
3.8 KiB
Go

package joust
import (
"fmt"
"math"
)
type Match struct {
Winner byte
WinnerAction byte
Loser byte
LoserAction byte
}
type Tournament struct {
Round uint16
Matches [7]Match
}
func (t Tournament) Champion() byte {
return t.Matches[6].Winner
}
// TournamentFor deterministically completes the four quarter-finals, two
// semi-finals and final. The PVF supplies four client battle timeline profiles
// per rider but no rider strength or win-probability field, so winner selection
// is an unbiased deterministic coin. This makes restarts reproduce the same
// bracket without inventing a hidden ranking.
func (c *Catalog) TournamentFor(round uint16) (Tournament, error) {
if c == nil || len(c.riders) < OpeningKnightCount {
return Tournament{}, ErrRoundUnavailable
}
roster, err := selectOpeningRiders(c.riders, round)
if err != nil {
return Tournament{}, err
}
tournament := Tournament{Round: round}
participants := make([]byte, OpeningKnightCount)
for index := range roster {
participants[index] = roster[index].ID
}
matchIndex := 0
next := make([]byte, 0, 4)
for index := 0; index < len(participants); index += 2 {
match, err := c.simulateMatch(round, matchIndex, participants[index], participants[index+1])
if err != nil {
return Tournament{}, err
}
tournament.Matches[matchIndex] = match
next = append(next, match.Winner)
matchIndex++
}
semi := make([]byte, 0, 2)
for index := 0; index < len(next); index += 2 {
match, err := c.simulateMatch(round, matchIndex, next[index], next[index+1])
if err != nil {
return Tournament{}, err
}
tournament.Matches[matchIndex] = match
semi = append(semi, match.Winner)
matchIndex++
}
final, err := c.simulateMatch(round, matchIndex, semi[0], semi[1])
if err != nil {
return Tournament{}, err
}
tournament.Matches[matchIndex] = final
return tournament, nil
}
func (c *Catalog) simulateMatch(round uint16, index int, leftID, rightID byte) (Match, error) {
left, leftOK := c.rider(leftID)
right, rightOK := c.rider(rightID)
if !leftOK || !rightOK || left.ID == right.ID {
return Match{}, fmt.Errorf("%w: match=%d riders=%d/%d", ErrRoundUnavailable, index, leftID, rightID)
}
state := tournamentRandomState(round, index, leftID, rightID)
winner, loser := left, right
if nextTournamentRandom(&state)&1 != 0 {
winner, loser = right, left
}
winnerAction, err := battleAction(winner.Win, loser.AttackType)
if err != nil {
return Match{}, err
}
loserAction, err := battleAction(loser.Loss, winner.AttackType)
if err != nil {
return Match{}, err
}
return Match{Winner: winner.ID, WinnerAction: winnerAction, Loser: loser.ID, LoserAction: loserAction}, nil
}
// battleAction returns the profile key consumed by the current EXE's
// sub_365C180 nested map lookup. Each rider's [win]/[loss] section is four
// seven-value timelines selected by the opponent's attack type. The values are
// timeline percentages, not seven independently selectable action IDs.
// Returning a value from a CDF roll here used to emit keys 5 and 6; the client
// inserted an empty vector for those absent keys and then deliberately aborted
// in sub_F2EE20 when it compared the two match timelines.
func battleAction(values []uint16, opponentAttackType byte) (byte, error) {
offset, ok := attackTypeTableOffset(opponentAttackType)
if !ok || len(values) != 28 {
return 0, ErrPVFMalformed
}
if values[offset+6] == 0 {
return 0, ErrPVFMalformed
}
return byte(offset / 7), nil
}
func tournamentRandomState(round uint16, index int, left, right byte) uint32 {
state := uint32(round)*0x9E3779B9 ^ uint32(index+1)*0x85EBCA6B ^ uint32(left)<<8 ^ uint32(right)
if state == 0 {
state = math.MaxUint32
}
return state
}
func nextTournamentRandom(state *uint32) uint32 {
*state ^= *state << 13
*state ^= *state >> 17
*state ^= *state << 5
return *state
}