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

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)
}