Files
90CNServer/server/internal/modules/dnf/repository/container_state.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

146 lines
5.1 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
var ErrCharacterContainerStateInvalid = errors.New("character container state is invalid")
// CharacterContainerState is the typed view of the current EXE op13 header
// state. The field names follow current handler behavior, not the old C# wire
// model.
type CharacterContainerState struct {
CharacterID string
MainSlotCount uint16
AvatarExpansion uint16
PersonalCargoSlotCount uint16
AccountCargoSelectionKey uint16
AccountCargoStateValue uint32
Source string
UpdatedAt time.Time
}
func CharacterContainerStateScope(characterID string) string {
return "character:" + strings.TrimSpace(characterID) + ":container_state"
}
// LoadCharacterContainerState reads the existing settings aggregate without
// mutating it or inventing state for characters that do not have a row.
func LoadCharacterContainerState(ctx context.Context, repo SettingsRepository, characterID string) (CharacterContainerState, bool, error) {
characterID = strings.TrimSpace(characterID)
if repo == nil || characterID == "" {
return CharacterContainerState{}, false, nil
}
record, found, err := repo.Load(ctx, CharacterContainerStateScope(characterID))
if err != nil || !found {
return CharacterContainerState{}, found, err
}
state, err := characterContainerStateFromSettings(record, characterID)
if err != nil {
return CharacterContainerState{}, false, err
}
return state, true, nil
}
// CharacterContainerStateFromSettings validates one already loaded settings
// row. Transaction owners use it to avoid escaping their scoped repository.
func CharacterContainerStateFromSettings(record SettingsRecord, characterID string) (CharacterContainerState, error) {
return characterContainerStateFromSettings(record, characterID)
}
func characterContainerStateFromSettings(record SettingsRecord, characterID string) (CharacterContainerState, error) {
values := record.Values
mainSlots, err := requiredContainerUint16(values, "main_list_param16")
if err != nil {
return CharacterContainerState{}, err
}
avatarExpansion, err := requiredContainerUint16(values, "avatar_list_param16")
if err != nil {
return CharacterContainerState{}, err
}
personalCargoSlots, err := requiredContainerUint16(values, "personal_cargo_list_param16")
if err != nil {
return CharacterContainerState{}, err
}
accountCargoKey, err := optionalContainerUint16(values, "account_cargo_selection_key")
if err != nil {
return CharacterContainerState{}, err
}
accountCargoValue, err := optionalContainerUint32(values, "account_cargo_value32")
if err != nil {
return CharacterContainerState{}, err
}
if !currentMainInventoryExpansion(mainSlots) {
return CharacterContainerState{}, fmt.Errorf("%w: main_list_param16=%d is not in the current EXE expansion table", ErrCharacterContainerStateInvalid, mainSlots)
}
if !currentAvatarInventoryExpansion(avatarExpansion) {
return CharacterContainerState{}, fmt.Errorf("%w: avatar_list_param16=%d is not in the current EXE expansion table", ErrCharacterContainerStateInvalid, avatarExpansion)
}
if !currentPersonalCargoSlotCount(personalCargoSlots) {
return CharacterContainerState{}, fmt.Errorf("%w: personal_cargo_list_param16=%d is not in the current EXE slot table", ErrCharacterContainerStateInvalid, personalCargoSlots)
}
return CharacterContainerState{
CharacterID: strings.TrimSpace(characterID),
MainSlotCount: mainSlots,
AvatarExpansion: avatarExpansion,
PersonalCargoSlotCount: personalCargoSlots,
AccountCargoSelectionKey: accountCargoKey,
AccountCargoStateValue: accountCargoValue,
Source: strings.TrimSpace(values["source"]),
UpdatedAt: record.UpdatedAt,
}, nil
}
func currentMainInventoryExpansion(value uint16) bool {
return value <= 24 && value%8 == 0
}
func currentAvatarInventoryExpansion(value uint16) bool {
return value <= 140 && value%7 == 0
}
func currentPersonalCargoSlotCount(value uint16) bool {
return value >= 8 && value <= 200 && (value-8)%16 == 0
}
func requiredContainerUint16(values map[string]string, key string) (uint16, error) {
raw := strings.TrimSpace(values[key])
if raw == "" {
return 0, fmt.Errorf("%w: %s is missing", ErrCharacterContainerStateInvalid, key)
}
value, err := strconv.ParseUint(raw, 0, 16)
if err != nil {
return 0, fmt.Errorf("%w: %s=%q: %v", ErrCharacterContainerStateInvalid, key, raw, err)
}
return uint16(value), nil
}
func optionalContainerUint16(values map[string]string, key string) (uint16, error) {
raw := strings.TrimSpace(values[key])
if raw == "" {
return 0, nil
}
value, err := strconv.ParseUint(raw, 0, 16)
if err != nil {
return 0, fmt.Errorf("%w: %s=%q: %v", ErrCharacterContainerStateInvalid, key, raw, err)
}
return uint16(value), nil
}
func optionalContainerUint32(values map[string]string, key string) (uint32, error) {
raw := strings.TrimSpace(values[key])
if raw == "" {
return 0, nil
}
value, err := strconv.ParseUint(raw, 0, 32)
if err != nil {
return 0, fmt.Errorf("%w: %s=%q: %v", ErrCharacterContainerStateInvalid, key, raw, err)
}
return uint32(value), nil
}