DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,122 @@
package inventory
import (
"encoding/binary"
"fmt"
"longheng.io/server/internal/modules/dnf/alignedcmd"
dnfguardianshield "longheng.io/server/internal/modules/dnf/guardianshield"
)
const currentGuardianShieldDeckNotificationMsgID uint16 = 580
func decodeCurrentGuardianShieldDeckRequest(body []byte) (dnfguardianshield.Deck, error) {
var deck dnfguardianshield.Deck
if len(body) != dnfguardianshield.DeckSlotCount*4 {
return deck, fmt.Errorf("current op657 guardian shield deck body length=%d want=%d", len(body), dnfguardianshield.DeckSlotCount*4)
}
for index := range deck {
deck[index] = binary.LittleEndian.Uint32(body[index*4 : index*4+4])
}
return deck, nil
}
func buildCurrentGuardianShieldDeckBody(deck dnfguardianshield.Deck) []byte {
body := make([]byte, dnfguardianshield.DeckSlotCount*4)
for index, itemID := range deck {
binary.LittleEndian.PutUint32(body[index*4:index*4+4], itemID)
}
return body
}
func buildCurrentGuardianShieldDeckSuccessBody(deck dnfguardianshield.Deck) []byte {
body := make([]byte, 2+dnfguardianshield.DeckSlotCount*4)
body[0] = 1
copy(body[2:], buildCurrentGuardianShieldDeckBody(deck))
return body
}
func ownerAppliedGuardianShieldMoveResult(opcode uint16, cmd Command, result dnfguardianshield.Result) alignedcmd.Result {
postActions := []alignedcmd.PostAction(nil)
if result.MainChanged {
// The Guardian's visible main shield is persisted in actor equipment
// slot 24. That slot is outside the ordinary fourteen-row mode0
// appearance table, so the town actor must be reconciled through the
// current client's native complete op342 equipment group followed by its
// op357 same-state redraw. Backup-only deck edits use op657 and do not
// touch the visible actor.
postActions = append(postActions, alignedcmd.PostActionRefreshSelectedActorExpandedEquipment)
// While the main deck slot stays occupied, additionally republish the
// selected actor's mode0 scene-object appearance rows exactly like an
// ordinary equipment change. The mode0 appearance summary carries the
// actor-slot-24 deck projection as an extra row, so the client redraws
// the shield immediately without a scene reselect. Equipping a shield
// while the deck[0] slot was empty still reaches this branch (empty ->
// item); only an unequip (deck[0] emptied) skips it and converges on
// the next scene bootstrap, mirroring the 90server deck-move sequence
// (op19 ACK -> op580 deck -> op342 expanded equipment -> mode0
// appearance + op357 user-state redraw).
if result.Deck[dnfguardianshield.MainDeckSlot] > 0 {
postActions = append(postActions, alignedcmd.PostActionRefreshSelectedActorAppearance)
}
}
return alignedcmd.Result{
Handled: true,
ResponseAllowed: true,
Operation: cmd.Operation,
Reason: fmt.Sprintf(
"guardian shield owner applied current op19 endpoints source=(%d,%d) destination=(%d,%d) changed=%t main_changed=%t; emitted native ACK then authoritative class0/op580 five-slot deck and, when the visible main changed, native op342/op357 town appearance refresh",
cmd.SourceListType,
cmd.SourceSlotIndex,
cmd.DestinationListType,
cmd.DestinationSlotIndex,
result.Changed,
result.MainChanged,
),
UpperResponses: []alignedcmd.UpperResponse{
class1Response(opcode, buildMoveItemspaceSuccessAck(cmd, cmd.MoveCount)),
class0Response(currentGuardianShieldDeckNotificationMsgID, buildCurrentGuardianShieldDeckBody(result.Deck)),
},
PostActions: postActions,
}
}
func ownerRejectedGuardianShieldMoveResult(opcode uint16, cmd Command, err error) alignedcmd.Result {
return alignedcmd.Result{
Handled: true,
ResponseAllowed: true,
Operation: cmd.Operation,
Reason: fmt.Sprintf("guardian shield move rejected without mutation: %s: %v", cmd.String(), err),
UpperResponses: []alignedcmd.UpperResponse{
class1Response(opcode, buildMoveItemspaceFailureAck(cmd, moveItemspaceRejectedErrorCode)),
},
}
}
func appliedGuardianShieldDeckResult(opcode uint16, result dnfguardianshield.Result) alignedcmd.Result {
return alignedcmd.Result{
Handled: true,
ResponseAllowed: true,
Operation: "change_guardian_shield_deck",
Reason: fmt.Sprintf(
"guardian shield owner atomically saved current op657 five-slot deck changed=%t main_changed=%t",
result.Changed,
result.MainChanged,
),
UpperResponses: []alignedcmd.UpperResponse{
class0Response(opcode, buildCurrentGuardianShieldDeckSuccessBody(result.Deck)),
},
}
}
func rejectedGuardianShieldDeckResult(opcode uint16, err error) alignedcmd.Result {
return alignedcmd.Result{
Handled: true,
ResponseAllowed: true,
Operation: "change_guardian_shield_deck",
Reason: fmt.Sprintf("guardian shield op657 rejected without mutation: %v", err),
// Current handler reads the remaining reserved/deck fields only when
// status is nonzero. A one-byte zero is therefore the closed failure body.
UpperResponses: []alignedcmd.UpperResponse{class0Response(opcode, []byte{0})},
}
}