服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
397 lines
14 KiB
Go
397 lines
14 KiB
Go
// 本文件解析副本类 C2S 请求体。
|
|
// 只记录当前已经对齐的请求字段,复杂场景状态交给后续 dungeon owner 处理。
|
|
package dungeoncmd
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
)
|
|
|
|
const (
|
|
SelectDungeonRequestSize = 21
|
|
GetItemRequestSize = 20
|
|
MoveMapRequestSize = 100
|
|
DieMonsterRequestSize = 62
|
|
DieMonsterVariableBaseSize = 66
|
|
DieMonsterVariableCombatEntrySize = 10
|
|
DieMonsterVariableRuntimeTailSize = 43
|
|
DieMonsterVariableMaxCombatCount = 254
|
|
BossDieCheckRequestSize = 39
|
|
ChangeTutorialFlagRequestSize = 6
|
|
)
|
|
|
|
// SelectDungeonRequest 描述选择副本请求。
|
|
type SelectDungeonRequest struct {
|
|
DungeonID uint32
|
|
Difficulty byte
|
|
EntryOption uint16
|
|
SelectionMode byte
|
|
RuntimeState byte
|
|
RuntimeToken uint16
|
|
Reserved uint32
|
|
PartyState byte
|
|
// LeaderObjectKey is the legacy name for the u32 at offset 16. Its
|
|
// semantics are not proved for the current EXE, so owners must retain it
|
|
// as opaque request state and must not use it as an authority boundary.
|
|
LeaderObjectKey uint32
|
|
SpecialMode byte
|
|
OpaqueTail []byte
|
|
}
|
|
|
|
// GetItemRequest is the exact 20-byte ordinary scene-item pickup body written
|
|
// by the current EXE sub_2326920. Only DropObjectKey has proved ownership
|
|
// semantics; the remaining coordinates/tokens are retained by wire position.
|
|
// The separate 18-byte op43 writer is a script/special-object variant and is
|
|
// deliberately rejected by DecodeGetItemRequest.
|
|
type GetItemRequest struct {
|
|
DropObjectKey uint32
|
|
FixedZero byte
|
|
ObjectContext byte
|
|
PlayerX uint16
|
|
PlayerY uint16
|
|
Token0 uint16
|
|
DropX uint16
|
|
DropY uint16
|
|
Token1 uint16
|
|
Token2 uint16
|
|
}
|
|
|
|
// MoveMapRequest 描述切换房间请求。
|
|
type MoveMapRequest struct {
|
|
NextX byte
|
|
NextY byte
|
|
PositionX uint32
|
|
PositionY uint32
|
|
MoveKind byte
|
|
TimingToken uint16
|
|
ShortState [8]uint16
|
|
IntegerState [8]uint32
|
|
SequenceToken uint16
|
|
RuntimeTail [36]byte
|
|
RuntimeState byte
|
|
OpaqueTail []byte
|
|
}
|
|
|
|
// DieMonsterRequest is the current EXE op39 body. Unknown combat scalars are
|
|
// retained by position so the room owner can use the proven runtime object key
|
|
// without inventing semantics for the remaining fields.
|
|
type DieMonsterRequest struct {
|
|
Layout DieMonsterRequestLayout
|
|
RuntimeObjectKey uint32
|
|
OwnerObjectKey uint16
|
|
CombatState [4]uint32
|
|
StateFlag byte
|
|
ShortState [2]uint16
|
|
ByteState [4]byte
|
|
RandomState uint16
|
|
HasActorState byte
|
|
ActorState uint32
|
|
ActorValue uint64
|
|
ActorShortState [7]uint16
|
|
TailState [2]byte
|
|
CombatEntryCount byte
|
|
CombatEntries []byte
|
|
RuntimeTail []byte
|
|
OpaqueTail []byte
|
|
}
|
|
|
|
// BossDieCheckRequest is the exact 39-byte plaintext body written by the
|
|
// current EXE sub_24351D0 for class-1/op117. Only the two object keys have
|
|
// proven ownership semantics. The remaining values are retained by their
|
|
// native offsets so the bridge does not invent names for unproven fields.
|
|
type BossDieCheckRequest struct {
|
|
RelatedActorObjectKey uint16
|
|
TargetObjectKey uint16
|
|
ReservedZero uint32
|
|
Field08 uint32
|
|
Field12 byte
|
|
Field13 uint32
|
|
Field17 uint64
|
|
Field25 [7]uint16
|
|
}
|
|
|
|
type DieMonsterRequestLayout byte
|
|
|
|
const (
|
|
DieMonsterRequestLayoutFixed62 DieMonsterRequestLayout = iota + 1
|
|
DieMonsterRequestLayoutVariableCombat
|
|
)
|
|
|
|
// ChangeTutorialFlagRequest is the exact six-byte C2S body written by the
|
|
// current EXE sub_33BDAF0: u8 prefix, u32 progress, u8 commit flag. The
|
|
// count/entry layout belongs to the S2C success callback and must not be used
|
|
// to decode this request direction.
|
|
type ChangeTutorialFlagRequest struct {
|
|
Prefix byte
|
|
Progress uint32
|
|
CommitFlag byte
|
|
}
|
|
|
|
// DecodeSelectDungeonRequest 解析选择副本请求。
|
|
func DecodeSelectDungeonRequest(body []byte) (SelectDungeonRequest, error) {
|
|
if len(body) < SelectDungeonRequestSize {
|
|
return SelectDungeonRequest{}, fmt.Errorf("body too short: got %d want >= %d", len(body), SelectDungeonRequestSize)
|
|
}
|
|
request := SelectDungeonRequest{
|
|
DungeonID: binary.LittleEndian.Uint32(body[0:4]),
|
|
Difficulty: body[4],
|
|
EntryOption: binary.LittleEndian.Uint16(body[5:7]),
|
|
SelectionMode: body[7],
|
|
RuntimeState: body[8],
|
|
RuntimeToken: binary.LittleEndian.Uint16(body[9:11]),
|
|
Reserved: binary.LittleEndian.Uint32(body[11:15]),
|
|
PartyState: body[15],
|
|
LeaderObjectKey: binary.LittleEndian.Uint32(body[16:20]),
|
|
SpecialMode: body[20],
|
|
}
|
|
request.OpaqueTail = append([]byte(nil), body[SelectDungeonRequestSize:]...)
|
|
return request, nil
|
|
}
|
|
|
|
// DecodeGetItemRequest accepts only the current ordinary-drop writer boundary.
|
|
func DecodeGetItemRequest(body []byte) (GetItemRequest, error) {
|
|
if len(body) != GetItemRequestSize {
|
|
return GetItemRequest{}, fmt.Errorf("invalid body length: got %d want %d", len(body), GetItemRequestSize)
|
|
}
|
|
return GetItemRequest{
|
|
DropObjectKey: binary.LittleEndian.Uint32(body[0:4]),
|
|
FixedZero: body[4],
|
|
ObjectContext: body[5],
|
|
PlayerX: binary.LittleEndian.Uint16(body[6:8]),
|
|
PlayerY: binary.LittleEndian.Uint16(body[8:10]),
|
|
Token0: binary.LittleEndian.Uint16(body[10:12]),
|
|
DropX: binary.LittleEndian.Uint16(body[12:14]),
|
|
DropY: binary.LittleEndian.Uint16(body[14:16]),
|
|
Token1: binary.LittleEndian.Uint16(body[16:18]),
|
|
Token2: binary.LittleEndian.Uint16(body[18:20]),
|
|
}, nil
|
|
}
|
|
|
|
// DecodeMoveMapRequest parses the fixed 100-byte plaintext body written by the
|
|
// current EXE sub_17C9D80. Its first 63 bytes are followed by the exact 36-byte
|
|
// sub_17C8490 runtime block and one final state byte. The native opcode-45
|
|
// transport normally Twofish-pads this plaintext to 112 wire bytes; the client
|
|
// compatibility path must bypass that process-local cipher before this decoder.
|
|
func DecodeMoveMapRequest(body []byte) (MoveMapRequest, error) {
|
|
if len(body) != MoveMapRequestSize {
|
|
return MoveMapRequest{}, fmt.Errorf("invalid body length: got %d want %d", len(body), MoveMapRequestSize)
|
|
}
|
|
request := MoveMapRequest{
|
|
NextX: body[0],
|
|
NextY: body[1],
|
|
PositionX: binary.LittleEndian.Uint32(body[2:6]),
|
|
PositionY: binary.LittleEndian.Uint32(body[6:10]),
|
|
MoveKind: body[10],
|
|
TimingToken: binary.LittleEndian.Uint16(body[11:13]),
|
|
RuntimeState: body[99],
|
|
}
|
|
offset := 13
|
|
for index := range request.ShortState {
|
|
request.ShortState[index] = binary.LittleEndian.Uint16(body[offset : offset+2])
|
|
offset += 2
|
|
}
|
|
for index := range request.IntegerState {
|
|
request.IntegerState[index] = binary.LittleEndian.Uint32(body[offset : offset+4])
|
|
offset += 4
|
|
}
|
|
request.SequenceToken = binary.LittleEndian.Uint16(body[offset : offset+2])
|
|
copy(request.RuntimeTail[:], body[63:99])
|
|
return request, nil
|
|
}
|
|
|
|
// DecodeDieMonsterRequest parses both current EXE death-writer families.
|
|
// sub_2276500/sub_22767C0 emit the fixed 62-byte form. Tutorial APC writer
|
|
// sub_2570F20 emits 66+10*count bytes: a 23-byte prefix, count ten-byte combat
|
|
// entries, and a 43-byte runtime tail which includes sub_226F700 output.
|
|
// Unknown values are retained by exact boundary; extra bytes remain opaque so
|
|
// the runtime owner can reject an unproven extension before changing state.
|
|
func DecodeDieMonsterRequest(body []byte) (DieMonsterRequest, error) {
|
|
if len(body) < DieMonsterRequestSize {
|
|
return DieMonsterRequest{}, fmt.Errorf("body too short: got %d want >= %d", len(body), DieMonsterRequestSize)
|
|
}
|
|
if len(body) != DieMonsterRequestSize {
|
|
return decodeVariableDieMonsterRequest(body)
|
|
}
|
|
request := DieMonsterRequest{
|
|
Layout: DieMonsterRequestLayoutFixed62,
|
|
RuntimeObjectKey: binary.LittleEndian.Uint32(body[0:4]),
|
|
OwnerObjectKey: binary.LittleEndian.Uint16(body[4:6]),
|
|
StateFlag: body[22],
|
|
ShortState: [2]uint16{
|
|
binary.LittleEndian.Uint16(body[23:25]),
|
|
binary.LittleEndian.Uint16(body[25:27]),
|
|
},
|
|
ByteState: [4]byte{body[27], body[28], body[29], body[30]},
|
|
RandomState: binary.LittleEndian.Uint16(body[31:33]),
|
|
HasActorState: body[33],
|
|
ActorState: binary.LittleEndian.Uint32(body[34:38]),
|
|
ActorValue: binary.LittleEndian.Uint64(body[38:46]),
|
|
TailState: [2]byte{body[60], body[61]},
|
|
OpaqueTail: append([]byte(nil), body[DieMonsterRequestSize:]...),
|
|
}
|
|
for index := range request.CombatState {
|
|
offset := 6 + index*4
|
|
request.CombatState[index] = binary.LittleEndian.Uint32(body[offset : offset+4])
|
|
}
|
|
for index := range request.ActorShortState {
|
|
offset := 46 + index*2
|
|
request.ActorShortState[index] = binary.LittleEndian.Uint16(body[offset : offset+2])
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
// DecodeBossDieCheckRequest parses only the current EXE's exact plaintext
|
|
// boundary. The ordinary protected path pads the same body to 40 wire bytes;
|
|
// that ciphertext must not be accepted by this decoder.
|
|
func DecodeBossDieCheckRequest(body []byte) (BossDieCheckRequest, error) {
|
|
if len(body) != BossDieCheckRequestSize {
|
|
return BossDieCheckRequest{}, fmt.Errorf(
|
|
"invalid body length: got %d want %d",
|
|
len(body),
|
|
BossDieCheckRequestSize,
|
|
)
|
|
}
|
|
request := BossDieCheckRequest{
|
|
RelatedActorObjectKey: binary.LittleEndian.Uint16(body[0:2]),
|
|
TargetObjectKey: binary.LittleEndian.Uint16(body[2:4]),
|
|
ReservedZero: binary.LittleEndian.Uint32(body[4:8]),
|
|
Field08: binary.LittleEndian.Uint32(body[8:12]),
|
|
Field12: body[12],
|
|
Field13: binary.LittleEndian.Uint32(body[13:17]),
|
|
Field17: binary.LittleEndian.Uint64(body[17:25]),
|
|
}
|
|
for index := range request.Field25 {
|
|
offset := 25 + index*2
|
|
request.Field25[index] = binary.LittleEndian.Uint16(body[offset : offset+2])
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
func decodeVariableDieMonsterRequest(body []byte) (DieMonsterRequest, error) {
|
|
if len(body) < DieMonsterVariableBaseSize {
|
|
return DieMonsterRequest{}, fmt.Errorf(
|
|
"unsupported die-monster boundary: got %d want %d or >= %d",
|
|
len(body), DieMonsterRequestSize, DieMonsterVariableBaseSize,
|
|
)
|
|
}
|
|
count := int(body[22])
|
|
if count > DieMonsterVariableMaxCombatCount {
|
|
return DieMonsterRequest{}, fmt.Errorf(
|
|
"die-monster combat count exceeds current writer limit: got %d want <= %d",
|
|
count, DieMonsterVariableMaxCombatCount,
|
|
)
|
|
}
|
|
expected := DieMonsterVariableBaseSize + count*DieMonsterVariableCombatEntrySize
|
|
if len(body) < expected {
|
|
return DieMonsterRequest{}, fmt.Errorf(
|
|
"variable die-monster body too short: got %d want >= %d for count %d",
|
|
len(body), expected, count,
|
|
)
|
|
}
|
|
entriesStart := 23
|
|
entriesEnd := entriesStart + count*DieMonsterVariableCombatEntrySize
|
|
runtimeTailEnd := entriesEnd + DieMonsterVariableRuntimeTailSize
|
|
request := DieMonsterRequest{
|
|
Layout: DieMonsterRequestLayoutVariableCombat,
|
|
RuntimeObjectKey: binary.LittleEndian.Uint32(body[0:4]),
|
|
OwnerObjectKey: binary.LittleEndian.Uint16(body[4:6]),
|
|
CombatEntryCount: byte(count),
|
|
CombatEntries: append([]byte(nil), body[entriesStart:entriesEnd]...),
|
|
RuntimeTail: append([]byte(nil), body[entriesEnd:runtimeTailEnd]...),
|
|
OpaqueTail: append([]byte(nil), body[runtimeTailEnd:]...),
|
|
}
|
|
for index := range request.CombatState {
|
|
offset := 6 + index*4
|
|
request.CombatState[index] = binary.LittleEndian.Uint32(body[offset : offset+4])
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
// DecodeChangeTutorialFlagRequest parses the current EXE C2S writer. The
|
|
// boundary is exact so a protected/padded body cannot be mistaken for the
|
|
// plaintext tutorial-finish handshake.
|
|
func DecodeChangeTutorialFlagRequest(body []byte) (ChangeTutorialFlagRequest, error) {
|
|
if len(body) != ChangeTutorialFlagRequestSize {
|
|
return ChangeTutorialFlagRequest{}, fmt.Errorf(
|
|
"body length mismatch: got %d want %d",
|
|
len(body),
|
|
ChangeTutorialFlagRequestSize,
|
|
)
|
|
}
|
|
return ChangeTutorialFlagRequest{
|
|
Prefix: body[0],
|
|
Progress: binary.LittleEndian.Uint32(body[1:5]),
|
|
CommitFlag: body[5],
|
|
}, nil
|
|
}
|
|
|
|
func (r SelectDungeonRequest) String() string {
|
|
return fmt.Sprintf(
|
|
"dungeon=%d difficulty=%d entryOption=%d selectionMode=%d runtimeState=%d runtimeToken=%d reserved=%d partyState=%d leader=%d specialMode=%d tail=%d",
|
|
r.DungeonID, r.Difficulty, r.EntryOption, r.SelectionMode, r.RuntimeState,
|
|
r.RuntimeToken, r.Reserved, r.PartyState, r.LeaderObjectKey, r.SpecialMode, len(r.OpaqueTail),
|
|
)
|
|
}
|
|
|
|
func (r GetItemRequest) String() string {
|
|
return fmt.Sprintf(
|
|
"dropObject=%d fixed=%d context=%d player=(%d,%d) token0=%d drop=(%d,%d) token1=%d token2=%d",
|
|
r.DropObjectKey,
|
|
r.FixedZero,
|
|
r.ObjectContext,
|
|
r.PlayerX,
|
|
r.PlayerY,
|
|
r.Token0,
|
|
r.DropX,
|
|
r.DropY,
|
|
r.Token1,
|
|
r.Token2,
|
|
)
|
|
}
|
|
|
|
func (r MoveMapRequest) String() string {
|
|
return fmt.Sprintf(
|
|
"next=(%d,%d) position=(%d,%d) kind=%d sequence=%d tail=%d",
|
|
r.NextX, r.NextY, r.PositionX, r.PositionY, r.MoveKind, r.SequenceToken, len(r.OpaqueTail),
|
|
)
|
|
}
|
|
|
|
func (r DieMonsterRequest) String() string {
|
|
return fmt.Sprintf(
|
|
"layout=%d runtimeObject=%d ownerObject=%d actorState=%d combatEntries=%d runtimeTail=%d tail=%d",
|
|
r.Layout, r.RuntimeObjectKey, r.OwnerObjectKey, r.ActorState,
|
|
r.CombatEntryCount, len(r.RuntimeTail), len(r.OpaqueTail),
|
|
)
|
|
}
|
|
|
|
func (r BossDieCheckRequest) String() string {
|
|
return fmt.Sprintf(
|
|
"relatedActor=%d target=%d reserved=%d field08=%d field12=%d field13=%d field17=%d",
|
|
r.RelatedActorObjectKey,
|
|
r.TargetObjectKey,
|
|
r.ReservedZero,
|
|
r.Field08,
|
|
r.Field12,
|
|
r.Field13,
|
|
r.Field17,
|
|
)
|
|
}
|
|
|
|
func (r ChangeTutorialFlagRequest) String() string {
|
|
return fmt.Sprintf("prefix=%d progress=%d commit=%d", r.Prefix, r.Progress, r.CommitFlag)
|
|
}
|
|
|
|
func readU16(body []byte, offset int) uint16 {
|
|
if offset+2 <= len(body) {
|
|
return binary.LittleEndian.Uint16(body[offset:])
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func readU32(body []byte, offset int) uint32 {
|
|
if offset+4 <= len(body) {
|
|
return binary.LittleEndian.Uint32(body[offset:])
|
|
}
|
|
return 0
|
|
}
|