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,244 @@
// 放弃任务的道具回收:只回收"属于这个任务"的道具。
//
// 参考端(_86ref/Server/DfoServer/Game/Quests/QuestGiveupItemRecoveryPolicy.cs
// + QuestGiveupApplicationService.cs)的口径:
//
// 1. 候选集 = 本任务的 `[depend give item]` 事件道具 ∪ 本任务的需求道具
// (`[seeking]` / `[seek n meet npc]` 的 `[int data]`);
// 2. 只有"是该任务的事件道具"**或**"PVF 家族是 `[quest]`(任务专属)"才真的删;
// **普通材料(`[material]` 等)一律不动** —— 那些是玩家自己的东西,
// 任务只是数背包里的数量,交付时才扣(见 finish_owner.go 的
// consumeFinishRequiredItems);
// 3. 还要给"其它仍在进行中的任务"留够需求量(retainCount),只删超出的部分。
//
// 现场依据(2026-09-22 角色 1111 日常任务 8557「进攻准备」):
// `[type] [seeking]`、`[int data] 10094602 10`、无 `[depend give item]`,
// 而 10094602(未精炼的念气结晶)是 `[material]` + `[trade]` 的可交易普通材料
// ⇒ 它的回收名单为空:放弃应当成功,且一件都不删。
package quest
import (
"sort"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// GiveUpReclaimEntry 是一件"放弃该任务后应当回收"的道具及其保留量。
type GiveUpReclaimEntry struct {
ItemID int64
// RetainCount 是其它仍在进行中的任务对该道具的需求量:只删超出这一数量的部分。
RetainCount int64
}
// GiveUpReclaimedItem 记录一次真实发生的扣减,供日志与后续背包刷新使用。
type GiveUpReclaimedItem struct {
SlotKey string
SlotIndex uint16
ItemID int64
Delta int64
PostCount int64
RawEntry []byte
}
// PlanGiveUpReclaim 计算放弃 abandonedQuestID 时应回收的道具。
// isQuestItem 判定某道具是否属于 PVF `[quest]` 家族(任务专属、落在任务栏
// 177..232 且不可交易)。nil 视为"没有任何任务专属道具":放弃仍然成功,只是
// 不回收任何东西 —— 这与参考端在物品元数据不可用时的降级一致。
func PlanGiveUpReclaim(
catalog *Catalog,
record dnfrepo.QuestRecord,
abandonedQuestID int64,
isQuestItem func(int64) bool,
) []GiveUpReclaimEntry {
if catalog == nil || abandonedQuestID <= 0 {
return nil
}
definition, known := catalog.Find(abandonedQuestID)
if !known {
return nil
}
activationCounts := giveUpItemRuleCounts(itemPairRulesFromInts(definition.DependGiveItemData))
candidates := make(map[int64]struct{}, len(activationCounts)+2)
for itemID, count := range activationCounts {
if count > 0 {
candidates[itemID] = struct{}{}
}
}
for _, rule := range finishDefinitionSeekingRules(definition) {
if rule.ItemID > 0 && rule.Count > 0 {
candidates[rule.ItemID] = struct{}{}
}
}
if len(candidates) == 0 {
return nil
}
plan := make([]GiveUpReclaimEntry, 0, len(candidates))
for itemID := range candidates {
if _, isActivationItem := activationCounts[itemID]; !isActivationItem &&
(isQuestItem == nil || !isQuestItem(itemID)) {
// 普通材料:不是这个任务发的、也不是任务专属 ⇒ 不回收。
continue
}
plan = append(plan, GiveUpReclaimEntry{
ItemID: itemID,
RetainCount: retainedGiveUpItemCount(catalog, record, abandonedQuestID, itemID),
})
}
sort.Slice(plan, func(left, right int) bool { return plan[left].ItemID < plan[right].ItemID })
return plan
}
// retainedGiveUpItemCount 取"其它进行中任务"对该道具的最大需求量。
func retainedGiveUpItemCount(
catalog *Catalog,
record dnfrepo.QuestRecord,
abandonedQuestID int64,
itemID int64,
) int64 {
if catalog == nil || itemID <= 0 {
return 0
}
retain := int64(0)
for _, questID := range activeGiveUpQuestIDs(record, abandonedQuestID) {
definition, known := catalog.Find(questID)
if !known {
continue
}
eventCount := giveUpItemRuleCounts(itemPairRulesFromInts(definition.DependGiveItemData))[itemID]
seekingCount := giveUpItemRuleCounts(finishDefinitionSeekingRules(definition))[itemID]
need := eventCount
if seekingCount > need {
need = seekingCount
}
if need > retain {
retain = need
}
}
return retain
}
// activeGiveUpQuestIDs 返回除被放弃任务之外的进行中任务 id(States 与 Progress
// 的并集;同一任务在两处出现时只算一次)。
func activeGiveUpQuestIDs(record dnfrepo.QuestRecord, abandonedQuestID int64) []int64 {
seen := make(map[int64]struct{}, len(record.States)+len(record.Progress))
ids := make([]int64, 0, len(record.States)+len(record.Progress))
collect := func(states map[int64]dnfrepo.QuestState) {
for questID, state := range states {
if questID <= 0 || questID == abandonedQuestID {
continue
}
if !isActiveQuestStatus(state.Status) {
continue
}
if _, exists := seen[questID]; exists {
continue
}
seen[questID] = struct{}{}
ids = append(ids, questID)
}
}
collect(record.States)
collect(record.Progress)
sort.Slice(ids, func(left, right int) bool { return ids[left] < ids[right] })
return ids
}
func giveUpItemRuleCounts(rules []RewardItemRule) map[int64]int64 {
if len(rules) == 0 {
return nil
}
counts := make(map[int64]int64, len(rules))
for _, rule := range rules {
if rule.ItemID <= 0 || rule.Count <= 0 {
continue
}
counts[rule.ItemID] += rule.Count
}
return counts
}
// applyGiveUpReclaim 按计划扣减角色背包主容器(list 0 非账号共享格)里的道具。
// 只处理计数为正的普通行:计数为 0 的原始行(数量只存在于 0x77 原始行里)不动,
// 避免误删来源不明的行。扣到 0 的行删除,否则改写计数并同步 0x77 原始行。
func applyGiveUpReclaim(
record *dnfrepo.InventoryRecord,
plan []GiveUpReclaimEntry,
) []GiveUpReclaimedItem {
if record == nil || len(plan) == 0 {
return nil
}
type reclaimSlot struct {
key string
index uint16
stack dnfrepo.ItemStack
}
byItem := make(map[int64][]reclaimSlot, len(plan))
for key, stack := range record.Slots {
index, ok := parseFinishMainInventorySlotKey(key)
if !ok || finishSlotIsAccountShared(index) {
continue
}
if stack.ItemID <= 0 || stack.Count <= 0 {
continue
}
byItem[stack.ItemID] = append(byItem[stack.ItemID], reclaimSlot{key: key, index: index, stack: stack})
}
for itemID := range byItem {
slots := byItem[itemID]
sort.Slice(slots, func(left, right int) bool { return slots[left].index < slots[right].index })
byItem[itemID] = slots
}
reclaimed := make([]GiveUpReclaimedItem, 0, len(plan))
for _, entry := range plan {
if entry.ItemID <= 0 {
continue
}
slots := byItem[entry.ItemID]
if len(slots) == 0 {
continue
}
held := int64(0)
for _, candidate := range slots {
held += candidate.stack.Count
}
remaining := held - entry.RetainCount
if remaining <= 0 {
continue
}
for _, candidate := range slots {
if remaining <= 0 {
break
}
take := candidate.stack.Count
if take > remaining {
take = remaining
}
post := candidate.stack.Count - take
receipt := GiveUpReclaimedItem{
SlotKey: candidate.key,
SlotIndex: candidate.index,
ItemID: entry.ItemID,
Delta: take,
PostCount: post,
}
if post <= 0 {
delete(record.Slots, candidate.key)
} else {
stack := candidate.stack
stack.Count = post
stack = finishStackWithUpdatedCount(stack, candidate.index, entry.ItemID, post)
record.Slots[candidate.key] = stack
receipt.RawEntry = append([]byte(nil), stack.RawEntry...)
}
reclaimed = append(reclaimed, receipt)
remaining -= take
}
}
if len(reclaimed) == 0 {
return nil
}
return reclaimed
}