服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
98 lines
3.3 KiB
Go
98 lines
3.3 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestCurrentPartyUDPRelayForwardBudgetLeavesNormalTrafficAlone pins the
|
|
// "defensive only" half of the forwarding budget: traffic a real party produces
|
|
// (here 60 datagrams per second for a minute, and a 2,000-datagram combat burst)
|
|
// is never refused, so the guard cannot change normal play.
|
|
func TestCurrentPartyUDPRelayForwardBudgetLeavesNormalTrafficAlone(t *testing.T) {
|
|
leg := ¤tPartyUDPRelayLeg{}
|
|
now := time.Unix(1758300000, 0).UTC()
|
|
for tick := 0; tick < 60; tick++ {
|
|
now = now.Add(time.Second)
|
|
for packet := 0; packet < 60; packet++ {
|
|
admitted, report, _ := leg.admitForward(now)
|
|
if !admitted || report {
|
|
t.Fatalf("normal traffic refused at second=%d packet=%d", tick, packet)
|
|
}
|
|
}
|
|
}
|
|
burst := ¤tPartyUDPRelayLeg{}
|
|
for packet := 0; packet < 2000; packet++ {
|
|
admitted, _, _ := burst.admitForward(now)
|
|
if !admitted {
|
|
t.Fatalf("2000-packet combat burst refused at packet=%d", packet)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCurrentPartyUDPRelayForwardBudgetCapsARetransmitStorm is the measured
|
|
// incident: a client that stops dispatching retransmits the same 9-byte payload
|
|
// at ~63,400 datagrams per second. The leg must forward at its budget and refuse
|
|
// the rest, and the refusal must be reported once per interval rather than per
|
|
// datagram.
|
|
func TestCurrentPartyUDPRelayForwardBudgetCapsARetransmitStorm(t *testing.T) {
|
|
leg := ¤tPartyUDPRelayLeg{}
|
|
start := time.Unix(1758300000, 0).UTC()
|
|
admitted := 0
|
|
reports := 0
|
|
simulated := 0
|
|
for second := 0; second < 5; second++ {
|
|
now := start.Add(time.Duration(second) * time.Second)
|
|
for packet := 0; packet < 63400; packet++ {
|
|
simulated++
|
|
ok, report, _ := leg.admitForward(now)
|
|
if ok {
|
|
admitted++
|
|
}
|
|
if report {
|
|
reports++
|
|
}
|
|
}
|
|
}
|
|
if simulated != 5*63400 {
|
|
t.Fatalf("simulated=%d", simulated)
|
|
}
|
|
// One full burst plus five seconds of refill, with a little slack for the
|
|
// bucket's per-datagram accounting.
|
|
upper := currentPartyUDPRelayForwardBurstPackets +
|
|
int(5*currentPartyUDPRelayForwardRatePerSecond) + 100
|
|
if admitted > upper {
|
|
t.Fatalf("admitted=%d exceeds budget ceiling=%d", admitted, upper)
|
|
}
|
|
if admitted > simulated/8 {
|
|
t.Fatalf("admitted=%d of %d: a 63k/s storm was not cut down", admitted, simulated)
|
|
}
|
|
if reports == 0 || reports > 6 {
|
|
t.Fatalf("throttle reports=%d, want at least one and never one per datagram", reports)
|
|
}
|
|
leg.mu.Lock()
|
|
refused := leg.throttledPackets
|
|
leg.mu.Unlock()
|
|
if refused != uint64(simulated-admitted) {
|
|
t.Fatalf("refused=%d admitted=%d simulated=%d", refused, admitted, simulated)
|
|
}
|
|
}
|
|
|
|
// TestCurrentPartyUDPRelayForwardBudgetRefillsAfterTheStorm pins recovery: once
|
|
// the client behaves again the same leg forwards normally, so a single storm does
|
|
// not permanently degrade the party transport.
|
|
func TestCurrentPartyUDPRelayForwardBudgetRefillsAfterTheStorm(t *testing.T) {
|
|
leg := ¤tPartyUDPRelayLeg{}
|
|
now := time.Unix(1758300000, 0).UTC()
|
|
for packet := 0; packet < 10000; packet++ {
|
|
leg.admitForward(now)
|
|
}
|
|
// One second of quiet, then a normal second of play.
|
|
now = now.Add(2 * time.Second)
|
|
for packet := 0; packet < 60; packet++ {
|
|
if admitted, _, _ := leg.admitForward(now); !admitted {
|
|
t.Fatalf("leg did not recover after the storm at packet=%d", packet)
|
|
}
|
|
}
|
|
}
|