Files
90CNServer/server/internal/services/dnfbridge/joust_history.go
T
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

202 lines
6.2 KiB
Go

package dnfbridge
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"sort"
"strings"
"time"
dnfjoust "longheng.io/server/internal/modules/dnf/joust"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const currentJoustHistorySettingsKey = "completed_rounds_v1"
type currentJoustHistoryRecord struct {
Round uint16 `json:"round"`
Winner byte `json:"winner"`
Multiplier float32 `json:"multiplier"`
}
func currentJoustHistoryScope(accountID string) string {
return "account:" + strings.TrimSpace(accountID) + ":joust"
}
func (service *Service) currentJoustHistory(
ctx context.Context,
session *gameSession,
now time.Time,
) ([]currentJoustHistoryRecord, error) {
if service == nil {
return nil, dnfjoust.ErrOwnerUnavailable
}
overrides, err := service.loadCurrentJoustHistoryOverrides(ctx, service.accountIDForSession(session))
if err != nil {
return nil, err
}
catalog, err := service.currentJoustCatalog(ctx)
if err != nil {
return nil, err
}
timeline := dnfjoust.TimelineAt(now)
latest := timeline.Round - 1
if timeline.Phase == dnfjoust.PhaseSettled {
latest = timeline.Round
}
records := make([]currentJoustHistoryRecord, 0, len(overrides))
for round, record := range overrides {
if round == 0 || round > latest {
continue
}
// History written by older, non-original brackets is retained in the
// database for recovery, but cannot be shown as an actual result after
// the original roster is restored. Never let such a legacy row enter
// the current client's visible history.
tournament, tournamentErr := catalog.TournamentFor(round)
if tournamentErr != nil || tournament.Champion() != record.Winner {
continue
}
records = append(records, record)
}
sort.Slice(records, func(left, right int) bool {
return uint16(latest-records[left].Round) < uint16(latest-records[right].Round)
})
if len(records) > currentJoustHistoryRecordCount {
records = records[:currentJoustHistoryRecordCount]
}
return records, nil
}
// currentJoustRecordedWinLoss projects only durable, actually settled rounds.
// Never derive counters from future or simulated rounds: a fresh activity must
// show 0 wins / 0 losses until a real tournament reaches settlement.
func (service *Service) currentJoustRecordedWinLoss(
ctx context.Context,
accountID string,
catalog *dnfjoust.Catalog,
) (map[byte][2]uint16, error) {
if catalog == nil {
return nil, dnfjoust.ErrRoundUnavailable
}
records, err := service.loadCurrentJoustHistoryOverrides(ctx, accountID)
if err != nil {
return nil, err
}
counters := make(map[byte][2]uint16, len(catalog.Riders()))
for round, record := range records {
tournament, tournamentErr := catalog.TournamentFor(round)
if tournamentErr != nil {
continue
}
if tournament.Champion() != record.Winner {
// Do not turn a stale pre-restoration row into a login failure. It is
// deliberately ignored until a settlement matches the active, original
// bracket in full.
continue
}
for _, match := range tournament.Matches {
winner := counters[match.Winner]
winner[0]++
counters[match.Winner] = winner
loser := counters[match.Loser]
loser[1]++
counters[match.Loser] = loser
}
}
return counters, nil
}
func currentJoustWinnerMultiplier(opening dnfjoust.OpeningRound, winner byte) (float32, bool) {
for _, rider := range opening.Riders {
if rider.ID == winner && rider.Multiplier > 0 && !math.IsNaN(float64(rider.Multiplier)) && !math.IsInf(float64(rider.Multiplier), 0) {
return rider.Multiplier, true
}
}
return 0, false
}
func (service *Service) loadCurrentJoustHistoryOverrides(
ctx context.Context,
accountID string,
) (map[uint16]currentJoustHistoryRecord, error) {
result := make(map[uint16]currentJoustHistoryRecord)
if service == nil {
return result, nil
}
repositories, ok := service.repositoryGroup()
if !ok || repositories.Settings == nil {
return result, nil
}
record, found, err := repositories.Settings.Load(ctx, currentJoustHistoryScope(accountID))
if err != nil || !found || record.Values == nil || strings.TrimSpace(record.Values[currentJoustHistorySettingsKey]) == "" {
return result, err
}
var stored []currentJoustHistoryRecord
if err := json.Unmarshal([]byte(record.Values[currentJoustHistorySettingsKey]), &stored); err != nil {
return nil, fmt.Errorf("decode joust history: %w", err)
}
for _, item := range stored {
if item.Round != 0 && item.Multiplier > 0 && !math.IsNaN(float64(item.Multiplier)) && !math.IsInf(float64(item.Multiplier), 0) {
result[item.Round] = item
}
}
return result, nil
}
func (service *Service) persistCurrentJoustHistoryRecord(
ctx context.Context,
accountID string,
completed currentJoustHistoryRecord,
now time.Time,
) error {
if service == nil || completed.Round == 0 || completed.Multiplier <= 0 {
return dnfjoust.ErrSettlementInvalid
}
repositories, ok := service.repositoryGroup()
if !ok || repositories.Settings == nil {
return errors.New("joust history settings repository unavailable")
}
service.joustHistoryMu.Lock()
defer service.joustHistoryMu.Unlock()
scope := currentJoustHistoryScope(accountID)
record, found, err := repositories.Settings.Load(ctx, scope)
if err != nil {
return err
}
if !found {
record = dnfrepo.SettingsRecord{Scope: scope, Values: make(map[string]string)}
} else {
record = dnfrepo.CloneSettings(record)
if record.Values == nil {
record.Values = make(map[string]string)
}
}
stored := make([]currentJoustHistoryRecord, 0, currentJoustHistoryRecordCount)
if raw := strings.TrimSpace(record.Values[currentJoustHistorySettingsKey]); raw != "" {
if err := json.Unmarshal([]byte(raw), &stored); err != nil {
return fmt.Errorf("decode joust history: %w", err)
}
}
updated := make([]currentJoustHistoryRecord, 0, currentJoustHistoryRecordCount)
updated = append(updated, completed)
for _, item := range stored {
if item.Round != completed.Round && len(updated) < currentJoustHistoryRecordCount {
updated = append(updated, item)
}
}
encoded, err := json.Marshal(updated)
if err != nil {
return err
}
record.Values[currentJoustHistorySettingsKey] = string(encoded)
if now.IsZero() {
now = time.Now().UTC()
}
record.UpdatedAt = now.UTC()
return dnfrepo.SaveSettingsFields(ctx, repositories.Settings, record, dnfrepo.SettingsFieldValues)
}