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,314 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
dnfdungeonpermission "longheng.io/server/internal/modules/dnf/dungeonpermission"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
const (
|
||||
currentDungeonResourceStateMsgID = 5
|
||||
// The accepted select-dungeon request owns state 1 for its selected PVF dungeon.
|
||||
currentDungeonResourceSelectedState = 1
|
||||
)
|
||||
|
||||
type currentDungeonResourceStateEntry struct {
|
||||
DungeonID uint32
|
||||
State byte
|
||||
}
|
||||
|
||||
func buildCurrentDungeonResourceStateEntriesBody(entries []currentDungeonResourceStateEntry) []byte {
|
||||
var writer packetWriter
|
||||
if len(entries) > int(^uint16(0)) {
|
||||
entries = entries[:int(^uint16(0))]
|
||||
}
|
||||
writer.writeUint16(uint16(len(entries)))
|
||||
for _, entry := range entries {
|
||||
writer.writeUint32(entry.DungeonID)
|
||||
writer.writeByte(entry.State)
|
||||
}
|
||||
return writer.bytes()
|
||||
}
|
||||
|
||||
func buildCurrentDungeonResourceStateBody(dungeonID uint32) []byte {
|
||||
return buildCurrentDungeonResourceStateEntriesBody([]currentDungeonResourceStateEntry{{
|
||||
DungeonID: dungeonID,
|
||||
State: currentDungeonResourceSelectedState,
|
||||
}})
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentDungeonResourceState(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
source string,
|
||||
) error {
|
||||
if session == nil || runtime == nil || runtime.Dungeon.ID <= 0 {
|
||||
return nil
|
||||
}
|
||||
clientDungeonID := currentDungeonClientID(runtime)
|
||||
if clientDungeonID <= 0 || clientDungeonID > int64(^uint32(0)) {
|
||||
return nil
|
||||
}
|
||||
body := buildCurrentDungeonResourceStateBody(uint32(clientDungeonID))
|
||||
s.logGameEvent(session, "game-upper-current-dungeon-resource-state-op5-send",
|
||||
"source", source,
|
||||
"char_id", session.selectedCharacterID,
|
||||
"msg_id", currentDungeonResourceStateMsgID,
|
||||
"classification", 0,
|
||||
"body_len", len(body),
|
||||
"row_count", 1,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"client_dungeon_id", clientDungeonID,
|
||||
"state", currentDungeonResourceSelectedState,
|
||||
"state_source", "validated_current_select_dungeon_request_and_real_pvf_runtime",
|
||||
"body_source", "current_exe_sub_1D37AC0_u16_count_u32_key_u8_state")
|
||||
return s.sendGameUpperRawClass(session, currentDungeonResourceStateMsgID, body, 0)
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentDungeonPermissionSnapshot(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
source string,
|
||||
) error {
|
||||
entries, err := s.currentDungeonPermissionSnapshotEntries(ctx, session)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-current-dungeon-permission-op5-snapshot-blocked",
|
||||
"source", source,
|
||||
"char_id", sessionSelectedCharacterID(session),
|
||||
"reason", "repository_or_worldmap_unavailable",
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
s.logGameEvent(session, "game-upper-current-dungeon-permission-op5-snapshot-empty",
|
||||
"source", source,
|
||||
"char_id", sessionSelectedCharacterID(session),
|
||||
"reason", "no_current_pvf_ordinary_difficulty_definition")
|
||||
return nil
|
||||
}
|
||||
body := buildCurrentDungeonResourceStateEntriesBody(entries)
|
||||
s.logGameEvent(session, "game-upper-current-dungeon-permission-op5-snapshot-send",
|
||||
"source", source,
|
||||
"char_id", sessionSelectedCharacterID(session),
|
||||
"msg_id", currentDungeonResourceStateMsgID,
|
||||
"classification", 0,
|
||||
"body_len", len(body),
|
||||
"row_count", len(entries),
|
||||
"body_source", "current_exe_sub_1D37AC0_u16_count_u32_key_u8_state",
|
||||
"state_source", "current_pvf_complete_ordinary_difficulty_authority_with_persisted_max_overrides")
|
||||
return s.sendGameUpperRawClass(session, currentDungeonResourceStateMsgID, body, 0)
|
||||
}
|
||||
|
||||
func (s *Service) currentDungeonPermissionSnapshotEntries(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
) ([]currentDungeonResourceStateEntry, error) {
|
||||
if session == nil || session.selectedCharacterID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Character == nil || repositories.DungeonPermission == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
snapshot, err := s.currentAccountDungeonPermissionSnapshot(ctx, session, repositories)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
table, _, err := s.dungeonWorldMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := currentDungeonResourceStateEntriesFromPermissionRecord(table, snapshot.Record)
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (s *Service) currentAccountDungeonPermissionSnapshot(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
repositories dnfrepo.Group,
|
||||
) (dnfdungeonpermission.Snapshot, error) {
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
owner, err := dnfdungeonpermission.NewOwner(repositories)
|
||||
if err != nil {
|
||||
return dnfdungeonpermission.Snapshot{}, err
|
||||
}
|
||||
snapshot, err := owner.LoadAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
return dnfdungeonpermission.Snapshot{}, err
|
||||
}
|
||||
if snapshot.MigratedEntries != 0 {
|
||||
s.logGameEvent(session, "game-dungeon-permission-account-migrated",
|
||||
"account_id", snapshot.AccountID,
|
||||
"char_id", sessionSelectedCharacterID(session),
|
||||
"legacy_owner_count", snapshot.LegacyOwners,
|
||||
"migrated_entry_count", snapshot.MigratedEntries,
|
||||
"state_source", "account_max_over_all_active_legacy_character_owners")
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func currentDungeonResourceStateEntriesFromPermissionRecord(
|
||||
table *worldmap.Table,
|
||||
record dnfrepo.DungeonPermissionRecord,
|
||||
) []currentDungeonResourceStateEntry {
|
||||
if table == nil {
|
||||
return nil
|
||||
}
|
||||
// Current EXE class0/op5 is the complete ordinary-dungeon difficulty
|
||||
// authority table, not a sparse list of dungeons that have already been
|
||||
// cleared. A zero state is meaningful: it exposes the dungeon at normal
|
||||
// difficulty. Omitting that row makes the client depend on a live quest
|
||||
// marker to discover the dungeon and hides the same unlocked map as soon as
|
||||
// the task is absent or completed.
|
||||
stateByDungeon := make(map[uint32]byte, len(record.Entries))
|
||||
for _, entry := range record.Entries {
|
||||
if entry.DungeonID == 0 {
|
||||
continue
|
||||
}
|
||||
if current, found := stateByDungeon[entry.DungeonID]; !found || entry.ClearState > current {
|
||||
stateByDungeon[entry.DungeonID] = entry.ClearState
|
||||
}
|
||||
}
|
||||
dungeons := table.Dungeons()
|
||||
out := make([]currentDungeonResourceStateEntry, 0, len(dungeons))
|
||||
for _, dungeon := range dungeons {
|
||||
if dungeon.ID <= 0 || dungeon.ID > int64(^uint32(0)) || len(dungeon.Metadata.DifficultyLevels) == 0 {
|
||||
continue
|
||||
}
|
||||
state := stateByDungeon[uint32(dungeon.ID)]
|
||||
if maxState := currentDungeonMaxClearState(dungeon); maxState == 0 {
|
||||
continue
|
||||
} else if state > maxState {
|
||||
state = maxState
|
||||
}
|
||||
out = append(out, currentDungeonResourceStateEntry{
|
||||
DungeonID: uint32(dungeon.ID),
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
sort.SliceStable(out, func(left, right int) bool {
|
||||
return out[left].DungeonID < out[right].DungeonID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func currentDungeonMaxClearState(dungeon worldmap.Dungeon) byte {
|
||||
if len(dungeon.Metadata.DifficultyLevels) != 0 {
|
||||
count := 0
|
||||
for _, level := range dungeon.Metadata.DifficultyLevels {
|
||||
if level != 0 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 1 {
|
||||
if count-1 > int(^uint8(0)) {
|
||||
return ^uint8(0)
|
||||
}
|
||||
return byte(count - 1)
|
||||
}
|
||||
// A dungeon with one declared level is a fixed-difficulty dungeon. The
|
||||
// current client still expects state 1 after its first clear so that
|
||||
// world-map prerequisites such as ordinary Luke/Anton advance to 1/1.
|
||||
if count == 1 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if len(dungeon.Metadata.DesignatedDifficulties) != 0 {
|
||||
return 4
|
||||
}
|
||||
if dungeon.Metadata.Difficulty.Set && dungeon.Metadata.Difficulty.Value >= 0 {
|
||||
return 4
|
||||
}
|
||||
// Real fixed-difficulty PVF dungeons commonly omit every difficulty tag.
|
||||
// Treating that shape as unsupported drops the settlement clear entirely;
|
||||
// state 1 is the client's first-clear value and preserves the prerequisite.
|
||||
return 1
|
||||
}
|
||||
|
||||
func currentDungeonNextClearState(dungeon worldmap.Dungeon, difficulty byte) byte {
|
||||
maxState := currentDungeonMaxClearState(dungeon)
|
||||
if maxState == 0 {
|
||||
return 0
|
||||
}
|
||||
next := difficulty + 1
|
||||
if next == 0 {
|
||||
next = 1
|
||||
}
|
||||
if next > maxState {
|
||||
next = maxState
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func (s *Service) commitCurrentDungeonPermission(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) ([]byte, byte, bool, error) {
|
||||
if session == nil || runtime == nil || session.selectedCharacterID == 0 || runtime.Dungeon.ID <= 0 {
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
state := currentDungeonNextClearState(runtime.Dungeon, runtime.Request.Difficulty)
|
||||
if state == 0 || runtime.Dungeon.ID > int64(^uint32(0)) {
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Character == nil || repositories.DungeonPermission == nil {
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
owner, err := dnfdungeonpermission.NewOwner(repositories)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
snapshot, entry, updated, err := owner.Advance(
|
||||
ctx,
|
||||
accountID,
|
||||
uint32(runtime.Dungeon.ID),
|
||||
state,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
if !updated {
|
||||
s.logGameEvent(session, "game-dungeon-permission-clear-state-unchanged",
|
||||
"account_id", snapshot.AccountID,
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"difficulty", runtime.Request.Difficulty,
|
||||
"state", entry.ClearState,
|
||||
"state_source", "persisted_clear_state_already_at_or_above_current_clear")
|
||||
return nil, entry.ClearState, false, nil
|
||||
}
|
||||
body := buildCurrentDungeonResourceStateEntriesBody([]currentDungeonResourceStateEntry{{
|
||||
DungeonID: entry.DungeonID,
|
||||
State: entry.ClearState,
|
||||
}})
|
||||
s.logGameEvent(session, "game-dungeon-permission-clear-state-updated",
|
||||
"account_id", snapshot.AccountID,
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"difficulty", runtime.Request.Difficulty,
|
||||
"state", entry.ClearState,
|
||||
"body_len", len(body),
|
||||
"body_source", "current_exe_sub_1D37AC0_u16_count_u32_key_u8_state",
|
||||
"state_source", "account_shared_settlement_clear_difficulty_plus_one_capped_by_runtime_pvf_or_fixed_difficulty_default")
|
||||
return body, entry.ClearState, true, nil
|
||||
}
|
||||
|
||||
func sessionSelectedCharacterID(session *gameSession) uint16 {
|
||||
if session == nil {
|
||||
return 0
|
||||
}
|
||||
return session.selectedCharacterID
|
||||
}
|
||||
Reference in new issue
Block a user