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,275 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user