Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

248 lines
9.2 KiB
Go

package dnfbridge
import (
"encoding/binary"
"errors"
"math"
"sort"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfguild "longheng.io/server/internal/modules/dnf/guild"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
func (s *Service) handleCurrentGuildCargo(session *gameSession, body []byte) error {
opcode := uint16(dnfenum.CmdPacketGuildCargo)
if len(body) != 0 {
return s.sendGameUpperFailure(session, opcode, currentGuildGenericError)
}
guild, err := s.currentGuildForSelectedCharacter(session)
if err != nil || guild == nil {
return s.sendGameUpperFailure(session, opcode, currentGuildGenericError)
}
return s.sendGameUpperSuccess(session, opcode, buildCurrentGuildCargoBody(guild))
}
func buildCurrentGuildCargoBody(guild *dnfguild.Guild) []byte {
var writer packetWriter
writer.writeUint32(uint32(guild.CargoLimit))
slots := make([]int, 0, len(guild.Cargo))
for slot := range guild.Cargo {
slots = append(slots, int(slot))
}
sort.Ints(slots)
writer.writeUint16(uint16(len(slots)))
for _, slot := range slots {
entry := currentItemListEntryFromStack(0, int16(slot), guild.Cargo[int16(slot)])
writer.writeBytes(entry.data[:])
}
return writer.bytes()
}
func (s *Service) handleCurrentGuildCargoHistory(session *gameSession, body []byte) error {
opcode := uint16(dnfenum.CmdPacketGuildCargoHistory)
if len(body) != 0 {
return s.sendGameUpperFailure(session, opcode, currentGuildGenericError)
}
guild, err := s.currentGuildForSelectedCharacter(session)
if err != nil || guild == nil {
return s.sendGameUpperFailure(session, opcode, currentGuildGenericError)
}
var writer packetWriter
writer.writeUint32(uint32(len(guild.CargoHistory)))
for _, row := range guild.CargoHistory {
var raw [57]byte
binary.LittleEndian.PutUint32(raw[:], uint32(row.CreatedAt.Unix()))
raw[4] = row.Action
var name packetWriter
writeClientNameDstr(&name, row.Name)
copy(raw[5:34], name.bytes()[4:])
binary.LittleEndian.PutUint32(raw[35:], uint32(row.Item.ItemID))
binary.LittleEndian.PutUint32(raw[39:], uint32(row.Item.Count))
entry := currentItemListEntryFromStack(0, 0, row.Item)
// sub_1D2C7F0 transposes the three magic-seal option columns.
for i, off := range []int{0x48, 0x4b, 0x4e, 0x49, 0x4c, 0x4f, 0x4a, 0x4d, 0x50, 0x51, 0x54, 0x55, 0x56, 0x53} {
raw[43+i] = entry.data[off]
}
if entry.data[0x52] == 0xff {
raw[53], raw[54], raw[55] = 0, 0, 0
}
writer.writeBytes(raw[:])
}
return s.sendGameUpperSuccess(session, opcode, writer.bytes())
}
func (s *Service) handleCurrentGuildCargoPush(session *gameSession, body []byte) error {
if len(body) != 13 || body[0] != 0 {
return s.sendGameUpperFailure(session, 247, 196)
}
return s.transferCurrentGuildCargo(session, true, int16(binary.LittleEndian.Uint16(body[1:])),
int16(binary.LittleEndian.Uint16(body[11:])), binary.LittleEndian.Uint32(body[3:]), binary.LittleEndian.Uint32(body[7:]))
}
func (s *Service) handleCurrentGuildCargoPop(session *gameSession, body []byte) error {
if len(body) != 11 || body[10] != 0 {
return s.sendGameUpperFailure(session, 248, 196)
}
return s.transferCurrentGuildCargo(session, false, -1, int16(binary.LittleEndian.Uint16(body)),
binary.LittleEndian.Uint32(body[2:]), binary.LittleEndian.Uint32(body[6:]))
}
func (s *Service) transferCurrentGuildCargo(session *gameSession, deposit bool, bagSlot, cargoSlot int16, itemID, count uint32) error {
opcode := uint16(248)
if deposit {
opcode = 247
}
ready, readySource := s.currentSceneTransitionReadyForState(session)
if !ready || itemID == 0 || itemID > math.MaxInt32 {
// 196 is the only byte this path can hand the client, and the CN client
// renders it as 「公会仓库尚未完成加载。」(dstr 21684) -- i.e. the player is
// told the warehouse has not loaded. Without this line a scene-state
// rejection is indistinguishable in the log from a malformed request:
// 2026-10-04 19:44 took a log round trip for exactly that reason.
s.logGameEvent(session, "game-guild-cargo-refused",
"opcode", opcode,
"item_id", itemID,
"count", count,
"deposit", deposit,
"bag_slot", bagSlot,
"cargo_slot", cargoSlot,
"scene_ready", ready,
"scene_ready_source", readySource)
return s.sendGameUpperFailure(session, opcode, 196)
}
catalog, err := s.currentPVFItemCatalog()
if err != nil {
return s.sendGameUpperFailure(session, opcode, 196)
}
definition, err := catalog.ResolveItem(itemID)
if err != nil {
return s.sendGameUpperFailure(session, opcode, 196)
}
ctx, cancel := s.currentGuildContext()
defer cancel()
var selected *dnfguild.Guild
var update currentItemListEntry
err = s.transactCurrentGuildAssets(ctx, session, func(tx dnfrepo.Group, registry *dnfguild.Registry, character *dnfrepo.CharacterRecord) error {
var found bool
selected, _, found = registry.GuildForCharacter(character.CharacterID)
if !found {
return dnfguild.ErrNotMember
}
if tx.Inventory == nil {
return dnfguild.ErrRepositoryUnavailable
}
inventory, found, err := tx.Inventory.Load(ctx, character.CharacterID)
if err != nil {
return err
}
if !found {
return dnfguild.ErrRepositoryUnavailable
}
source := selected.Cargo[cargoSlot]
if deposit {
source = inventory.Slots[currentCeraShopInventorySlotKey(0, bagSlot)]
}
if currentDungeonDiscardStackLocked(source) || currentDungeonDiscardStackExpired(source, s.gameplayNow()) {
return dnfguild.ErrPermissionDenied
}
allowed := currentDungeonDiscardTradeable(definition)
if definition.Kind == dungeonDropItemEquipment {
attach := normalizeMagicBoxPVFType(definition.AttachType)
allowed = allowed || (attach == "sealing" && currentItemSealFlag(source) != 0)
if definition.Rarity >= 2 && currentItemSealFlag(source) == 0 {
allowed = false
}
} else {
allowed = allowed && !source.Bind && currentItemSealFlag(source) == 0
}
rule := dnfguild.CargoRule{ItemID: itemID, Equipment: definition.Kind == dungeonDropItemEquipment,
Allowed: allowed, StackLimit: definition.StackLimit, SlotStart: definition.SlotStart, SlotEnd: definition.SlotEnd}
bagSlot, err = selected.TransferCargo(character.CharacterID, &inventory, deposit, bagSlot, cargoSlot, itemID, count, rule, s.gameplayNow())
if err != nil {
return err
}
if remaining, occupied := inventory.Slots[currentCeraShopInventorySlotKey(0, bagSlot)]; occupied {
update = currentItemListEntryFromStack(0, bagSlot, remaining)
} else {
// 原生物品编号 0 是金币,清空格子必须使用 -1。
update.patchCore(bagSlot, math.MaxUint32, 0)
}
return tx.Inventory.Save(ctx, inventory)
})
if err != nil {
s.logGameEvent(session, "game-guild-cargo-rejected", "opcode", opcode, "item_id", itemID, "count", count, "error", err)
return s.sendGameUpperFailure(session, opcode, currentGuildCargoError(err, deposit))
}
var ack packetWriter
if deposit {
ack.writeByte(0)
ack.writeUint16(uint16(bagSlot))
ack.writeUint32(count)
ack.writeUint16(uint16(cargoSlot))
} else {
ack.writeUint16(uint16(cargoSlot))
ack.writeByte(0)
ack.writeUint16(uint16(bagSlot))
ack.writeUint32(count)
}
// Native ACK moves the source object. Authoritative refresh must follow it.
if err := s.sendGameUpperSuccess(session, opcode, ack.bytes()); err != nil {
return err
}
if err := s.sendGameUpperRawClass(session, 14, buildCurrentItemUpdateBody(0, []currentItemListEntry{update}), 0); err != nil {
return err
}
s.logGameEvent(session, "game-guild-cargo-saved", "opcode", opcode, "item_id", itemID, "count", count, "bag_slot", bagSlot, "cargo_slot", cargoSlot)
return s.sendGameUpperSuccess(session, 261, buildCurrentGuildCargoBody(selected))
}
func currentGuildCargoError(err error, deposit bool) byte {
switch {
case errors.Is(err, dnfguild.ErrNotMember):
return 100
case errors.Is(err, dnfguild.ErrPermissionDenied):
return 36
case errors.Is(err, dnfguild.ErrCargoFull):
if !deposit {
return 204
}
return 198
default:
return 196
}
}
func (s *Service) handleCurrentGuildCargoMove(session *gameSession, body []byte) error {
ready, readySource := s.currentSceneTransitionReadyForState(session)
if !ready || len(body) != 12 {
// Same invisible-196 shape as the deposit path: log why before the byte.
s.logGameEvent(session, "game-guild-cargo-refused",
"opcode", 249,
"body_len", len(body),
"scene_ready", ready,
"scene_ready_source", readySource)
return s.sendGameUpperFailure(session, 249, 196)
}
ctx, cancel := s.currentGuildContext()
defer cancel()
var selected *dnfguild.Guild
err := s.transactCurrentGuildAssets(ctx, session, func(_ dnfrepo.Group, registry *dnfguild.Registry, character *dnfrepo.CharacterRecord) error {
var found bool
selected, _, found = registry.GuildForCharacter(character.CharacterID)
if !found {
return dnfguild.ErrNotMember
}
return selected.MoveCargo(character.CharacterID, int16(binary.LittleEndian.Uint16(body)), int16(binary.LittleEndian.Uint16(body[6:])),
binary.LittleEndian.Uint32(body[2:]), binary.LittleEndian.Uint32(body[8:]), s.gameplayNow())
})
if err != nil {
return s.sendGameUpperFailure(session, 249, currentGuildCargoError(err, false))
}
ack := append(append([]byte(nil), body[:2]...), body[6:8]...)
if err := s.sendGameUpperSuccess(session, 249, ack); err != nil {
return err
}
return s.sendGameUpperSuccess(session, 261, buildCurrentGuildCargoBody(selected))
}