Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

188 lines
5.4 KiB
Go

// 本文件提供角色链 fallback 的只读 owner 预检。
// 登录、选角、USERINFO 和角色列表仍由 dnfbridge 专门链路闭合,这里不写角色也不开放成功 ACK。
package character
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
var (
ErrOwnerUnavailable = errors.New("character owner unavailable")
ErrCharacterRequired = errors.New("selected character id required")
ErrCharacterNotFound = errors.New("character record not found")
ErrAccountMismatch = errors.New("character account mismatch")
)
// Owner 是角色链 fallback 的只读查询边界。
type Owner struct {
characters dnfrepo.CharacterRepository
}
// PlanResult 描述角色请求当前能从角色仓储确认到的上下文。
type PlanResult struct {
AccountID string
CharacterID string
CharacterKnown bool
Name string
Slot int
Job string
Level int
RosterCount int
NameTaken bool
NameOwnerID string
RequestedName string
RequestedJob byte
RequestedSlot int
SelectedOrRequested uint16
}
// NewOwner 创建角色 owner;缺少角色仓储时拒绝预检。
func NewOwner(repos dnfrepo.Group) (*Owner, error) {
if repos.Character == nil {
return nil, ErrOwnerUnavailable
}
return &Owner{characters: repos.Character}, nil
}
// Plan 只读取角色名索引、账号角色列表或当前角色快照,不承担真实登录/选角回包。
func (o *Owner) Plan(ctx context.Context, cmd Command) (PlanResult, error) {
if o == nil || o.characters == nil {
return PlanResult{}, ErrOwnerUnavailable
}
result := PlanResult{
AccountID: strings.TrimSpace(cmd.AccountID),
RequestedName: strings.TrimSpace(cmd.Name),
RequestedJob: cmd.Job,
RequestedSlot: int(cmd.Slot),
SelectedOrRequested: cmd.SlotOrCharacterID,
}
if result.AccountID != "" {
roster, err := o.characters.ListByAccount(ctx, result.AccountID, 32)
if err != nil {
return PlanResult{}, err
}
result.RosterCount = len(roster)
}
switch cmd.Operation {
case "select_character":
record, ok, err := o.findSelected(ctx, cmd)
if err != nil {
return PlanResult{}, err
}
if !ok {
return PlanResult{}, ErrCharacterNotFound
}
fillCharacterResult(&result, record)
case "return_select_character":
// 返回角色选择只需要账号角色列表上下文,真实 op2 刷新仍由专门链路生成。
case "get_userinfo":
record, ok, err := o.findSelected(ctx, cmd)
if err != nil {
return PlanResult{}, err
}
if !ok {
return PlanResult{}, ErrCharacterNotFound
}
fillCharacterResult(&result, record)
case "create_character", "check_double_character_name":
if result.RequestedName != "" {
id, taken, err := o.characters.FindIDByName(ctx, result.RequestedName)
if err != nil {
return PlanResult{}, err
}
result.NameTaken = taken
result.NameOwnerID = id
}
case "delete_character":
if result.RequestedName != "" {
id, taken, err := o.characters.FindIDByName(ctx, result.RequestedName)
if err != nil {
return PlanResult{}, err
}
result.NameTaken = taken
result.NameOwnerID = id
if taken {
record, ok, err := o.characters.Load(ctx, id)
if err != nil {
return PlanResult{}, err
}
if ok {
fillCharacterResult(&result, record)
}
}
}
}
return result, nil
}
func (o *Owner) findSelected(ctx context.Context, cmd Command) (dnfrepo.CharacterRecord, bool, error) {
accountID := strings.TrimSpace(cmd.AccountID)
if cmd.SelectedCharacterID != 0 {
record, ok, err := o.loadByNumericID(ctx, cmd.SelectedCharacterID)
if err != nil || !ok {
return record, ok, err
}
if !matchesAccount(record, accountID) {
return dnfrepo.CharacterRecord{}, false, ErrAccountMismatch
}
return record, true, nil
}
if cmd.SlotOrCharacterID != 0 {
if record, ok, err := o.loadByNumericID(ctx, cmd.SlotOrCharacterID); err != nil || (ok && matchesAccount(record, accountID)) {
return record, ok, err
}
}
if accountID == "" {
return dnfrepo.CharacterRecord{}, false, ErrCharacterRequired
}
roster, err := o.characters.ListByAccount(ctx, accountID, 32)
if err != nil {
return dnfrepo.CharacterRecord{}, false, err
}
requestedSlot := int(cmd.SlotOrCharacterID)
for _, record := range roster {
if record.Slot == requestedSlot {
return dnfrepo.CloneCharacter(record), true, nil
}
}
return dnfrepo.CharacterRecord{}, false, nil
}
func (o *Owner) loadByNumericID(ctx context.Context, id uint16) (dnfrepo.CharacterRecord, bool, error) {
key := strconv.FormatUint(uint64(id), 10)
record, ok, err := o.characters.Load(ctx, key)
if err != nil || !ok {
return dnfrepo.CharacterRecord{}, ok, err
}
return dnfrepo.CloneCharacter(record), true, nil
}
func matchesAccount(record dnfrepo.CharacterRecord, accountID string) bool {
return accountID == "" || strings.TrimSpace(record.AccountID) == accountID
}
func fillCharacterResult(result *PlanResult, record dnfrepo.CharacterRecord) {
record = dnfrepo.CloneCharacter(record)
result.CharacterID = record.CharacterID
result.CharacterKnown = true
result.AccountID = record.AccountID
result.Name = record.Name
result.Slot = record.Slot
result.Job = record.Job
result.Level = record.Level
}
func planError(operation string, err error) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", strings.TrimSpace(operation), err)
}