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,217 @@
|
||||
package darkknightcombo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfskill "longheng.io/server/internal/modules/dnf/skill"
|
||||
)
|
||||
|
||||
var fixedComboRootPathRE = regexp.MustCompile(`(?i)(^|/)comboset([0-5])\.skl$`)
|
||||
|
||||
const (
|
||||
ordinaryQuickSlotEnd = 6
|
||||
extensionQuickSlotFrom = 198
|
||||
extensionQuickSlotEnd = 204
|
||||
nonShortcutSlotFrom = 12
|
||||
nonShortcutSlotEnd = 198
|
||||
preferredTreeSlotFrom = 150
|
||||
)
|
||||
|
||||
func IsShortcutSlot(slot int) bool {
|
||||
return (slot >= 0 && slot < ordinaryQuickSlotEnd) ||
|
||||
(slot >= extensionQuickSlotFrom && slot < extensionQuickSlotEnd)
|
||||
}
|
||||
|
||||
// IsExclusiveComboSlot reports the six ASDFGH coordinates owned by the
|
||||
// Dark Knight combo manager. Ordinary skills remain allowed in the separate
|
||||
// QWERTY extension coordinates (198..203).
|
||||
func IsExclusiveComboSlot(slot int) bool {
|
||||
return slot >= 0 && slot < ordinaryQuickSlotEnd
|
||||
}
|
||||
|
||||
// NormalizeFixedComboRootSlots installs the learned PVF combo-set roots in the
|
||||
// six ASDFGH coordinates owned by the Dark Knight combat HUD. The current
|
||||
// client does not synthesize these roots from the combo-editor notification:
|
||||
// they must be ordinary op19 skill rows at slots 0..5. Historical ordinary
|
||||
// occupants are preserved in QWERTY (198..203) first and only overflow into a
|
||||
// non-shortcut skill-tree coordinate.
|
||||
func NormalizeFixedComboRootSlots(
|
||||
catalog *dnfskill.Table,
|
||||
states map[int64]dnfrepo.SkillState,
|
||||
existing dnfrepo.SkillLayout,
|
||||
) (dnfrepo.SkillLayout, int, error) {
|
||||
if catalog == nil {
|
||||
return nil, 0, fmt.Errorf("dark knight combo root catalog is unavailable")
|
||||
}
|
||||
layout := make(dnfrepo.SkillLayout, len(existing))
|
||||
for slot, skillID := range existing {
|
||||
layout[slot] = skillID
|
||||
}
|
||||
|
||||
rootsBySlot := make(map[int]uint16, ordinaryQuickSlotEnd)
|
||||
rootSkillIDs := make(map[uint16]struct{}, ordinaryQuickSlotEnd)
|
||||
for rawSkillID, state := range states {
|
||||
if !state.Enabled || state.Level <= 0 || rawSkillID <= 0 || rawSkillID > 0xffff {
|
||||
continue
|
||||
}
|
||||
skillID := uint16(rawSkillID)
|
||||
definition, found := catalog.Find(Job, skillID)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
slot, root := fixedComboRootSlot(definition.Path)
|
||||
if !root {
|
||||
continue
|
||||
}
|
||||
if existingRoot, duplicate := rootsBySlot[slot]; duplicate && existingRoot != skillID {
|
||||
return nil, 0, fmt.Errorf("dark knight combo slot %d has duplicate PVF roots %d and %d", slot, existingRoot, skillID)
|
||||
}
|
||||
rootsBySlot[slot] = skillID
|
||||
rootSkillIDs[skillID] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove every stale/wrong coordinate for a learned combo root before
|
||||
// displacing ordinary ASDFGH occupants. This also heals duplicate roots.
|
||||
for slot, skillID := range layout {
|
||||
if _, root := rootSkillIDs[skillID]; root {
|
||||
delete(layout, slot)
|
||||
}
|
||||
}
|
||||
|
||||
moved := 0
|
||||
for slot := 0; slot < ordinaryQuickSlotEnd; slot++ {
|
||||
skillID, occupied := layout[slot]
|
||||
if !occupied {
|
||||
continue
|
||||
}
|
||||
to := firstFreeSlot(layout, extensionQuickSlotFrom, extensionQuickSlotEnd)
|
||||
if to < 0 {
|
||||
to = firstFreeNonShortcutSlot(layout)
|
||||
}
|
||||
if to < 0 {
|
||||
return nil, 0, fmt.Errorf("cannot move ordinary dark knight skill %d from combo root slot %d", skillID, slot)
|
||||
}
|
||||
delete(layout, slot)
|
||||
layout[to] = skillID
|
||||
moved++
|
||||
}
|
||||
for slot, skillID := range rootsBySlot {
|
||||
layout[slot] = skillID
|
||||
}
|
||||
return layout, moved, nil
|
||||
}
|
||||
|
||||
func fixedComboRootSlot(skillPath string) (int, bool) {
|
||||
normalized := strings.ReplaceAll(strings.TrimSpace(skillPath), `\`, "/")
|
||||
match := fixedComboRootPathRE.FindStringSubmatch(normalized)
|
||||
if len(match) != 3 {
|
||||
return 0, false
|
||||
}
|
||||
slot, err := strconv.Atoi(match[2])
|
||||
return slot, err == nil && slot >= 0 && slot < ordinaryQuickSlotEnd
|
||||
}
|
||||
|
||||
// CleanAutoComboQuickSlots removes native combo candidates from shortcut
|
||||
// coordinates while preserving every learned skill in a non-shortcut tree
|
||||
// coordinate. Root skills are always excluded and saved children take
|
||||
// priority even when their current PVF classification is incomplete.
|
||||
func CleanAutoComboQuickSlots(
|
||||
catalog *dnfskill.Table,
|
||||
states map[int64]dnfrepo.SkillState,
|
||||
existing dnfrepo.SkillLayout,
|
||||
rootSkillIDs map[uint16]struct{},
|
||||
savedChildSkillIDs map[uint16]struct{},
|
||||
) (dnfrepo.SkillLayout, int, error) {
|
||||
return cleanQuickSlots(existing, func(skillID uint16) bool {
|
||||
if _, root := rootSkillIDs[skillID]; root {
|
||||
return false
|
||||
}
|
||||
if _, saved := savedChildSkillIDs[skillID]; saved {
|
||||
return true
|
||||
}
|
||||
state := states[int64(skillID)]
|
||||
if !state.Enabled || state.Level <= 0 || catalog == nil {
|
||||
return false
|
||||
}
|
||||
definition, found := catalog.Find(Job, skillID)
|
||||
if !found || !definition.Active || definition.IsTPSkill() || definition.SkillClass < 0 || definition.SkillClass > 3 {
|
||||
return false
|
||||
}
|
||||
path := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(definition.Path), `\`, "/"))
|
||||
path = strings.TrimPrefix(path, "skill/")
|
||||
return strings.HasPrefix(path, "demonicswordman/") && !strings.Contains(path, "/notapplicable/")
|
||||
})
|
||||
}
|
||||
|
||||
// CleanSavedChildrenQuickSlots performs the second half of an auto-combo
|
||||
// commit. A manual combo save must not call this function.
|
||||
func CleanSavedChildrenQuickSlots(
|
||||
existing dnfrepo.SkillLayout,
|
||||
rootSkillIDs map[uint16]struct{},
|
||||
savedChildSkillIDs map[uint16]struct{},
|
||||
) (dnfrepo.SkillLayout, int, error) {
|
||||
return cleanQuickSlots(existing, func(skillID uint16) bool {
|
||||
if _, root := rootSkillIDs[skillID]; root {
|
||||
return false
|
||||
}
|
||||
_, saved := savedChildSkillIDs[skillID]
|
||||
return saved
|
||||
})
|
||||
}
|
||||
|
||||
func cleanQuickSlots(existing dnfrepo.SkillLayout, shouldMove func(uint16) bool) (dnfrepo.SkillLayout, int, error) {
|
||||
layout := make(dnfrepo.SkillLayout, len(existing))
|
||||
for slot, skillID := range existing {
|
||||
layout[slot] = skillID
|
||||
}
|
||||
shortcutSlots := make([]int, 0, len(layout))
|
||||
for slot, skillID := range layout {
|
||||
if IsShortcutSlot(slot) && shouldMove(skillID) {
|
||||
shortcutSlots = append(shortcutSlots, slot)
|
||||
}
|
||||
}
|
||||
sort.Ints(shortcutSlots)
|
||||
moved := 0
|
||||
for _, from := range shortcutSlots {
|
||||
skillID, occupied := layout[from]
|
||||
if !occupied {
|
||||
continue
|
||||
}
|
||||
to := firstFreeNonShortcutSlot(layout)
|
||||
if to < 0 {
|
||||
return nil, 0, fmt.Errorf("cannot move dark knight combo skill %d from shortcut %d", skillID, from)
|
||||
}
|
||||
delete(layout, from)
|
||||
layout[to] = skillID
|
||||
moved++
|
||||
}
|
||||
return layout, moved, nil
|
||||
}
|
||||
|
||||
func firstFreeNonShortcutSlot(layout dnfrepo.SkillLayout) int {
|
||||
for slot := preferredTreeSlotFrom; slot < nonShortcutSlotEnd; slot++ {
|
||||
if _, occupied := layout[slot]; !occupied {
|
||||
return slot
|
||||
}
|
||||
}
|
||||
for slot := nonShortcutSlotFrom; slot < nonShortcutSlotEnd; slot++ {
|
||||
if _, occupied := layout[slot]; !occupied {
|
||||
return slot
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func firstFreeSlot(layout dnfrepo.SkillLayout, start int, end int) int {
|
||||
for slot := start; slot < end; slot++ {
|
||||
if _, occupied := layout[slot]; !occupied {
|
||||
return slot
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in new issue
Block a user