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