服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
276 lines
9.8 KiB
Go
276 lines
9.8 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"encoding/binary"
|
||
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
)
|
||
|
||
func buildRosterSlots(characters []dnfrepo.CharacterRecord) []byte {
|
||
// 当前客户端默认只开�?8 个角色槽,额外槽位必须由道具/扩展栏位请求链路触发�
|
||
var writer packetWriter
|
||
writer.writeByte(2)
|
||
writer.writeByte(latestRosterRouteNormal)
|
||
writer.writeByte(latestRosterContextNormal)
|
||
writer.writeUint16(defaultCharacterSlots)
|
||
writer.writeUint16(0)
|
||
writer.writeUint16(0)
|
||
writer.writeUint32(0)
|
||
writer.writeUint16(0)
|
||
writer.writeByte(0)
|
||
writer.writeByte(0)
|
||
writer.writeUint32(0)
|
||
writer.writeUint32(0)
|
||
return writer.bytes()
|
||
}
|
||
|
||
func buildRosterChars(characters []dnfrepo.CharacterRecord) []byte {
|
||
var writer packetWriter
|
||
count := rosterCount(characters)
|
||
writer.writeByte(3)
|
||
writer.writeUint16(uint16(count))
|
||
for idx, character := range characters {
|
||
if idx >= count {
|
||
break
|
||
}
|
||
writer.writeByte(latestRosterRouteNormal)
|
||
writer.writeByte(latestRosterContextNormal)
|
||
writeRosterChar(&writer, character)
|
||
}
|
||
return writer.bytes()
|
||
}
|
||
|
||
func rosterCount(characters []dnfrepo.CharacterRecord) int {
|
||
return rosterCountForLimit(characters, defaultCharacterSlots)
|
||
}
|
||
|
||
func rosterCountForLimit(characters []dnfrepo.CharacterRecord, slotLimit int) int {
|
||
slotLimit = normalizedCharacterSlotLimit(slotLimit)
|
||
count := len(characters)
|
||
if count > slotLimit {
|
||
return slotLimit
|
||
}
|
||
if count > 0xffff {
|
||
return 0xffff
|
||
}
|
||
return count
|
||
}
|
||
|
||
func writeRosterChar(writer *packetWriter, character dnfrepo.CharacterRecord) {
|
||
charID := uint32(numericCharacterID(character))
|
||
job := byte(numericCharacterStat(character.Job))
|
||
growType := currentClientProjectedGrow(character)
|
||
level := byte(character.Level)
|
||
if level == 0 {
|
||
level = 1
|
||
}
|
||
slot := uint16(character.Slot)
|
||
|
||
writer.writeZero(8)
|
||
writer.writeUint16(uint16(charID))
|
||
writer.writeByte(latestCharacterStateActive)
|
||
writer.writeByte(0)
|
||
|
||
writer.writeUint32(charID)
|
||
writer.writeUint32(0)
|
||
writer.writeUint32(0)
|
||
writer.writeByte(job)
|
||
writer.writeByte(growType)
|
||
writer.writeByte(level)
|
||
writer.writeByte(0)
|
||
writer.writeByte(0)
|
||
|
||
writer.writeUint32(0)
|
||
writer.writeUint16(slot)
|
||
writer.writeUint32(charID)
|
||
writeFixedUTF16(writer, character.Name, 32)
|
||
writer.writeByte(job)
|
||
writer.writeByte(0)
|
||
|
||
writer.writeUint32(0)
|
||
writer.writeByte(0)
|
||
}
|
||
|
||
func buildRosterBodies(characters []dnfrepo.CharacterRecord) [][]byte {
|
||
return [][]byte{
|
||
buildRosterSlots(characters),
|
||
buildRosterChars(characters),
|
||
}
|
||
}
|
||
|
||
func buildCSharpRosterBody(characters []dnfrepo.CharacterRecord) []byte {
|
||
return buildCSharpRosterBodyForLimit(characters, defaultCharacterSlots)
|
||
}
|
||
|
||
func buildCSharpRosterBodyForLimit(characters []dnfrepo.CharacterRecord, slotLimit int) []byte {
|
||
return buildNoPackRosterBodyForLimit(characters, slotLimit)
|
||
}
|
||
|
||
func buildNoPackRosterBody(characters []dnfrepo.CharacterRecord) []byte {
|
||
return buildNoPackRosterBodyForLimit(characters, defaultCharacterSlots)
|
||
}
|
||
|
||
func buildNoPackRosterBodyForLimit(characters []dnfrepo.CharacterRecord, slotLimit int) []byte {
|
||
var writer packetWriter
|
||
slotLimit = effectiveCharacterSlotLimit(characters, slotLimit)
|
||
totalCount := rosterCountForLimit(characters, slotLimit)
|
||
rows := noPackRosterRowsForBatch(characters, totalCount, 0, slotLimit)
|
||
header := defaultNoPackRosterHeaderForLimit(totalCount, slotLimit)
|
||
// Current EXE sub_200BEA0 stores the tail byte as a one-based roster
|
||
// page number. It subtracts one and records SelectedOrPage against that
|
||
// page. Current EXE sub_33D3470/sub_33D5410 prove that its transport page
|
||
// boundary is 36, while the account's default unlocked capacity remains 32.
|
||
header.PageCount = noPackRosterLastOccupiedPage(characters, totalCount)
|
||
|
||
// sub_200BEA0 mode=2 consumes this exact 15-byte prefix before it begins
|
||
// calling sub_200B250 for role rows. Use fixed offsets rather than a
|
||
// sequential writer: byte 13..14 is the only row-loop count, and a zero
|
||
// there produces a healthy-but-empty (0/17) character selection screen.
|
||
prefix := make([]byte, 15)
|
||
prefix[0] = 2
|
||
prefix[1] = byte(totalCount)
|
||
prefix[2] = 5
|
||
binary.LittleEndian.PutUint16(prefix[3:5], clampRosterUint16(header.TotalOrSlotLimit))
|
||
binary.LittleEndian.PutUint16(prefix[5:7], clampRosterUint16(header.UsedOrRemain))
|
||
binary.LittleEndian.PutUint16(prefix[7:9], rosterUint16Value(header.SelectedOrPage, 0))
|
||
binary.LittleEndian.PutUint32(prefix[9:13], rosterUint32Value(header.RosterState, 0))
|
||
binary.LittleEndian.PutUint16(prefix[13:15], uint16(len(rows)))
|
||
writer.writeBytes(prefix)
|
||
for _, character := range rows {
|
||
// Extension batches carry the account-wide roster slot, not the
|
||
// row's zero-based position inside this 36-slot page. Re-numbering
|
||
// slot 36 as zero makes the client overwrite the first-page actor.
|
||
writeNoPackRosterEntry(&writer, character.Slot, character)
|
||
}
|
||
writer.writeByte(rosterByteValue(header.PageCount, rosterDefaultPageCount))
|
||
writer.writeByte(rosterHeaderByte(header.RosterFlag, 0))
|
||
// mode=2 β��״̬ flag �ᴥ���ͻ��˽�ɫ��״̬ˢ�£�Ĭ���� 0������ÿ�δ�ѡ��ҳ��������������Ч��
|
||
writer.writeUint32(rosterUint32Value(header.RosterValueA, 0))
|
||
writer.writeUint32(rosterUint32Value(header.RosterValueB, 0))
|
||
return writer.bytes()
|
||
}
|
||
|
||
func buildNoPackRosterExtensionBodyForLimit(characters []dnfrepo.CharacterRecord, slotLimit int, start int) ([]byte, bool) {
|
||
slotLimit = effectiveCharacterSlotLimit(characters, slotLimit)
|
||
if start < currentRosterExtensionBatchSize ||
|
||
start >= slotLimit ||
|
||
start%currentRosterExtensionBatchSize != 0 {
|
||
return nil, false
|
||
}
|
||
totalCount := rosterCountForLimit(characters, slotLimit)
|
||
rows := noPackRosterRowsForBatch(characters, totalCount, start, slotLimit)
|
||
if len(rows) == 0 {
|
||
return nil, false
|
||
}
|
||
|
||
var writer packetWriter
|
||
writer.writeByte(4)
|
||
writer.writeUint16(uint16(totalCount))
|
||
writer.writeUint16(uint16(len(rows)))
|
||
for _, character := range rows {
|
||
// �� mode-2 һ������ʵ��λ�����������ڵ������ţ�һ����¼�IJ�λ����
|
||
// 0..59 ֮�⣬rosterSlotValue ����䵽����������ѵ� 36 �Ų�λд�� 0��
|
||
// �ͻ��˾ͻ�����չ���ε��и��ǵ��һҳ�Ľ�ɫ��
|
||
writeNoPackRosterEntry(&writer, character.Slot, character)
|
||
}
|
||
return writer.bytes(), true
|
||
}
|
||
|
||
func noPackRosterRowsForBatch(characters []dnfrepo.CharacterRecord, count int, start int, slotLimit int) []dnfrepo.CharacterRecord {
|
||
if start < 0 || start >= slotLimit || count <= 0 {
|
||
return nil
|
||
}
|
||
end := start + currentRosterExtensionBatchSize
|
||
if end > slotLimit {
|
||
end = slotLimit
|
||
}
|
||
rows := make([]dnfrepo.CharacterRecord, 0, count)
|
||
for idx, character := range characters {
|
||
if idx >= count {
|
||
break
|
||
}
|
||
if character.Slot < start || character.Slot >= end {
|
||
continue
|
||
}
|
||
rows = append(rows, character)
|
||
}
|
||
return rows
|
||
}
|
||
|
||
func noPackRosterLastOccupiedPage(characters []dnfrepo.CharacterRecord, count int) int64 {
|
||
page := int64(rosterDefaultPageCount)
|
||
for idx, character := range characters {
|
||
if idx >= count {
|
||
break
|
||
}
|
||
if character.Slot < 0 || character.Slot >= maxCharacterSlots {
|
||
continue
|
||
}
|
||
candidate := int64(character.Slot/currentRosterExtensionBatchSize + 1)
|
||
if candidate > page {
|
||
page = candidate
|
||
}
|
||
}
|
||
return page
|
||
}
|
||
|
||
func noteEmptyRosterSlotProbe(session *gameSession, body []byte) {
|
||
// Current EXE sub_200BEA0(mode=2) finalizes an empty selector with index
|
||
// UINT16_MAX, then immediately sends class1/op679 and clears selector ready.
|
||
// Remember only that exact mode-2 empty snapshot so the request handler can
|
||
// complete the client's no-extension handshake without changing ordinary
|
||
// slot-extension behavior.
|
||
session.emptyRosterSlotProbePending = len(body) >= 15 &&
|
||
body[0] == 2 &&
|
||
body[1] == 0 &&
|
||
binary.LittleEndian.Uint16(body[7:9]) == 0 &&
|
||
binary.LittleEndian.Uint16(body[13:15]) == 0
|
||
}
|
||
|
||
// defaultNoPackRosterHeader ����ɶ˰�ȫ�IJ�λ�����ֶΡ�
|
||
func defaultNoPackRosterHeader(count int) dnfrepo.CharacterRosterHeader {
|
||
return defaultNoPackRosterHeaderForLimit(count, defaultCharacterSlots)
|
||
}
|
||
|
||
func defaultNoPackRosterHeaderForLimit(count int, slotLimit int) dnfrepo.CharacterRosterHeader {
|
||
slotLimit = normalizedCharacterSlotLimit(slotLimit)
|
||
return dnfrepo.CharacterRosterHeader{
|
||
UnkA: latestRosterRouteNormal,
|
||
UnkB: latestRosterContextNormal,
|
||
TotalOrSlotLimit: int64(slotLimit),
|
||
UsedOrRemain: int64(slotLimit),
|
||
SelectedOrPage: int64(count),
|
||
PageCount: rosterDefaultPageCount,
|
||
RosterFlag: 0,
|
||
}
|
||
}
|
||
|
||
func normalizedCharacterSlotLimit(limit int) int {
|
||
if limit < defaultCharacterSlots || limit > maxCharacterSlots {
|
||
return defaultCharacterSlots
|
||
}
|
||
return limit
|
||
}
|
||
|
||
// effectiveCharacterSlotLimit ��������̧���ܸ����������ò�λ��
|
||
//
|
||
// �˺ż�¼ȱʧ��Ԫ�������ȡʧ��ʱ�������䵽���� 32��������Ľ�ɫ��Ȼ����
|
||
// 32..59 �Ų�λ�ϡ����ͶӰ���Ż��䣬��Щ��ɫ��������ѡ��ҳ��ʧ��op679 ��չ����Ҳ��
|
||
// ��Ϊ start >= slotLimit ���ܣ���ҿ����ľ��ǡ���ɫȫû�ˡ�����ֻ�þ������ܲ���
|
||
// �ٽ������þ����Ѿ����ڵĽ�ɫ¶��¶�档
|
||
func effectiveCharacterSlotLimit(characters []dnfrepo.CharacterRecord, limit int) int {
|
||
effective := normalizedCharacterSlotLimit(limit)
|
||
for _, character := range characters {
|
||
if character.Slot >= effective && character.Slot < maxCharacterSlots {
|
||
effective = character.Slot + 1
|
||
}
|
||
}
|
||
if count := len(characters); count > effective {
|
||
effective = count
|
||
}
|
||
if effective > maxCharacterSlots {
|
||
effective = maxCharacterSlots
|
||
}
|
||
return effective
|
||
}
|