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,218 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dnfaura "longheng.io/server/internal/modules/dnf/auraskin"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
currentOpenAuraSkinSlotRequestSize = 4
|
||||
currentOpenAuraSkinSlotFlagStat = dnfaura.FlagStat
|
||||
currentAuraSkinSilentRestoreSource = "current_exe_town_ui_lifecycle_marked_restore"
|
||||
)
|
||||
|
||||
var errCurrentOpenAuraSkinSlotInvalidRequest = errors.New("current aura skin slot request invalid")
|
||||
|
||||
type currentOpenAuraSkinSlotRequest struct {
|
||||
SourceSlot int16
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentOpenAuraSkinSlot(session *gameSession, body []byte) error {
|
||||
request, err := decodeCurrentOpenAuraSkinSlotRequest(body)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-current-open-aura-skin-slot-rejected",
|
||||
"body_len", len(body),
|
||||
"reason", err.Error())
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketOpenAuraSkinSlot), 4)
|
||||
}
|
||||
catalog, err := s.currentPVFItemCatalog()
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-current-open-aura-skin-slot-rejected",
|
||||
"source_slot", request.SourceSlot,
|
||||
"reason", "pvf_catalog_unavailable",
|
||||
"error", err.Error())
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketOpenAuraSkinSlot), 4)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
result, err := s.commitCurrentOpenAuraSkinSlot(ctx, session, catalog, request)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-current-open-aura-skin-slot-rejected",
|
||||
"source_slot", request.SourceSlot,
|
||||
"reason", err.Error())
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketOpenAuraSkinSlot), 4)
|
||||
}
|
||||
// Current NoPack registration 0x1D3798F binds op863 to
|
||||
// sub_1D25590. Its success path consumes only the common result marker;
|
||||
// it does not read a source slot or any other business byte. Keep the
|
||||
// status success marker supplied by sendGameUpperSuccess, but do not append
|
||||
// the request's source slot to the response.
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketOpenAuraSkinSlot), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if result.SourceChanged {
|
||||
var sourceUpdate currentItemListEntry
|
||||
if result.SourceRemoved {
|
||||
sourceUpdate.patchCore(result.SourceSlot, math.MaxUint32, 0)
|
||||
} else {
|
||||
sourceUpdate = currentItemListEntryFromStack(
|
||||
dnfrepo.MainInventoryListType,
|
||||
result.SourceSlot,
|
||||
result.RemainingStack,
|
||||
)
|
||||
}
|
||||
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), buildCurrentItemUpdateBody(dnfrepo.MainInventoryListType, []currentItemListEntry{sourceUpdate}), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// The ordinary success ACK intentionally reaches sub_1D25590 once so the
|
||||
// real expansion keeps its native effect. Follow it with the compatibility
|
||||
// marker that mirrors the committed state into sub_26905A0's durable bit;
|
||||
// the DLL consumes this marker without replaying the effect.
|
||||
if err := s.sendCurrentAuraSkinSlotPersistentStateMarker(session, true, "current_exe_op863_commit"); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-current-open-aura-skin-slot-accepted",
|
||||
"source_slot", result.SourceSlot,
|
||||
"source_item_id", result.SourceItemID,
|
||||
"source_pvf_path", result.SourcePVFPath,
|
||||
"consumed", result.Consumed,
|
||||
"already_open", result.AlreadyOpen,
|
||||
"source_update", result.SourceChanged)
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeCurrentOpenAuraSkinSlotRequest(body []byte) (currentOpenAuraSkinSlotRequest, error) {
|
||||
if len(body) != currentOpenAuraSkinSlotRequestSize {
|
||||
return currentOpenAuraSkinSlotRequest{}, fmt.Errorf("%w: body_len=%d", errCurrentOpenAuraSkinSlotInvalidRequest, len(body))
|
||||
}
|
||||
slot := binary.LittleEndian.Uint32(body)
|
||||
if slot > uint32(math.MaxInt16) {
|
||||
return currentOpenAuraSkinSlotRequest{}, fmt.Errorf("%w: source_slot=%d", errCurrentOpenAuraSkinSlotInvalidRequest, slot)
|
||||
}
|
||||
return currentOpenAuraSkinSlotRequest{SourceSlot: int16(slot)}, nil
|
||||
}
|
||||
|
||||
func (s *Service) commitCurrentOpenAuraSkinSlot(ctx context.Context, session *gameSession, catalog *pvfDungeonDropCatalog, request currentOpenAuraSkinSlotRequest) (dnfaura.Result, error) {
|
||||
if s == nil || session == nil || catalog == nil {
|
||||
return dnfaura.Result{}, dnfaura.ErrOwnerUnavailable
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok {
|
||||
return dnfaura.Result{}, dnfaura.ErrOwnerUnavailable
|
||||
}
|
||||
owner, err := dnfaura.NewOwner(repositories, currentAuraSkinCatalog{catalog: catalog})
|
||||
if err != nil {
|
||||
return dnfaura.Result{}, err
|
||||
}
|
||||
return owner.Open(ctx, dnfaura.Command{
|
||||
AccountID: s.accountIDForSession(session),
|
||||
SelectedCharacterID: session.selectedCharacterID,
|
||||
SourceSlot: request.SourceSlot,
|
||||
})
|
||||
}
|
||||
|
||||
// sendCurrentAuraSkinSlotTownUIReadyState projects the durable aura-slot flag
|
||||
// once per town UI lifecycle. Both open and closed state are explicit so a
|
||||
// process that changes characters cannot inherit the previous actor's bit.
|
||||
// The matching DLL consumes the marker without invoking op863's one-shot
|
||||
// unlock-effect handler.
|
||||
func (s *Service) sendCurrentAuraSkinSlotTownUIReadyState(session *gameSession) error {
|
||||
if session == nil || session.selectedCharacterID == 0 {
|
||||
return nil
|
||||
}
|
||||
session.auraSkinMu.Lock()
|
||||
if session.auraSkinTownUIReadyStateSent {
|
||||
session.auraSkinMu.Unlock()
|
||||
s.logGameEvent(session, "game-current-aura-skin-slot-state-skipped",
|
||||
"source", currentAuraSkinSilentRestoreSource,
|
||||
"reason", "already_sent_for_current_town_scene")
|
||||
return nil
|
||||
}
|
||||
session.auraSkinTownUIReadyStateSent = true
|
||||
session.auraSkinMu.Unlock()
|
||||
|
||||
resetGate := func() {
|
||||
session.auraSkinMu.Lock()
|
||||
session.auraSkinTownUIReadyStateSent = false
|
||||
session.auraSkinMu.Unlock()
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Character == nil {
|
||||
resetGate()
|
||||
return dnfrepo.ErrRepoMissing
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
characterID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
character, found, err := repositories.Character.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
resetGate()
|
||||
return err
|
||||
}
|
||||
if !found || strings.TrimSpace(character.CharacterID) != characterID {
|
||||
resetGate()
|
||||
return fmt.Errorf("aura skin state character %s not found", characterID)
|
||||
}
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
if accountID != "" && strings.TrimSpace(character.AccountID) != accountID {
|
||||
resetGate()
|
||||
return fmt.Errorf("aura skin state character %s account mismatch", characterID)
|
||||
}
|
||||
opened := character.Stats != nil && character.Stats[currentOpenAuraSkinSlotFlagStat] != 0
|
||||
if err := s.sendCurrentAuraSkinSlotPersistentStateMarker(session, opened, currentAuraSkinSilentRestoreSource); err != nil {
|
||||
resetGate()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentAuraSkinSlotPersistentStateMarker(session *gameSession, opened bool, source string) error {
|
||||
state := byte(0)
|
||||
if opened {
|
||||
state = 1
|
||||
}
|
||||
body := []byte{state, 'A', 'U', 'R', 'A'}
|
||||
s.logGameEvent(session, "game-current-aura-skin-slot-state-send",
|
||||
"source", source,
|
||||
"char_id", session.selectedCharacterID,
|
||||
"msg_id", uint16(dnfenum.CmdPacketOpenAuraSkinSlot),
|
||||
"classification", dnfproto.DefaultChannelClassification,
|
||||
"body_len", len(body),
|
||||
"opened", opened,
|
||||
"restore_mode", "marked_persistent_native_state_without_unlock_effect")
|
||||
return s.sendGameUpperRawClass(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketOpenAuraSkinSlot),
|
||||
body,
|
||||
dnfproto.DefaultChannelClassification,
|
||||
)
|
||||
}
|
||||
|
||||
type currentAuraSkinCatalog struct {
|
||||
catalog *pvfDungeonDropCatalog
|
||||
}
|
||||
|
||||
func (c currentAuraSkinCatalog) ResolveItem(itemID uint32) (dnfaura.ItemDefinition, error) {
|
||||
if c.catalog == nil {
|
||||
return dnfaura.ItemDefinition{}, dnfaura.ErrOwnerUnavailable
|
||||
}
|
||||
definition, err := c.catalog.ResolveItem(itemID)
|
||||
if err != nil {
|
||||
return dnfaura.ItemDefinition{}, err
|
||||
}
|
||||
return dnfaura.ItemDefinition{
|
||||
Kind: string(definition.Kind),
|
||||
PVFPath: definition.PVFPath,
|
||||
}, nil
|
||||
}
|
||||
Reference in new issue
Block a user