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,159 @@
package pvp
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
"time"
repo "longheng.io/server/internal/modules/dnf/repository"
)
var ErrTotalRewardInventoryFull = errors.New("total mission reward inventory full")
type TotalRewardPlacement func(repo.InventoryRecord, int64, int64, time.Time) (repo.InventoryRecord, error)
type TotalRewards struct {
Character repo.CharacterRecord
Items map[int16]repo.ItemStack
Granted, Pending []uint16
}
func totalRewardKey(id uint16) string { return "pvp_total_target_" + strconv.Itoa(int(id)) + "_paid" }
// GrantTotalTargets consumes committed account progress. Progress survives an
// interrupted delivery; the next result or login resumes it. Items, emblems and
// the account-wide paid marker commit together, so changing characters or
// replaying a result cannot pay a target twice. No speculative mail is created.
func GrantTotalTargets(ctx context.Context, repos repo.Group, c *Catalog, accountID, selected string, now time.Time, place TotalRewardPlacement) (TotalRewards, error) {
var result TotalRewards
if c == nil || repos.RentalAssets == nil || now.IsZero() {
return result, fmt.Errorf("total reward dependencies missing")
}
err := repo.WithinRentalAssetsScoped(ctx, repos.RentalAssets, accountID, selected,
func(accounts repo.AccountRepository, characters repo.CharacterRepository, inventories repo.InventoryRepository, _ repo.EquipmentRepository) error {
account, found, err := accounts.Load(ctx, accountID)
if err != nil {
return err
}
if !found || account.AccountID != accountID {
return fmt.Errorf("total reward account missing")
}
character, found, err := characters.Load(ctx, selected)
if err != nil {
return err
}
if !found || character.AccountID != accountID || character.Stats["delete_flag"] != 0 {
return fmt.Errorf("total reward character owner mismatch")
}
character = repo.CloneCharacter(character)
if character.Stats == nil {
character.Stats = map[string]int64{}
}
p, err := ReadTotalProgress(account)
if err != nil {
return err
}
rows, err := c.normalRows(totalMissionCharacter(accountID, p.Stats))
if err != nil {
return err
}
complete := map[uint16]bool{}
for _, row := range rows {
complete[row.ID] = row.Progress == row.Target
}
rewardIDs := map[int64]bool{}
var inventory repo.InventoryRecord
loaded, dirty := false, false
for _, m := range c.Normal {
paid := p.Stats[totalRewardKey(m.ID)]
if paid != 0 && paid != 1 || paid == 1 && !complete[m.ID] {
return fmt.Errorf("invalid total reward marker %d", m.ID)
}
if m.RewardItemID > 0 {
rewardIDs[m.RewardItemID] = true
}
if paid == 1 || !complete[m.ID] || m.PreRequired != 0 && !complete[m.PreRequired] {
continue
}
balance := character.Stats[EmblemsKey]
if m.Emblems < 0 || m.Emblems > math.MaxInt32 || balance < 0 || balance > math.MaxInt32-m.Emblems {
return fmt.Errorf("total reward emblem overflow")
}
if m.RewardItemID > 0 {
if place == nil {
return fmt.Errorf("total mission item placement missing")
}
if !loaded {
inventory, found, err = inventories.Load(ctx, selected)
if err != nil {
return err
}
if !found || inventory.CharacterID != selected {
return fmt.Errorf("total reward inventory missing")
}
loaded = true
}
next, err := place(repo.CloneInventory(inventory), m.RewardItemID, m.RewardItemCount, now)
if errors.Is(err, ErrTotalRewardInventoryFull) {
result.Pending = append(result.Pending, m.ID)
continue
}
if err != nil {
return err
}
if next.CharacterID != selected {
return fmt.Errorf("total reward inventory owner changed")
}
inventory, dirty = next, true
}
character.Stats[EmblemsKey] = balance + m.Emblems
p.Stats[totalRewardKey(m.ID)] = 1
result.Granted = append(result.Granted, m.ID)
}
if len(result.Granted) > 0 {
if dirty {
if err := repo.SaveInventoryFields(ctx, inventories, inventory, repo.InventoryFieldSlots); err != nil {
return err
}
}
if err := repo.SaveCharacterFields(ctx, characters, character, repo.CharacterFieldStats); err != nil {
return err
}
raw, err := json.Marshal(p)
if err != nil {
return err
}
if err := repo.SaveAccountMetadataEntry(ctx, accounts, account, TotalProgressMetadataKey, string(raw), now); err != nil {
return err
}
}
// Publish current absolute values also on an idempotent retry, including
// an indeterminate commit that completed before the connection failed.
if len(rewardIDs) > 0 && !loaded {
inventory, _, err = inventories.Load(ctx, selected)
if err != nil {
return err
}
}
result.Items = map[int16]repo.ItemStack{}
for key, stack := range inventory.Slots {
if !rewardIDs[stack.ItemID] {
continue
}
var list, slot int
if _, err := fmt.Sscanf(key, "%d:%d", &list, &slot); err == nil && list == 0 && slot >= 0 && slot <= math.MaxInt16 {
result.Items[int16(slot)] = stack
}
}
result.Character = character
return nil
})
if err != nil {
return TotalRewards{}, err
}
return result, nil
}