服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
230 lines
6.7 KiB
Go
230 lines
6.7 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
const (
|
|
currentPVPRoomInfoMsgID uint16 = 0x0029
|
|
currentPVPRoomStateMsgID uint16 = 0x002A
|
|
currentPVPSeatStateMsgID uint16 = 0x002B
|
|
currentPVPUserAreaMsgID uint16 = 0x0017
|
|
currentPVPUserState byte = 0x02
|
|
currentPVPRoomState byte = 0x01
|
|
currentPVPRoomManager byte = 0x00
|
|
currentPVPRoomSeatCount = 8
|
|
currentPVPEntireRoomID uint16 = 1
|
|
currentPVPDefaultBattleMode byte = 2
|
|
)
|
|
|
|
type currentPVPMakeRoomRequest struct {
|
|
roomNameType byte
|
|
roomName []byte
|
|
mapIndex int16
|
|
hasPassword bool
|
|
password []byte
|
|
specialBattleMode byte
|
|
dojoDifficulty byte
|
|
}
|
|
|
|
func parseCurrentPVPMakeRoomRequest(body []byte) (currentPVPMakeRoomRequest, error) {
|
|
var request currentPVPMakeRoomRequest
|
|
if len(body) == 0 {
|
|
return request, fmt.Errorf("pvp room request is empty")
|
|
}
|
|
offset := 0
|
|
readByte := func() (byte, error) {
|
|
if offset >= len(body) {
|
|
return 0, fmt.Errorf("pvp room request truncated at %d", offset)
|
|
}
|
|
value := body[offset]
|
|
offset++
|
|
return value, nil
|
|
}
|
|
readInt16 := func() (int16, error) {
|
|
if len(body)-offset < 2 {
|
|
return 0, fmt.Errorf("pvp room request missing map index")
|
|
}
|
|
value := int16(uint16(body[offset]) | uint16(body[offset+1])<<8)
|
|
offset += 2
|
|
return value, nil
|
|
}
|
|
readBytes := func(max int, field string) ([]byte, error) {
|
|
if len(body)-offset < 4 {
|
|
return nil, fmt.Errorf("pvp room request missing %s length", field)
|
|
}
|
|
length := int(uint32(body[offset]) | uint32(body[offset+1])<<8 | uint32(body[offset+2])<<16 | uint32(body[offset+3])<<24)
|
|
offset += 4
|
|
if length < 0 || length > max || len(body)-offset < length {
|
|
return nil, fmt.Errorf("pvp room request invalid %s length %d", field, length)
|
|
}
|
|
value := append([]byte(nil), body[offset:offset+length]...)
|
|
offset += length
|
|
return value, nil
|
|
}
|
|
|
|
var err error
|
|
if request.roomNameType, err = readByte(); err != nil {
|
|
return request, err
|
|
}
|
|
if request.roomNameType > 0x7f {
|
|
return request, fmt.Errorf("pvp room name type exceeds signed-byte range")
|
|
}
|
|
if request.roomNameType == 0 {
|
|
if request.roomName, err = readBytes(29, "room name"); err != nil {
|
|
return request, err
|
|
}
|
|
}
|
|
if request.mapIndex, err = readInt16(); err != nil {
|
|
return request, err
|
|
}
|
|
var hasPassword byte
|
|
if hasPassword, err = readByte(); err != nil {
|
|
return request, err
|
|
}
|
|
if hasPassword > 1 {
|
|
return request, fmt.Errorf("pvp room password flag is not boolean")
|
|
}
|
|
request.hasPassword = hasPassword == 1
|
|
if request.hasPassword {
|
|
if request.password, err = readBytes(8, "password"); err != nil {
|
|
return request, err
|
|
}
|
|
}
|
|
if request.specialBattleMode, err = readByte(); err != nil {
|
|
return request, err
|
|
}
|
|
// sub_10AD640 special3 writes the PVF difficulty+1; other writers use zero.
|
|
if request.specialBattleMode == 3 {
|
|
if len(body)-offset != 1 || body[offset] < 1 || body[offset] > 3 {
|
|
return request, fmt.Errorf("pvp dojo request has invalid difficulty")
|
|
}
|
|
request.dojoDifficulty = body[offset]
|
|
offset++
|
|
} else if len(body)-offset == 1 && body[offset] == 0 {
|
|
offset++
|
|
}
|
|
if offset != len(body) {
|
|
return request, fmt.Errorf("pvp room request has %d trailing bytes", len(body)-offset)
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
func buildCurrentPVPUserStateBody(objectKey uint16) ([]byte, error) {
|
|
return buildCurrentPVPUserStateBodyWithState(objectKey, currentPVPUserState)
|
|
}
|
|
|
|
func buildCurrentPVPUserStateBodyWithState(objectKey uint16, state byte) ([]byte, error) {
|
|
if objectKey == 0 {
|
|
return nil, fmt.Errorf("pvp user state requires a nonzero object key")
|
|
}
|
|
return []byte{1, byte(objectKey), byte(objectKey >> 8), state}, nil
|
|
}
|
|
|
|
func buildCurrentPVPUserAreaBody(objectKey, x, y uint16, townID, direction, areaState byte) []byte {
|
|
return buildCurrentTownUserAreaNotificationBody(
|
|
objectKey,
|
|
townID,
|
|
0xfe,
|
|
x,
|
|
y,
|
|
direction,
|
|
areaState,
|
|
)
|
|
}
|
|
|
|
func buildCurrentPVPRoomInfoBody(room *currentPVPRoom) []byte {
|
|
request := room.request
|
|
var writer packetWriter
|
|
writer.writeUint16(1)
|
|
writer.writeUint16(room.id)
|
|
writer.writeByte(request.roomNameType)
|
|
if request.roomNameType == 0 {
|
|
writer.writeInt32(len(request.roomName))
|
|
writer.writeBytes(request.roomName)
|
|
}
|
|
writer.writeByte(room.state())
|
|
writer.writeByte(room.manager)
|
|
writer.writeUint16(uint16(request.mapIndex))
|
|
writer.writeByte(room.battleMode)
|
|
// Current EXE sub_1D67AF0 reads only u8 state + u16 actor per seat.
|
|
for seat := 0; seat < currentPVPRoomSeatCount; seat++ {
|
|
if member := room.members[seat]; member != nil {
|
|
writer.writeByte(room.seatState[seat])
|
|
writer.writeUint16(member.characterID)
|
|
} else {
|
|
writer.writeByte(0xff)
|
|
writer.writeUint16(0xffff)
|
|
}
|
|
}
|
|
if request.hasPassword {
|
|
writer.writeByte(1)
|
|
} else {
|
|
writer.writeByte(0)
|
|
}
|
|
writer.writeInt32(0)
|
|
return writer.bytes()
|
|
}
|
|
|
|
func currentPVPBattleModeForMake(request currentPVPMakeRoomRequest) byte {
|
|
if request.specialBattleMode == 1 {
|
|
return 6
|
|
}
|
|
if request.specialBattleMode == 3 {
|
|
// Native sub_10AFA10 mode10 starts the PVF arcade round list.
|
|
return 10
|
|
}
|
|
return currentPVPDefaultBattleMode
|
|
}
|
|
|
|
func normalizeCurrentPVPBattleMode(mode byte) byte {
|
|
if mode != 6 && !isCurrentPVPManualBattleMode(mode) {
|
|
return currentPVPDefaultBattleMode
|
|
}
|
|
return mode
|
|
}
|
|
|
|
func isCurrentPVPManualBattleMode(mode byte) bool {
|
|
// Current EXE 0x155932B sends 8 for Btn_TotalMatch; 6/10 are practice.
|
|
return (mode >= 1 && mode <= 5) || mode == 8
|
|
}
|
|
|
|
func buildCurrentPVPAllSeatStatesBody(room *currentPVPRoom) []byte {
|
|
// 0x1D5A110 consumes every seat before applying the mode. Publish them
|
|
// together because 0x2738430 infers teams from the first occupied seat.
|
|
body := []byte{byte(room.id), byte(room.id >> 8), room.battleMode, 0}
|
|
for seat, member := range room.members {
|
|
if member != nil {
|
|
body[3]++
|
|
body = append(body, byte(seat), room.seatState[seat], byte(member.characterID), byte(member.characterID>>8))
|
|
}
|
|
}
|
|
return body
|
|
}
|
|
|
|
func buildCurrentPVPSeatStateBody(roomID uint16, battleMode, seat, state byte, userID uint16) []byte {
|
|
var writer packetWriter
|
|
writer.writeUint16(roomID)
|
|
writer.writeByte(battleMode)
|
|
writer.writeByte(1)
|
|
writer.writeByte(seat)
|
|
writer.writeByte(state)
|
|
writer.writeUint16(userID)
|
|
return writer.bytes()
|
|
}
|
|
|
|
func buildCurrentPVPDestroyedRoomStateBody(roomID uint16) []byte {
|
|
return buildCurrentPVPRoomStateBody(roomID, 0, 0, 0, currentPVPDefaultBattleMode, 0)
|
|
}
|
|
|
|
func buildCurrentPVPRoomStateBody(roomID uint16, roomState, manager byte, mapIndex int16, battleMode byte, matchingType int32) []byte {
|
|
var writer packetWriter
|
|
writer.writeUint16(roomID)
|
|
writer.writeByte(roomState)
|
|
writer.writeByte(manager)
|
|
writer.writeUint16(uint16(mapIndex))
|
|
writer.writeByte(battleMode)
|
|
writer.writeInt32(int(matchingType))
|
|
return writer.bytes()
|
|
}
|