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

298 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 本文件负责宠物命令的协议分发。
// 宠物孵化会改动物品栏、宠物列表和城镇显示状态,当前只解析请求体,不直接回成功包。
package pet
import (
"context"
"fmt"
"longheng.io/server/internal/modules/dnf/alignedcmd"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
)
type handler struct{}
// alignedHatchResolver adapts the bridge-owned runtime-PVF boundary without
// making the pet domain depend on bridge services or PVF parser internals.
type alignedHatchResolver struct {
resolve alignedcmd.PetHatchResolver
}
func (resolver alignedHatchResolver) ResolveHatch(eggItemID int64) (PetHatchDefinition, error) {
if resolver.resolve == nil {
return PetHatchDefinition{}, ErrPetCatalogUnavailable
}
resolved, err := resolver.resolve(eggItemID)
if err != nil {
return PetHatchDefinition{}, err
}
return PetHatchDefinition{
EggItemID: resolved.EggItemID,
HatchedItemID: resolved.HatchedItemID,
EggPVFPath: resolved.EggPVFPath,
HatchedPVFPath: resolved.HatchedPVFPath,
MinimumLevel: resolved.MinimumLevel,
}, nil
}
// NewHandler 创建宠物协议处理器。
func NewHandler() alignedcmd.Handler {
return handler{}
}
// Domain 返回宠物业务域。
func (handler) Domain() dnfenum.AlignedDomain {
return dnfenum.AlignedDomainPet
}
// Handle 解析宠物请求,并在宠物 owner 与列表刷新闭合前禁止成功 ACK。
func (handler) Handle(ctx context.Context, req alignedcmd.Request) (alignedcmd.Result, error) {
switch dnfenum.CmdPacket(req.Opcode) {
case dnfenum.CmdPacketRenameCreature:
parsed, err := DecodeRenameCreatureRequest(req.Body)
cmd := NewRenameCommand(req, parsed)
if err != nil {
return blockedParsedResult(cmd.Operation, cmd.String(), err), nil
}
owner, err := NewOwner(req.Repositories)
if err != nil {
return ownerBlockedResult(cmd.Operation, cmd.String(), err), nil
}
result, err := owner.Rename(ctx, RenameCommand{
SelectedCharacterID: req.SelectedCharacterID,
ListType: parsed.ListType,
SlotIndex: parsed.SlotIndex,
NameRaw: parsed.NameRaw,
})
if err != nil {
return ownerBlockedResult(cmd.Operation, cmd.String(), err), nil
}
return ownerRenameAppliedResultOpen(req.Opcode, cmd, result), nil
case dnfenum.CmdPacketHatchCreature:
parsed, err := DecodeHatchCreatureRequest(req.Body)
operation := "hatch_creature"
cmd := NewHatchCommand(req, operation, parsed)
if err != nil {
return blockedParsedResult(cmd.Operation, cmd.String(), err), nil
}
var owner *Owner
if req.PetHatchResolver == nil {
owner, err = NewOwner(req.Repositories)
} else {
owner, err = NewOwner(req.Repositories, alignedHatchResolver{resolve: req.PetHatchResolver})
}
if err != nil {
return ownerBlockedResult(cmd.Operation, cmd.String(), err), nil
}
result, err := owner.Hatch(ctx, HatchCommand{
SelectedCharacterID: req.SelectedCharacterID,
ListType: parsed.ListType,
SlotIndex: parsed.SlotIndex,
})
if err != nil {
return ownerBlockedResult(cmd.Operation, cmd.String(), err), nil
}
return ownerAppliedResultOpen(req.Opcode, cmd.Operation, cmd.String(), result), nil
case dnfenum.CmdPacketHatchCreatureEgg:
return alignedcmd.Result{
Handled: true,
ResponseAllowed: false,
Operation: "hatch_creature_egg_blocked",
Reason: "current EXE proves op173 only as a bodyless S2C refresh; old C2S hatch mapping is blocked",
}, nil
case dnfenum.CmdPacketRequestHatchedCreature:
return alignedcmd.Result{
Handled: true,
ResponseAllowed: false,
Operation: "request_hatched_creature_blocked",
Reason: "current NoPack has no proved C2S op174 pet request; scene class0/op174 is a generic runtime-object refresh, so the historical binding is disabled",
}, nil
default:
return alignedcmd.Result{
Handled: false,
ResponseAllowed: false,
Operation: "unregistered",
Reason: fmt.Sprintf("pet 模块未登记 opcode %d", req.Opcode),
}, nil
}
}
func ownerRenameAppliedResultOpen(opcode uint16, cmd Command, result RenameResult) alignedcmd.Result {
if result.ListType != listTypePet ||
(result.SourceListType != currentMainInventoryListType && result.SourceListType != listTypePet) ||
result.SlotIndex < 0 ||
(result.SlotIndex > 139 && (result.SlotIndex < 189 || result.SlotIndex > 238)) {
return ownerBlockedResult(cmd.Operation, cmd.String(), ErrPetStateInvalid)
}
itemSlotRefreshes := []alignedcmd.ItemSlotRefresh{{
ListType: result.SourceListType,
SlotIndex: result.SlotIndex,
}}
postActions := []alignedcmd.PostAction(nil)
// A list-7 op14 row immediately after op100 is unstable on the legacy
// client. The durable card decrement is committed first; a full list-7
// op13 projection then makes the new count visible without replaying the
// crashing incremental row.
if result.SourceListType == listTypePet {
itemSlotRefreshes = nil
postActions = append(postActions, alignedcmd.PostActionRefreshSelectedPetInventory)
}
return alignedcmd.Result{
Handled: true,
ResponseAllowed: true,
Operation: cmd.Operation,
Reason: fmt.Sprintf(
"pet owner applied: %s %s char=%s key=%s serial=%d item=%d changed=%t card_remaining=%d; emitted current EXE class1/op100 semantic-creature ACK, native class0/op101 in-place name notification and main-inventory source-card row refresh",
cmd.Operation,
cmd.String(),
result.CharacterID,
result.PetKey,
result.CreatureSerial,
result.ItemID,
result.Changed,
result.RemainingCount,
),
UpperResponses: []alignedcmd.UpperResponse{
class1Response(opcode, buildRenameCreatureSuccessAck(result.SlotIndex, result.ListType)),
class0Response(101, buildRenameCreatureNameNotificationBody(cmd.SelectedCharacterID, result.NameRaw)),
},
ItemSlotRefreshes: itemSlotRefreshes,
PostActions: postActions,
}
}
func blockedParsedResult(operation string, summary string, err error) alignedcmd.Result {
if err != nil {
return alignedcmd.Result{
Handled: true,
ResponseAllowed: false,
Operation: operation,
Reason: fmt.Sprintf("%s 请求体解析失败:%v;禁止回成功包", operation, err),
}
}
return alignedcmd.Result{
Handled: true,
ResponseAllowed: false,
Operation: operation,
Reason: fmt.Sprintf("已解析 %s:%s;宠物物品、宠物列表和城镇显示刷新闭合前禁止回包", operation, summary),
}
}
func ownerBlockedResult(operation string, summary string, err error) alignedcmd.Result {
return alignedcmd.Result{
Handled: true,
ResponseAllowed: false,
Operation: operation,
Reason: fmt.Sprintf("已解析 %s:%s;pet owner 写库未完成:%v;禁止回成功包", operation, summary, err),
}
}
func ownerAppliedResult(operation string, summary string, result HatchResult) alignedcmd.Result {
return alignedcmd.Result{
Handled: true,
ResponseAllowed: false,
Operation: operation,
Reason: fmt.Sprintf("已解析 %s:%s;pet owner 已写库 char=%s key=%s item=%d slot=%d changed=%t;列表刷新/城镇显示回包未闭合,禁止回成功包", operation, summary, result.CharacterID, result.PetKey, result.ItemID, result.SourceSlotIndex, result.Changed),
}
}
func ownerAppliedResultOpen(opcode uint16, operation string, summary string, result HatchResult) alignedcmd.Result {
responses := petHatchResponses(opcode, result)
if len(responses) == 0 {
return ownerAppliedResult(operation, summary, result)
}
return alignedcmd.Result{
Handled: true,
ResponseAllowed: true,
Operation: operation,
Reason: fmt.Sprintf(
"pet owner applied: %s %s char=%s key=%s item=%d slot=%d changed=%t; emitted typed ACK, list-7 refresh and creature-list bodies",
operation,
summary,
result.CharacterID,
result.PetKey,
result.ItemID,
result.SourceSlotIndex,
result.Changed,
),
UpperResponses: responses,
}
}
func ownerReadResult(operation string, summary string, result ListResult) alignedcmd.Result {
return alignedcmd.Result{
Handled: true,
ResponseAllowed: false,
Operation: operation,
Reason: fmt.Sprintf("已解析 %s:%s;pet owner 已读取 char=%s count=%d equipped=%q townDisplay=%t;当前 EXE 响应包体未闭合,禁止回成功包", operation, summary, result.CharacterID, result.EntryCount, result.EquippedKey, result.TownDisplay),
}
}
func ownerReadResultOpen(operation string, summary string, result ListResult) alignedcmd.Result {
responses := petReadResponses(operation, result)
return alignedcmd.Result{
Handled: true,
ResponseAllowed: len(responses) > 0,
Operation: operation,
Reason: fmt.Sprintf(
"pet owner read: %s %s char=%s count=%d typed=%d equipped=%q townDisplay=%t",
operation,
summary,
result.CharacterID,
result.EntryCount,
len(result.Entries),
result.EquippedKey,
result.TownDisplay,
),
UpperResponses: responses,
}
}
func petReadResponses(operation string, result ListResult) []alignedcmd.UpperResponse {
if operation != "request_hatched_creature" || len(result.Entries) != result.EntryCount {
return nil
}
creatureListBody, err := buildCreatureListBody(result.Entries)
if err != nil {
return nil
}
return []alignedcmd.UpperResponse{
class1Response(uint16(dnfenum.CmdPacketRequestHatchedCreature), buildSuccessAck()),
class0Response(0x0069, creatureListBody),
}
}
func petHatchResponses(opcode uint16, result HatchResult) []alignedcmd.UpperResponse {
if len(result.Entries) != result.EntryCount || result.EntryCount == 0 {
return nil
}
creatureListBody, err := buildCreatureListBody(result.Entries)
if err != nil {
return nil
}
return []alignedcmd.UpperResponse{
class1Response(opcode, buildSuccessAck()),
class0Response(0x000D, buildPetItemListRefreshBody(result.PetInventory)),
class0Response(0x0069, creatureListBody),
}
}
func class1Response(opcode uint16, body []byte) alignedcmd.UpperResponse {
return alignedcmd.UpperResponse{
MsgID: opcode,
Body: body,
Classification: dnfproto.DefaultChannelClassification,
AllowCodec: true,
}
}
func class0Response(msgID uint16, body []byte) alignedcmd.UpperResponse {
return alignedcmd.UpperResponse{
MsgID: msgID,
Body: body,
Classification: 0,
AllowCodec: true,
}
}