服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
202 lines
6.2 KiB
Go
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)
|
|
}
|