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,147 @@
|
||||
package raid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// LukeCardDisplayMsgID is the tarot display notification the client's
|
||||
// CardSetWindow is driven by: opcode 1181 (0x49D), LUKE_RAID_CARD_DISPLAY.
|
||||
//
|
||||
// It is a server push. The name table lists no C2S counterpart -- every
|
||||
// LUKE_RAID_* packet is an ENUM_NOTIPACKET_* -- and the handler at 0x010CDB90
|
||||
// is registered by 0x010CDF50, so nothing the client sends can produce one.
|
||||
const LukeCardDisplayMsgID uint16 = 1181
|
||||
|
||||
// The body is a single record with no outer count:
|
||||
//
|
||||
// u8 cardSetIndex index of the day's [CARD SET MAP INFO] record; the
|
||||
// client only checks it against the record count and
|
||||
// drops the WHOLE packet when it is out of range
|
||||
// u8 N number of card records (one per phase-one sanctum = 3)
|
||||
// N x {
|
||||
// u16 A sanctum dungeon id (5054 / 5055 / 5056)
|
||||
// u8 B that sanctum's [PATTERN] index in LukeCardSet.etc
|
||||
// u8 K card slots of that pattern = its [CARD POS] pair count
|
||||
// K x {
|
||||
// u32 P the tarot card of this slot: a [CARD] monster id
|
||||
// u8 Q read but never consumed by this client build; must be
|
||||
// sent, or every following field shifts
|
||||
// }
|
||||
// }
|
||||
// u8 openWindowFlag non-zero pops the CardSetWindow on its own
|
||||
//
|
||||
// Field widths are pinned by the handler's read helpers: 1 byte 0x34E3970,
|
||||
// 2 bytes 0x34E39E0, 4 bytes 0x34E3A50 (tooling: tools\_rev_卢克塔罗牌_1181规格_20260929.md
|
||||
// §1.3). There are no coordinates in the body -- the client takes those from
|
||||
// `[CARD POS]` in LukeCardSet.etc itself.
|
||||
const (
|
||||
// lukeCardDisplayMaxRecords bounds N: it is a u8 on the wire.
|
||||
lukeCardDisplayMaxRecords = 255
|
||||
// lukeCardDisplaySlotPadding is the Q byte of every slot. This client build
|
||||
// reads it into [ebp-0x12] and never looks at it again; it is emitted as a
|
||||
// fixed zero rather than guessed at.
|
||||
lukeCardDisplaySlotPadding byte = 0
|
||||
)
|
||||
|
||||
var ErrLukeCardDisplayShapeInvalid = errors.New("luke card display shape is invalid")
|
||||
|
||||
// LukeCardDisplaySanctum is one card record: which sanctum, which of its
|
||||
// patterns, and the tarot card sitting in each of that pattern's slots.
|
||||
type LukeCardDisplaySanctum struct {
|
||||
// DungeonID is field A. It must be one of the sanctum ids the 1181 handler
|
||||
// can resolve in LukeCardSet.etc, or the window draws nothing for it.
|
||||
DungeonID uint32
|
||||
// Pattern is field B: the index half of that sanctum's `[PATTERN]` key.
|
||||
Pattern int
|
||||
// Cards is the P field of every slot, in slot order. Its length is K, and it
|
||||
// must equal the pattern's `[CARD POS]` pair count: the client walks the slot
|
||||
// coordinates and the P list together, so a short list reads past the end.
|
||||
Cards []uint32
|
||||
}
|
||||
|
||||
// LukeCardDisplay is one 1181 body before encoding.
|
||||
type LukeCardDisplay struct {
|
||||
// CardSetIndex is the day's `[CARD SET MAP INFO]` record index.
|
||||
CardSetIndex byte
|
||||
// CardSetCount is how many such records the client holds, i.e. the exclusive
|
||||
// upper bound the client validates CardSetIndex against. It comes from
|
||||
// luke.etc's `[CARD SET MAP INFO]` (5 records = 15 values / 3) and the
|
||||
// builder refuses to emit a body the client would drop whole.
|
||||
CardSetCount int
|
||||
// Sanctums is N records, one per phase-one sanctum, in wire order.
|
||||
Sanctums []LukeCardDisplaySanctum
|
||||
// OpenWindow asks the client to open the CardSetWindow itself.
|
||||
OpenWindow bool
|
||||
}
|
||||
|
||||
// LukeCardDisplayBodySize is the exact body length of that shape, so a caller
|
||||
// can size a buffer without encoding it first.
|
||||
func LukeCardDisplayBodySize(sanctums []LukeCardDisplaySanctum) int {
|
||||
size := 2 + 1 // cardSetIndex, N, openWindowFlag
|
||||
for _, sanctum := range sanctums {
|
||||
size += 4 + 5*len(sanctum.Cards) // A, B, K, then K x (u32 P, u8 Q)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// BuildLukeCardDisplayBody encodes one 1181 body.
|
||||
//
|
||||
// It refuses anything the client would silently mis-draw instead of sending a
|
||||
// half-described packet: an out-of-range cardSetIndex (the client drops the
|
||||
// whole packet), a non-positive N or K, a dungeon id that does not fit its u16
|
||||
// field, a zero card id (the client reads a missing key as an empty slot, so a
|
||||
// zero is always a caller bug), or a card count above the u8 field width.
|
||||
func BuildLukeCardDisplayBody(display LukeCardDisplay) ([]byte, error) {
|
||||
if display.CardSetCount <= 0 {
|
||||
return nil, fmt.Errorf("%w: card_set_count=%d want_positive", ErrLukeCardDisplayShapeInvalid, display.CardSetCount)
|
||||
}
|
||||
if int(display.CardSetIndex) >= display.CardSetCount {
|
||||
return nil, fmt.Errorf("%w: card_set_index=%d card_set_count=%d",
|
||||
ErrLukeCardDisplayShapeInvalid, display.CardSetIndex, display.CardSetCount)
|
||||
}
|
||||
if len(display.Sanctums) == 0 || len(display.Sanctums) > lukeCardDisplayMaxRecords {
|
||||
return nil, fmt.Errorf("%w: records=%d want=1..%d",
|
||||
ErrLukeCardDisplayShapeInvalid, len(display.Sanctums), lukeCardDisplayMaxRecords)
|
||||
}
|
||||
for index, sanctum := range display.Sanctums {
|
||||
if sanctum.DungeonID == 0 || sanctum.DungeonID > math.MaxUint16 {
|
||||
return nil, fmt.Errorf("%w: record=%d dungeon=%d want=1..%d",
|
||||
ErrLukeCardDisplayShapeInvalid, index, sanctum.DungeonID, math.MaxUint16)
|
||||
}
|
||||
if sanctum.Pattern < 0 || sanctum.Pattern > math.MaxUint8 {
|
||||
return nil, fmt.Errorf("%w: record=%d dungeon=%d pattern=%d want=0..%d",
|
||||
ErrLukeCardDisplayShapeInvalid, index, sanctum.DungeonID, sanctum.Pattern, math.MaxUint8)
|
||||
}
|
||||
if len(sanctum.Cards) == 0 || len(sanctum.Cards) > lukeCardDisplayMaxRecords {
|
||||
return nil, fmt.Errorf("%w: record=%d dungeon=%d slots=%d want=1..%d",
|
||||
ErrLukeCardDisplayShapeInvalid, index, sanctum.DungeonID, len(sanctum.Cards), lukeCardDisplayMaxRecords)
|
||||
}
|
||||
for slot, cardID := range sanctum.Cards {
|
||||
if cardID == 0 {
|
||||
return nil, fmt.Errorf("%w: record=%d dungeon=%d slot=%d card=0",
|
||||
ErrLukeCardDisplayShapeInvalid, index, sanctum.DungeonID, slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer := packetWriter{buf: make([]byte, 0, LukeCardDisplayBodySize(display.Sanctums))}
|
||||
writer.writeByte(display.CardSetIndex)
|
||||
writer.writeByte(byte(len(display.Sanctums)))
|
||||
for _, sanctum := range display.Sanctums {
|
||||
writer.writeUint16(uint16(sanctum.DungeonID))
|
||||
writer.writeByte(byte(sanctum.Pattern))
|
||||
writer.writeByte(byte(len(sanctum.Cards)))
|
||||
for _, cardID := range sanctum.Cards {
|
||||
writer.writeUint32(cardID)
|
||||
writer.writeByte(lukeCardDisplaySlotPadding)
|
||||
}
|
||||
}
|
||||
var openWindow byte
|
||||
if display.OpenWindow {
|
||||
openWindow = 1
|
||||
}
|
||||
writer.writeByte(openWindow)
|
||||
return writer.bytes(), nil
|
||||
}
|
||||
Reference in new issue
Block a user