服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
142 lines
5.4 KiB
Go
142 lines
5.4 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
)
|
|
|
|
// currentPartySnapshotPublishRetryDelays bounds the re-publish attempts after a
|
|
// party formation was deferred because the peer endpoint table could not be
|
|
// built yet.
|
|
//
|
|
// The first entry is short because the client's native UDP probe answer is
|
|
// normally already in flight when the invite is accepted; the later ones cover a
|
|
// client that only probes again when its scene changes (town entry, selector
|
|
// return). The list is deliberately finite and pre-scheduled: a party formation
|
|
// must never be able to arm an unbounded chain of timers, and the operator can
|
|
// read the attempt count off the log.
|
|
var currentPartySnapshotPublishRetryDelays = []time.Duration{
|
|
time.Second,
|
|
3 * time.Second,
|
|
7 * time.Second,
|
|
15 * time.Second,
|
|
}
|
|
|
|
// scheduleManagedRuntimePartySnapshotRetry re-runs the two-phase formation frame
|
|
// after publishManagedRuntimePartySnapshots deferred it.
|
|
//
|
|
// Live 2026-09-29 on 203.0.113.12: twelve game-managed-runtime-party-snapshot-deferred
|
|
// events, all with reason native_peer_endpoint_table_incomplete_before_two_phase_publish
|
|
// (party 48, member 32), and the operator's report is the matching symptom --
|
|
// "有时候组队还组不上 显示连接中". A deferral sends nothing at all: the accepting
|
|
// player holds the class1 ACK while no member ever receives op11/op9/op153, so the
|
|
// client has nothing to resolve the peer endpoint from and says 连接中 forever.
|
|
//
|
|
// Every attempt re-reads the CURRENT manager snapshot and re-runs the same
|
|
// admission gate, so a party that has since been dissolved, re-formed or
|
|
// published is left alone. Arming a new token supersedes the previous token's
|
|
// pending attempts, and a successful publish settles the token outright -- which
|
|
// is what makes it safe to re-publish at all, because op11 is a formation
|
|
// handshake and replaying it over a live link makes the client tear the peer
|
|
// connection down and re-enter the connecting state.
|
|
func (s *Service) scheduleManagedRuntimePartySnapshotRetry(
|
|
state alignedcmd.PartyState,
|
|
memberID uint16,
|
|
reason string,
|
|
) {
|
|
if s == nil || state.PartyID <= 0 || state.PartyID >= int(^uint16(0)) {
|
|
return
|
|
}
|
|
queue := s.ensureGameplayTimeQueue()
|
|
if queue == nil {
|
|
return
|
|
}
|
|
token := s.armManagedRuntimePartySnapshotRetry(state.PartyID)
|
|
captured := cloneRuntimePartyState(state)
|
|
for attempt, delay := range currentPartySnapshotPublishRetryDelays {
|
|
attempt, delay := attempt, delay
|
|
last := attempt == len(currentPartySnapshotPublishRetryDelays)-1
|
|
name := fmt.Sprintf("party-snapshot-publish-retry:%d:%d", captured.PartyID, attempt)
|
|
if err := queue.ScheduleAfter(name, delay, func(time.Time) {
|
|
if !s.managedRuntimePartySnapshotRetryCurrent(captured.PartyID, token) {
|
|
// Either a later deferral re-armed the sequence or the formation frame
|
|
// has already been published for this membership.
|
|
return
|
|
}
|
|
published, err := s.publishManagedRuntimePartySnapshots(captured)
|
|
switch {
|
|
case err != nil:
|
|
s.logGameEvent(nil, "game-managed-runtime-party-snapshot-retry-failed",
|
|
"party_id", captured.PartyID,
|
|
"member_id", memberID,
|
|
"attempt", attempt,
|
|
"delay_seconds", int(delay/time.Second),
|
|
"defer_reason", reason,
|
|
"error", err)
|
|
case published:
|
|
s.logGameEvent(nil, "game-managed-runtime-party-snapshot-retry-published",
|
|
"party_id", captured.PartyID,
|
|
"member_id", memberID,
|
|
"attempt", attempt,
|
|
"delay_seconds", int(delay/time.Second),
|
|
"defer_reason", reason,
|
|
"member_count", len(runtimePartyMembers(captured)))
|
|
case last:
|
|
s.logGameEvent(nil, "game-managed-runtime-party-snapshot-retry-unresolved",
|
|
"party_id", captured.PartyID,
|
|
"member_id", memberID,
|
|
"attempts", len(currentPartySnapshotPublishRetryDelays),
|
|
"last_delay_seconds", int(delay/time.Second),
|
|
"defer_reason", reason)
|
|
}
|
|
}); err != nil {
|
|
s.logGameEvent(nil, "game-managed-runtime-party-snapshot-retry-schedule-failed",
|
|
"party_id", captured.PartyID,
|
|
"member_id", memberID,
|
|
"attempt", attempt,
|
|
"error", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// armManagedRuntimePartySnapshotRetry starts a new retry epoch for a party and
|
|
// returns its token. A later arming for the same party invalidates the earlier
|
|
// token, so two deferrals cannot race each other into a double publish.
|
|
func (s *Service) armManagedRuntimePartySnapshotRetry(partyID int) uint64 {
|
|
s.partyPublishMu.Lock()
|
|
defer s.partyPublishMu.Unlock()
|
|
s.partyPublishSerial++
|
|
token := s.partyPublishSerial
|
|
if s.partyPublishFormation == nil {
|
|
s.partyPublishFormation = make(map[int]uint64)
|
|
}
|
|
s.partyPublishFormation[partyID] = token
|
|
return token
|
|
}
|
|
|
|
// managedRuntimePartySnapshotRetryCurrent reports whether a scheduled attempt is
|
|
// still the live one for its party.
|
|
func (s *Service) managedRuntimePartySnapshotRetryCurrent(partyID int, token uint64) bool {
|
|
if s == nil || token == 0 {
|
|
return false
|
|
}
|
|
s.partyPublishMu.Lock()
|
|
defer s.partyPublishMu.Unlock()
|
|
return s.partyPublishFormation[partyID] == token
|
|
}
|
|
|
|
// settleManagedRuntimePartySnapshotRetry retires the pending retries for a party
|
|
// whose formation frame has been published. Every scheduled attempt for that
|
|
// party becomes a no-op, so the formation handshake is never replayed.
|
|
func (s *Service) settleManagedRuntimePartySnapshotRetry(partyID int) {
|
|
if s == nil || partyID <= 0 {
|
|
return
|
|
}
|
|
s.partyPublishMu.Lock()
|
|
defer s.partyPublishMu.Unlock()
|
|
delete(s.partyPublishFormation, partyID)
|
|
}
|