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,319 @@
package player
import (
"context"
"fmt"
"strings"
"time"
)
// MailRecord 是玩家邮箱中的单封邮件记录。
type MailRecord struct {
ID string `json:"id"`
Title string `json:"title"`
State string `json:"state"`
CreatedAt time.Time `json:"created_at"`
}
// AddCurrency 增减玩家指定货币并记录关键字段变更事件。
func (m *Module) AddCurrency(ctx context.Context, accountID, currency string, delta int64) (Profile, error) {
currency = strings.TrimSpace(currency)
if currency == "" {
return Profile{}, fmt.Errorf("currency is required")
}
return m.mutateCritProfile(ctx, accountID, ProfileOperationAddCurrency, "handler.add_currency", func(profile *Profile, _ time.Time) (map[string]any, error) {
if profile.Currencies == nil {
profile.Currencies = defaultCurrencies()
}
before := profile.Currencies[currency]
profile.Currencies[currency] += delta
return map[string]any{
"currency": currency,
"delta": delta,
"balance_before": before,
"balance_after": profile.Currencies[currency],
}, nil
})
}
// SetInventoryItem 设置玩家背包物品数量,数量不大于零时删除该物品。
func (m *Module) SetInventoryItem(ctx context.Context, accountID, itemID string, quantity int64) (Profile, error) {
itemID = strings.TrimSpace(itemID)
if itemID == "" {
return Profile{}, fmt.Errorf("item id is required")
}
return m.mutateCritProfile(ctx, accountID, ProfileOperationSetInventoryItem, "handler.set_inventory_item", func(profile *Profile, _ time.Time) (map[string]any, error) {
if profile.Inventory == nil {
profile.Inventory = defaultInventory()
}
before := profile.Inventory[itemID]
if quantity <= 0 {
delete(profile.Inventory, itemID)
} else {
profile.Inventory[itemID] = quantity
}
return map[string]any{
"item_id": itemID,
"quantity_before": before,
"quantity_after": profile.Inventory[itemID],
"removed": quantity <= 0,
}, nil
})
}
// SetQuestState 设置玩家任务状态并写入任务字段。
func (m *Module) SetQuestState(ctx context.Context, accountID, questID, state string) (Profile, error) {
questID = strings.TrimSpace(questID)
state = strings.TrimSpace(state)
if questID == "" {
return Profile{}, fmt.Errorf("quest id is required")
}
return m.mutateCritProfile(ctx, accountID, ProfileOperationSetQuestState, "handler.set_quest_state", func(profile *Profile, _ time.Time) (map[string]any, error) {
if profile.Quests == nil {
profile.Quests = defaultQuests()
}
before := profile.Quests[questID]
profile.Quests[questID] = state
return map[string]any{
"quest_id": questID,
"state_before": before,
"state_after": state,
}, nil
})
}
// MarkMailRead 标记玩家邮件已读,并返回是否找到和是否发生变更。
func (m *Module) MarkMailRead(ctx context.Context, accountID, mailID string) (Profile, bool, bool, error) {
accountID = strings.TrimSpace(accountID)
mailID = strings.TrimSpace(mailID)
if accountID == "" {
return Profile{}, false, false, fmt.Errorf("account id is required")
}
if mailID == "" {
return Profile{}, false, false, fmt.Errorf("mail id is required")
}
now := time.Now().UTC()
var activeFound, activeChanged bool
if before, profile, ok, err := m.profiles.MutateActive(accountID, func(profile *Profile, now time.Time) ([]ProfileField, error) {
activeFound, activeChanged = markMailRead(profile, mailID)
if activeChanged {
fields := dirtyFieldsByModule(ProfileModuleMailbox)
eventProfile := cloneProfile(*profile)
eventProfile.UpdatedAt = now
eventProfile.Version++
if err := m.emitProfileMutation(ctx, eventProfile, "read_mail", "handler.read_mail", fields, map[string]any{
"mail_id": mailID,
"changed": true,
"unread_count": unreadMailCount(eventProfile),
}); err != nil {
return nil, err
}
return fields, nil
}
return nil, nil
}); err != nil {
return Profile{}, false, false, err
} else if ok {
if activeChanged {
m.observeSummaryChange(ctx, before, profile)
}
return cloneProfile(profile), activeFound, activeChanged, nil
}
profile, ok, err := m.profiles.LoadDetached(ctx, accountID)
if err != nil {
return Profile{}, false, false, err
}
dirtyFields := newProfileFieldSet(dirtyFieldsByModule(ProfileModuleRuntime)...)
if !ok {
profile = m.profiles.NewDetached(accountID, now)
dirtyFields.AddAll()
} else {
profile = normAccountID(profile, accountID)
dirtyFields.Merge(ensureProfileModules(&profile, now))
}
found, changed := markMailRead(&profile, mailID)
if !found {
return cloneProfile(profile), false, false, nil
}
profile.State = "online"
profile.LoadedAt = now
profile.UpdatedAt = now
profile.Version++
if changed {
dirtyFields.Add(dirtyFieldsByModule(ProfileModuleMailbox)...)
}
attachedProfile, attached := m.profiles.AttachIfAbsent(accountID, profile, dirtyFields.List()...)
if !attached {
_, current, ok, err := m.profiles.MutateActive(accountID, func(profile *Profile, now time.Time) ([]ProfileField, error) {
found, changed = markMailRead(profile, mailID)
if changed {
fields := dirtyFieldsByModule(ProfileModuleMailbox)
eventProfile := cloneProfile(*profile)
eventProfile.UpdatedAt = now
eventProfile.Version++
if err := m.emitProfileMutation(ctx, eventProfile, "read_mail", "handler.read_mail", fields, map[string]any{
"mail_id": mailID,
"changed": true,
"unread_count": unreadMailCount(eventProfile),
}); err != nil {
return nil, err
}
return fields, nil
}
return nil, nil
})
if err != nil {
return Profile{}, false, false, err
}
if ok && changed {
m.observeSummary(ctx, current)
}
return cloneProfile(current), found, changed, nil
}
m.observeSummary(ctx, attachedProfile)
if changed {
if err := m.emitProfileMutation(ctx, attachedProfile, "read_mail", "handler.read_mail", dirtyFieldsByModule(ProfileModuleMailbox), map[string]any{
"mail_id": mailID,
"changed": true,
"unread_count": unreadMailCount(attachedProfile),
}); err != nil {
return cloneProfile(attachedProfile), true, changed, err
}
}
return cloneProfile(attachedProfile), true, changed, nil
}
func applyProfileDefaults(profile *Profile, now time.Time) {
if profile == nil {
return
}
if strings.TrimSpace(profile.Name) == "" {
profile.Name = "Player " + profile.AccountID
}
if profile.Level <= 0 {
profile.Level = 1
}
_ = backfillModules(profile, now)
}
func cloneProfileModules(profile Profile) Profile {
profile.Currencies = cloneInt64Map(profile.Currencies)
profile.Inventory = cloneInt64Map(profile.Inventory)
profile.Quests = cloneStringMap(profile.Quests)
if profile.Mailbox != nil {
profile.Mailbox = append([]MailRecord(nil), profile.Mailbox...)
}
return profile
}
func backfillCurrency(profile *Profile, _ time.Time) bool {
if profile == nil || profile.Currencies != nil {
return false
}
profile.Currencies = defaultCurrencies()
return true
}
func backfillInventory(profile *Profile, _ time.Time) bool {
if profile == nil || profile.Inventory != nil {
return false
}
profile.Inventory = defaultInventory()
return true
}
func backfillProfileQuest(profile *Profile, _ time.Time) bool {
if profile == nil || profile.Quests != nil {
return false
}
profile.Quests = defaultQuests()
return true
}
func backfillProfileMail(profile *Profile, now time.Time) bool {
if profile == nil || profile.Mailbox != nil {
return false
}
profile.Mailbox = defaultMailbox(now)
return true
}
func defaultCurrencies() map[string]int64 {
return map[string]int64{
"gold": 0,
"diamond": 0,
}
}
func defaultInventory() map[string]int64 {
return map[string]int64{
"starter_sword": 1,
}
}
func defaultQuests() map[string]string {
return map[string]string{
"main_001": "accepted",
}
}
func defaultMailbox(now time.Time) []MailRecord {
return []MailRecord{
{
ID: "welcome",
Title: "Welcome",
State: "unread",
CreatedAt: now,
},
}
}
func markMailRead(profile *Profile, mailID string) (bool, bool) {
for i := range profile.Mailbox {
if profile.Mailbox[i].ID == mailID {
if profile.Mailbox[i].State == "read" {
return true, false
}
profile.Mailbox[i].State = "read"
return true, true
}
}
return false, false
}
func unreadMailCount(profile Profile) int {
count := 0
for _, mail := range profile.Mailbox {
if mail.State != "read" {
count++
}
}
return count
}
func cloneInt64Map(in map[string]int64) map[string]int64 {
if in == nil {
return nil
}
out := make(map[string]int64, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func cloneStringMap(in map[string]string) map[string]string {
if in == nil {
return nil
}
out := make(map[string]string, len(in))
for k, v := range in {
out[k] = v
}
return out
}