Files
90CNServer/server/internal/modules/dnf/characterdata/aggregate.go
T
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

177 lines
5.3 KiB
Go

// Package characterdata owns protocol-independent character state assembly.
// Packet builders may consume an Aggregate, but wire layouts do not belong here.
package characterdata
import (
"context"
"errors"
"fmt"
"strings"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
var (
ErrCharacterIDRequired = errors.New("character id is required")
ErrCharacterNotFound = errors.New("character not found")
ErrRepositoryUnavailable = errors.New("character data repository is unavailable")
)
// Parts selects the durable child records to load with a character.
type Parts uint32
const (
PartInventory Parts = 1 << iota
PartEquipment
PartSkill
PartQuest
PartPet
AllParts = PartInventory | PartEquipment | PartSkill | PartQuest | PartPet
)
// Presence distinguishes a missing child record from a persisted empty record.
type Presence struct {
Inventory bool
Equipment bool
Skill bool
Quest bool
Pet bool
}
// Aggregate is a detached snapshot of one character and its requested durable
// child records. Generic settings are intentionally excluded until their
// character scopes become typed repositories.
type Aggregate struct {
Character dnfrepo.CharacterRecord
Inventory dnfrepo.InventoryRecord
Equipment dnfrepo.EquipmentRecord
Skill dnfrepo.SkillRecord
Quest dnfrepo.QuestRecord
Pet dnfrepo.PetRecord
Presence Presence
}
// Loader assembles character state through repository interfaces without
// depending on dnfbridge sessions or packet formats.
type Loader struct {
repositories dnfrepo.Group
}
func NewLoader(repositories dnfrepo.Group) *Loader {
return &Loader{repositories: repositories}
}
// Load reads the character base record and requested child records.
func (l *Loader) Load(ctx context.Context, characterID string, parts Parts) (Aggregate, bool, error) {
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return Aggregate{}, false, ErrCharacterIDRequired
}
if l == nil || l.repositories.Character == nil {
return Aggregate{}, false, repositoryError("character")
}
character, found, err := l.repositories.Character.Load(ctxOrBackground(ctx), characterID)
if err != nil || !found {
return Aggregate{}, found, err
}
aggregate, err := l.Hydrate(ctx, character, parts)
if err != nil {
return Aggregate{}, false, err
}
return aggregate, true, nil
}
// Hydrate attaches requested child records to an already loaded character.
func (l *Loader) Hydrate(ctx context.Context, character dnfrepo.CharacterRecord, parts Parts) (Aggregate, error) {
character = dnfrepo.CloneCharacter(character)
characterID := strings.TrimSpace(character.CharacterID)
if characterID == "" {
return Aggregate{}, ErrCharacterIDRequired
}
if l == nil {
return Aggregate{}, repositoryError("group")
}
ctx = ctxOrBackground(ctx)
aggregate := Aggregate{Character: character}
var err error
if parts&PartInventory != 0 {
if l.repositories.Inventory == nil {
return Aggregate{}, repositoryError("inventory")
}
aggregate.Inventory, aggregate.Presence.Inventory, err = l.repositories.Inventory.Load(ctx, characterID)
if err != nil {
return Aggregate{}, fmt.Errorf("load character inventory: %w", err)
}
aggregate.Inventory = dnfrepo.CloneInventory(aggregate.Inventory)
if !aggregate.Presence.Inventory {
aggregate.Inventory.CharacterID = characterID
}
}
if parts&PartEquipment != 0 {
if l.repositories.Equipment == nil {
return Aggregate{}, repositoryError("equipment")
}
aggregate.Equipment, aggregate.Presence.Equipment, err = l.repositories.Equipment.Load(ctx, characterID)
if err != nil {
return Aggregate{}, fmt.Errorf("load character equipment: %w", err)
}
aggregate.Equipment = dnfrepo.CloneEquipment(aggregate.Equipment)
if !aggregate.Presence.Equipment {
aggregate.Equipment.CharacterID = characterID
}
}
if parts&PartSkill != 0 {
if l.repositories.Skill == nil {
return Aggregate{}, repositoryError("skill")
}
aggregate.Skill, aggregate.Presence.Skill, err = l.repositories.Skill.Load(ctx, characterID)
if err != nil {
return Aggregate{}, fmt.Errorf("load character skill: %w", err)
}
aggregate.Skill = dnfrepo.CloneSkill(aggregate.Skill)
if !aggregate.Presence.Skill {
aggregate.Skill.CharacterID = characterID
}
}
if parts&PartQuest != 0 {
if l.repositories.Quest == nil {
return Aggregate{}, repositoryError("quest")
}
aggregate.Quest, aggregate.Presence.Quest, err = l.repositories.Quest.Load(ctx, characterID)
if err != nil {
return Aggregate{}, fmt.Errorf("load character quest: %w", err)
}
aggregate.Quest = dnfrepo.CloneQuest(aggregate.Quest)
if !aggregate.Presence.Quest {
aggregate.Quest.CharacterID = characterID
}
}
if parts&PartPet != 0 {
if l.repositories.Pet == nil {
return Aggregate{}, repositoryError("pet")
}
aggregate.Pet, aggregate.Presence.Pet, err = l.repositories.Pet.Load(ctx, characterID)
if err != nil {
return Aggregate{}, fmt.Errorf("load character pet: %w", err)
}
aggregate.Pet = dnfrepo.ClonePet(aggregate.Pet)
if !aggregate.Presence.Pet {
aggregate.Pet.CharacterID = characterID
}
}
return aggregate, nil
}
func repositoryError(name string) error {
return fmt.Errorf("%w: %s", ErrRepositoryUnavailable, name)
}
func ctxOrBackground(ctx context.Context) context.Context {
if ctx == nil {
return context.Background()
}
return ctx
}