DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,251 @@
|
||||
package dnfbridge
|
||||
|
||||
// The m7 pass: the two item-auction reports that came out of the 无色小晶块 test.
|
||||
//
|
||||
// 1. 「拍卖行最低价(以一口价为准)」 showed SIX identical rows for a market with
|
||||
// one listing. The op182 response's last twelve dwords are not trade history:
|
||||
// FUN_01d1d720 reads exactly two groups of six and hands them to the panel
|
||||
// filler (FUN_0300e2f0 -> FUN_0300a520 for owner 7), and FUN_0300b190 paints
|
||||
// each pair as dstr 46422 「%d个」 + dstr 46252/60160 「每个 %s 金币」, with
|
||||
// dstr 46450 「暂无拍卖品」 for a slot whose price or quantity is below 1.
|
||||
// Repeating the average in all six claimed six offers that did not exist.
|
||||
//
|
||||
// 2. The buyer could only take the whole stack. A buyout-only listing published
|
||||
// its unit price in the 当前竞拍价 slot, so the client believed a start price
|
||||
// was set and appended dstr 46263 「已设定拍卖起始价的物品无法单个购买。」
|
||||
// to the confirm dialog. The slot has to carry the -1 sentinel the painters
|
||||
// test (see currentAuctionNoPrice).
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dnfauction "longheng.io/server/internal/modules/dnf/auction"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. The buyout-only row must publish -1 in the 当前竞拍价 slot.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestCurrentAuctionBuyoutOnlyRowCarriesTheNoBidSentinel replays the live case:
|
||||
// item 3037 (无色小晶块) x2000 registered at 一口价 100 with 竞拍价 left empty.
|
||||
// The op183 body the client sent was
|
||||
//
|
||||
// 00 24 6601 DD0B0000 D0070000 FFFFFFFF 64000000 64000000 18000000
|
||||
// item count bid=-1 instant unit duration
|
||||
//
|
||||
// so the row must come back with -1 where the seller has no bid, and the whole
|
||||
// stack total 200000 in the 一口价 pair. Before this fix the row carried
|
||||
// 200000/200000/100 and the client offered no single-unit purchase.
|
||||
func TestCurrentAuctionBuyoutOnlyRowCarriesTheNoBidSentinel(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
seedCurrentAuctionBridgeCharacter(t, ctx, repositories, "10", "account-seller", "卖家", 20_000, map[string]dnfrepo.ItemStack{
|
||||
"0:9": {ItemID: 3037, Count: 2000},
|
||||
})
|
||||
owner, _ := dnfauction.NewOwner(repositories)
|
||||
if _, err := owner.Register(ctx, dnfauction.RegisterCommand{
|
||||
ListingID: 51, SellerID: "10", AccountID: "account-seller", SellerName: "卖家",
|
||||
ListType: 0, SourceSlot: 9, ExpectedID: 3037, Count: 2000, UnitPrice: 100,
|
||||
StartPrice: -1, BuyoutPrice: 100, Duration: time.Hour, ListingLimit: 5,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listings, err := owner.Search(ctx, []uint32{3037})
|
||||
if err != nil || len(listings) != 1 {
|
||||
t.Fatalf("search=%+v err=%v", listings, err)
|
||||
}
|
||||
if listings[0].HasBidding() {
|
||||
t.Fatalf("registration invented a start price: %+v", listings[0])
|
||||
}
|
||||
eligibility := func(uint32) bool { return true }
|
||||
row := buildCurrentAuctionSearchBody(listings, eligibility)[7:]
|
||||
bid := binary.LittleEndian.Uint32(row[0x09:0x0D])
|
||||
buyout := binary.LittleEndian.Uint32(row[0x0D:0x11])
|
||||
unit := binary.LittleEndian.Uint32(row[0x11:0x15])
|
||||
if bid != currentAuctionNoPrice {
|
||||
t.Fatalf("op186 当前竞拍价=%d (0x%08X) want the -1 sentinel; a number here makes the client refuse a single-unit buy",
|
||||
bid, bid)
|
||||
}
|
||||
if buyout != 200_000 || unit != 100 {
|
||||
t.Fatalf("op186 一口价=%d 单价=%d want 200000/100", buyout, unit)
|
||||
}
|
||||
owned := buildCurrentAuctionOwnedBody(listings, "10", eligibility)[2:]
|
||||
if got := binary.LittleEndian.Uint32(owned[0x08:0x0C]); got != currentAuctionNoPrice {
|
||||
t.Fatalf("op188 当前竞拍价=%d (0x%08X) want the -1 sentinel", got, got)
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(owned[0x0C:0x10]); got != 200_000 {
|
||||
t.Fatalf("op188 一口价=%d want 200000", got)
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(owned[0x93:0x97]); got != 100 {
|
||||
t.Fatalf("op188 单价=%d want 100", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The sentinel is shared with the gold consignment rows; pin that they cannot
|
||||
// drift apart, since both painters compare against the same literal.
|
||||
func TestCurrentAuctionNoPriceSentinelIsThePaintersLiteral(t *testing.T) {
|
||||
if currentAuctionNoPrice != 0xFFFFFFFF {
|
||||
t.Fatalf("sentinel=0x%08X want 0xFFFFFFFF", currentAuctionNoPrice)
|
||||
}
|
||||
if currentGoldConsignNoBid != currentAuctionNoPrice {
|
||||
t.Fatalf("gold sentinel=0x%08X but the item sentinel is 0x%08X",
|
||||
currentGoldConsignNoBid, currentAuctionNoPrice)
|
||||
}
|
||||
if currentAuctionNoPrice == 0 {
|
||||
t.Fatal("the sentinel must not be zero - zero is a real price")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. The op182 ladder.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCurrentAuctionPriceLadderMergesAndSortsByUnitPrice(t *testing.T) {
|
||||
listing := func(price int64, count int64) dnfauction.Listing {
|
||||
return dnfauction.Listing{UnitPrice: price, Item: dnfrepo.ItemStack{ItemID: 3037, Count: count}}
|
||||
}
|
||||
ladder := currentAuctionPriceLadder([]dnfauction.Listing{
|
||||
listing(120, 300),
|
||||
listing(100, 2000),
|
||||
listing(100, 500), // same price as the previous one: merged, not repeated
|
||||
}, currentAuctionPriceLadderSlots)
|
||||
if len(ladder) != currentAuctionPriceLadderSlots {
|
||||
t.Fatalf("ladder length=%d want %d", len(ladder), currentAuctionPriceLadderSlots)
|
||||
}
|
||||
if ladder[0] != (currentAuctionPriceRung{unitPrice: 100, quantity: 2500}) {
|
||||
t.Fatalf("rung0=%+v want 100/2500", ladder[0])
|
||||
}
|
||||
if ladder[1] != (currentAuctionPriceRung{unitPrice: 120, quantity: 300}) {
|
||||
t.Fatalf("rung1=%+v want 120/300", ladder[1])
|
||||
}
|
||||
for index := 2; index < currentAuctionPriceLadderSlots; index++ {
|
||||
if ladder[index] != (currentAuctionPriceRung{}) {
|
||||
t.Fatalf("rung%d=%+v want the empty rung", index, ladder[index])
|
||||
}
|
||||
}
|
||||
|
||||
// Junk rows never invent a rung.
|
||||
empty := currentAuctionPriceLadder([]dnfauction.Listing{
|
||||
listing(0, 5), listing(50, 0), listing(-3, 9),
|
||||
}, currentAuctionPriceLadderSlots)
|
||||
for index, rung := range empty {
|
||||
if rung != (currentAuctionPriceRung{}) {
|
||||
t.Fatalf("rung%d of the junk-only ladder=%+v want empty", index, rung)
|
||||
}
|
||||
}
|
||||
|
||||
// More distinct prices than slots: the cheapest win, ascending.
|
||||
many := currentAuctionPriceLadder([]dnfauction.Listing{
|
||||
listing(700, 7), listing(100, 1), listing(300, 3), listing(200, 2),
|
||||
listing(600, 6), listing(500, 5), listing(400, 4),
|
||||
}, currentAuctionPriceLadderSlots)
|
||||
for index, want := range []uint32{100, 200, 300, 400, 500, 600} {
|
||||
if many[index].unitPrice != want {
|
||||
t.Fatalf("rung%d price=%d want %d", index, many[index].unitPrice, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCurrentAuctionAverageResponseCarriesTheLadder walks the whole op182
|
||||
// answer: pay byte, the average, the total count, then the six prices and the
|
||||
// six quantities the 上架 dialog draws.
|
||||
func TestCurrentAuctionAverageResponseCarriesTheLadder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
seedCurrentAuctionBridgeCharacter(t, ctx, repositories, "10", "account-seller", "卖家", 20_000, map[string]dnfrepo.ItemStack{
|
||||
"0:9": {ItemID: 3037, Count: 2000},
|
||||
"0:8": {ItemID: 3037, Count: 300},
|
||||
})
|
||||
owner, _ := dnfauction.NewOwner(repositories)
|
||||
for index, probe := range []struct {
|
||||
listingID uint64
|
||||
slot int16
|
||||
count int64
|
||||
price int64
|
||||
}{
|
||||
{61, 9, 2000, 100},
|
||||
{62, 8, 300, 120},
|
||||
} {
|
||||
if _, err := owner.Register(ctx, dnfauction.RegisterCommand{
|
||||
ListingID: probe.listingID, SellerID: "10", AccountID: "account-seller", SellerName: "卖家",
|
||||
ListType: 0, SourceSlot: probe.slot, ExpectedID: 3037, Count: probe.count,
|
||||
UnitPrice: probe.price, StartPrice: -1, BuyoutPrice: probe.price,
|
||||
Duration: time.Hour, ListingLimit: 5,
|
||||
}); err != nil {
|
||||
t.Fatalf("register %d: %v", index, err)
|
||||
}
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{gameUpperHeader: gameUpperHeaderChannel13, gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
auctionItemEligibility: func(itemID uint32) bool { return itemID == 3037 },
|
||||
}
|
||||
session := &gameSession{conn: connection, selectedCharacterID: 11, accountID: "account-buyer"}
|
||||
request := make([]byte, 8)
|
||||
request[0] = currentAuctionPayTypeGold
|
||||
binary.LittleEndian.PutUint32(request[1:5], 3037)
|
||||
if err := service.sendCurrentAuctionAveragePriceLocked(session, request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
packet, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("trailing=%x", trailing)
|
||||
}
|
||||
if packet.Header.MsgID != uint16(dnfenum.CmdPacketAuctionAskAveragePrice) {
|
||||
t.Fatalf("header=%+v", packet.Header)
|
||||
}
|
||||
body := packet.Body
|
||||
// 1 success + 1 pay + 4 average + 4 count + 6*4 prices + 6*4 quantities.
|
||||
if len(body) != 2+4+4+6*4+6*4 {
|
||||
t.Fatalf("body len=%d want 58 (%x)", len(body), body)
|
||||
}
|
||||
if body[0] != 1 || body[1] != currentAuctionPayTypeGold {
|
||||
t.Fatalf("envelope=%x", body[:2])
|
||||
}
|
||||
// The average is (2000*100 + 300*120) / 2300 = 102.6 -> 102.
|
||||
if got := binary.LittleEndian.Uint32(body[2:6]); got != 102 {
|
||||
t.Fatalf("average=%d want 102", got)
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(body[6:10]); got != 2300 {
|
||||
t.Fatalf("count=%d want 2300", got)
|
||||
}
|
||||
wantPrices := []uint32{100, 120, 0, 0, 0, 0}
|
||||
wantCounts := []uint32{2000, 300, 0, 0, 0, 0}
|
||||
for index := 0; index < currentAuctionPriceLadderSlots; index++ {
|
||||
price := binary.LittleEndian.Uint32(body[10+index*4 : 14+index*4])
|
||||
quantity := binary.LittleEndian.Uint32(body[10+24+index*4 : 14+24+index*4])
|
||||
if price != wantPrices[index] || quantity != wantCounts[index] {
|
||||
t.Fatalf("rung%d=%d/%d want %d/%d (body=%x)", index, price, quantity,
|
||||
wantPrices[index], wantCounts[index], body)
|
||||
}
|
||||
}
|
||||
|
||||
// With nothing on sale the ladder is empty and only the PVF reference price
|
||||
// fills the average, so the dialog can still open on a brand-new item.
|
||||
connection.write.Reset()
|
||||
empty := make([]byte, 8)
|
||||
empty[0] = currentAuctionPayTypeGold
|
||||
binary.LittleEndian.PutUint32(empty[1:5], 3001)
|
||||
if err := service.sendCurrentAuctionAveragePriceLocked(session, empty); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idle, _ := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if len(idle.Body) != 58 {
|
||||
t.Fatalf("empty-market body len=%d want 58", len(idle.Body))
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(idle.Body[6:10]); got != 0 {
|
||||
t.Fatalf("empty-market count=%d want 0", got)
|
||||
}
|
||||
for index := 0; index < currentAuctionPriceLadderSlots; index++ {
|
||||
if got := binary.LittleEndian.Uint32(idle.Body[10+index*4 : 14+index*4]); got != 0 {
|
||||
t.Fatalf("empty-market rung%d price=%d want 0", index, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user