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,200 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
type dungeonMazeSelection struct {
|
||||
Index int
|
||||
Reason string
|
||||
QuestID int64
|
||||
}
|
||||
|
||||
// selectDungeonMaze mirrors the PVF domain rule used by df_game_r's
|
||||
// GetAppropriateMaze flow: an active quest connection wins first, then a
|
||||
// cleared-quest connection, then one of the non-quest mazes. The current op16
|
||||
// writer does not carry a proved maze index, so the choice is owned by the
|
||||
// server's real PVF and persisted quest state rather than by an opaque request
|
||||
// field.
|
||||
func selectDungeonMaze(
|
||||
mazes []worldmap.Maze,
|
||||
difficulty byte,
|
||||
record dnfrepo.QuestRecord,
|
||||
choose func(int) (int, error),
|
||||
) (dungeonMazeSelection, error) {
|
||||
if len(mazes) == 0 {
|
||||
return dungeonMazeSelection{}, worldmap.ErrMazeNotFound
|
||||
}
|
||||
active, cleared := dungeonQuestStateSets(record)
|
||||
if selected, ok, err := selectQuestConnectedMaze(mazes, active, 0, difficulty, choose); ok || err != nil {
|
||||
selected.Reason = "active_quest_connection"
|
||||
return selected, err
|
||||
}
|
||||
if selected, ok, err := selectQuestConnectedMaze(mazes, cleared, 1, difficulty, choose); ok || err != nil {
|
||||
selected.Reason = "cleared_quest_connection"
|
||||
return selected, err
|
||||
}
|
||||
|
||||
defaults := make([]int, 0, len(mazes))
|
||||
for index := range mazes {
|
||||
if len(mazes[index].QuestConnection) == 0 {
|
||||
defaults = append(defaults, index)
|
||||
}
|
||||
}
|
||||
if len(defaults) == 0 {
|
||||
return dungeonMazeSelection{Index: 0, Reason: "pvf_first_maze_no_default"}, nil
|
||||
}
|
||||
index, err := chooseDungeonWeightedMaze(mazes, defaults, choose)
|
||||
if err != nil {
|
||||
return dungeonMazeSelection{}, fmt.Errorf("choose non-quest maze: %w", err)
|
||||
}
|
||||
return dungeonMazeSelection{Index: index, Reason: "pvf_non_quest_maze"}, nil
|
||||
}
|
||||
|
||||
// chooseDungeonWeightedMaze follows a dungeon's optional PVF [maze chance
|
||||
// rate] values. Time Rift declares eight normal routes at 11875 each and two
|
||||
// special routes at 2500 each, so treating the ten mazes as uniform makes an
|
||||
// unsupported special route four times too likely. Legacy dungeons without
|
||||
// any rate keep the original candidate-count chooser shape.
|
||||
func chooseDungeonWeightedMaze(
|
||||
mazes []worldmap.Maze,
|
||||
candidates []int,
|
||||
choose func(int) (int, error),
|
||||
) (int, error) {
|
||||
weighted := false
|
||||
for _, index := range candidates {
|
||||
if index >= 0 && index < len(mazes) && mazes[index].ChanceRate.Set {
|
||||
weighted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !weighted {
|
||||
return chooseDungeonCandidate(candidates, choose)
|
||||
}
|
||||
|
||||
total := int64(0)
|
||||
weights := make([]int64, len(candidates))
|
||||
for position, index := range candidates {
|
||||
if index < 0 || index >= len(mazes) {
|
||||
return 0, fmt.Errorf("maze candidate index %d outside [0,%d)", index, len(mazes))
|
||||
}
|
||||
weight := int64(1)
|
||||
if mazes[index].ChanceRate.Set {
|
||||
weight = mazes[index].ChanceRate.Value
|
||||
}
|
||||
if weight <= 0 || weight > int64(^uint(0)>>1)-total {
|
||||
return 0, fmt.Errorf("invalid maze chance rate=%d maze=%d", weight, index)
|
||||
}
|
||||
weights[position] = weight
|
||||
total += weight
|
||||
}
|
||||
if total <= 0 || total > int64(^uint(0)>>1) {
|
||||
return 0, fmt.Errorf("invalid total maze chance rate=%d", total)
|
||||
}
|
||||
if choose == nil {
|
||||
return 0, fmt.Errorf("%w: weighted_candidates=%d total_weight=%d", errDungeonChoiceRequired, len(candidates), total)
|
||||
}
|
||||
roll, err := choose(int(total))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if roll < 0 || int64(roll) >= total {
|
||||
return 0, fmt.Errorf("dungeon weighted maze roll %d outside [0,%d)", roll, total)
|
||||
}
|
||||
remaining := int64(roll)
|
||||
for position, weight := range weights {
|
||||
if remaining < weight {
|
||||
return candidates[position], nil
|
||||
}
|
||||
remaining -= weight
|
||||
}
|
||||
return 0, fmt.Errorf("dungeon weighted maze roll %d did not resolve", roll)
|
||||
}
|
||||
|
||||
func selectQuestConnectedMaze(
|
||||
mazes []worldmap.Maze,
|
||||
questIDs map[int64]struct{},
|
||||
requiredType int64,
|
||||
difficulty byte,
|
||||
choose func(int) (int, error),
|
||||
) (dungeonMazeSelection, bool, error) {
|
||||
if len(questIDs) == 0 {
|
||||
return dungeonMazeSelection{}, false, nil
|
||||
}
|
||||
candidates := make([]int, 0, 1)
|
||||
for index := range mazes {
|
||||
connection := mazes[index].QuestConnection
|
||||
if len(connection) < 2 || connection[0] != requiredType {
|
||||
continue
|
||||
}
|
||||
questID := connection[1]
|
||||
if _, exists := questIDs[questID]; !exists {
|
||||
continue
|
||||
}
|
||||
if requiredType == 0 && len(connection) >= 3 && connection[2] >= 0 && int64(difficulty) < connection[2] {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, index)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return dungeonMazeSelection{}, false, nil
|
||||
}
|
||||
index, err := chooseDungeonCandidate(candidates, choose)
|
||||
if err != nil {
|
||||
return dungeonMazeSelection{}, true, fmt.Errorf("choose quest-connected maze: %w", err)
|
||||
}
|
||||
return dungeonMazeSelection{Index: index, QuestID: mazes[index].QuestConnection[1]}, true, nil
|
||||
}
|
||||
|
||||
func chooseDungeonCandidate(candidates []int, choose func(int) (int, error)) (int, error) {
|
||||
if len(candidates) == 0 {
|
||||
return 0, worldmap.ErrMazeNotFound
|
||||
}
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0], nil
|
||||
}
|
||||
if choose == nil {
|
||||
return 0, fmt.Errorf("%w: candidates=%d", errDungeonChoiceRequired, len(candidates))
|
||||
}
|
||||
position, err := choose(len(candidates))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if position < 0 || position >= len(candidates) {
|
||||
return 0, fmt.Errorf("dungeon candidate position %d outside [0,%d)", position, len(candidates))
|
||||
}
|
||||
return candidates[position], nil
|
||||
}
|
||||
|
||||
func dungeonQuestStateSets(record dnfrepo.QuestRecord) (active map[int64]struct{}, cleared map[int64]struct{}) {
|
||||
active = make(map[int64]struct{})
|
||||
cleared = make(map[int64]struct{})
|
||||
collect := func(states map[int64]dnfrepo.QuestState) {
|
||||
for questID, state := range states {
|
||||
if questID <= 0 {
|
||||
continue
|
||||
}
|
||||
switch normalizeDungeonQuestStatus(state.Status) {
|
||||
case "active", "accepted", "inprogress", "progress":
|
||||
active[questID] = struct{}{}
|
||||
case "complete", "completed", "cleared", "finished", "done":
|
||||
cleared[questID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
collect(record.States)
|
||||
collect(record.Progress)
|
||||
return active, cleared
|
||||
}
|
||||
|
||||
func normalizeDungeonQuestStatus(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
value = strings.ReplaceAll(value, "_", "")
|
||||
value = strings.ReplaceAll(value, "-", "")
|
||||
value = strings.ReplaceAll(value, " ", "")
|
||||
return value
|
||||
}
|
||||
Reference in new issue
Block a user