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,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
|
||||
}
|
||||
Reference in new issue
Block a user