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,345 @@
package sqlite
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/platform/db"
)
type persistentStore[T any] struct {
component *Component
selectStore func(repository.Group) db.Store[T]
}
func (s *persistentStore[T]) Load(ctx context.Context, key string) (record T, found bool, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var loadErr error
record, found, loadErr = s.selectStore(group).Load(ctx, key)
return loadErr
})
return record, found, err
}
func (s *persistentStore[T]) Save(ctx context.Context, record T) error {
return s.component.mutateAsync(ctx, func(group repository.Group) error {
return s.selectStore(group).Save(ctx, record)
})
}
func (s *persistentStore[T]) Check(ctx context.Context) error { return s.component.check(ctx) }
type accountStore struct {
*persistentStore[repository.AccountRecord]
}
func (s *accountStore) ListAccounts(ctx context.Context, query string, limit int) (records []repository.AccountRecord, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var listErr error
records, listErr = repository.ListAccounts(ctx, group.Account, query, limit)
return listErr
})
return records, err
}
func (s *accountStore) FindAccountIDByLoginUsername(ctx context.Context, username string) (string, bool, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return "", false, err
}
username = strings.ToLower(strings.TrimSpace(username))
if username == "" {
return "", false, nil
}
if s == nil || s.component == nil || s.component.db == nil {
return "", false, ErrDatabaseRequired
}
var accountID string
err := s.component.db.QueryRowContext(ctx,
`SELECT account_id FROM dnf_login_accounts WHERE username = ? AND state = 'active' LIMIT 1`, username,
).Scan(&accountID)
if errors.Is(err, sql.ErrNoRows) {
return "", false, nil
}
if err != nil {
return "", false, fmt.Errorf("find SQLite login username: %w", err)
}
return strings.TrimSpace(accountID), true, nil
}
func (s *accountStore) SaveMetadataEntry(ctx context.Context, accountID, key, value string, updatedAt time.Time) error {
return s.component.mutateAsync(ctx, func(group repository.Group) error {
writer, ok := group.Account.(repository.AccountMetadataEntryWriter)
if !ok {
return repository.ErrAccountMetadataUnavailable
}
return writer.SaveMetadataEntry(ctx, accountID, key, value, updatedAt)
})
}
func (s *accountStore) FindAccountIDByRepresentName(ctx context.Context, name string) (accountID string, found bool, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
finder, ok := group.Account.(repository.RepresentAccountNameFinder)
if !ok {
return fmt.Errorf("account represent-name lookup is unavailable")
}
var findErr error
accountID, found, findErr = finder.FindAccountIDByRepresentName(ctx, name)
return findErr
})
return accountID, found, err
}
type characterStore struct {
*persistentStore[repository.CharacterRecord]
}
type questStore struct {
*persistentStore[repository.QuestRecord]
}
type unitedFriendStore struct{ component *Component }
func (s *unitedFriendStore) Check(ctx context.Context) error { return s.component.check(ctx) }
func (s *unitedFriendStore) Load(ctx context.Context, key string) (record repository.UnitedFriendRecord, found bool, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var loadErr error
record, found, loadErr = group.UnitedFriend.Load(ctx, key)
return loadErr
})
return record, found, err
}
func (s *unitedFriendStore) Save(ctx context.Context, record repository.UnitedFriendRecord) error {
return s.component.mutateSync(ctx, func(group repository.Group) error {
return group.UnitedFriend.Save(ctx, record)
})
}
func (s *unitedFriendStore) ListObservers(ctx context.Context, friendName string) (owners []string, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var listErr error
owners, listErr = group.UnitedFriend.ListObservers(ctx, friendName)
return listErr
})
return owners, err
}
// SaveQuestStateEntry mutates only one in-memory objective under SQLite's
// serialized mutation gate. The resulting snapshot is then flushed by the
// existing asynchronous WAL/checkpoint owner, preserving instant gameplay
// acknowledgement without allowing a stale full record to overwrite another
// objective update.
func (s *questStore) SaveQuestStateEntry(
ctx context.Context,
characterID string,
field repository.QuestField,
questID int64,
state repository.QuestState,
updatedAt time.Time,
) error {
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return db.ErrRecordKeyRequired
}
if field != repository.QuestFieldStates && field != repository.QuestFieldProgress {
return fmt.Errorf("unsupported quest state field %q", field)
}
cloned := repository.CloneQuest(repository.QuestRecord{
States: map[int64]repository.QuestState{questID: state},
}).States[questID]
return s.component.mutateAsync(ctx, func(group repository.Group) error {
record, found, err := group.Quest.Load(ctx, characterID)
if err != nil {
return err
}
if !found {
record = repository.QuestRecord{CharacterID: characterID}
} else {
record = repository.CloneQuest(record)
}
switch field {
case repository.QuestFieldStates:
if record.States == nil {
record.States = make(map[int64]repository.QuestState)
}
record.States[questID] = cloned
case repository.QuestFieldProgress:
if record.Progress == nil {
record.Progress = make(map[int64]repository.QuestState)
}
record.Progress[questID] = cloned
}
record.CharacterID = characterID
record.UpdatedAt = updatedAt
return group.Quest.Save(ctx, record)
})
}
func (s *characterStore) ListByAccount(ctx context.Context, accountID string, limit int) (records []repository.CharacterRecord, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var listErr error
records, listErr = group.Character.ListByAccount(ctx, accountID, limit)
return listErr
})
return records, err
}
func (s *characterStore) FindIDByName(ctx context.Context, name string) (characterID string, found bool, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var findErr error
characterID, found, findErr = group.Character.FindIDByName(ctx, name)
return findErr
})
return characterID, found, err
}
func (s *characterStore) NextNumericID(ctx context.Context) (next int, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var nextErr error
next, nextErr = group.Character.NextNumericID(ctx)
return nextErr
})
return next, err
}
func (s *characterStore) CreateCharacter(ctx context.Context, record repository.CharacterRecord) error {
return s.component.mutateSync(ctx, func(group repository.Group) error {
creator, ok := group.Character.(repository.CharacterCreator)
if !ok {
return repository.ErrCharacterCreateMissing
}
return creator.CreateCharacter(ctx, record)
})
}
func (s *characterStore) SwapCharacterSlots(ctx context.Context, accountID string, slotA, slotB int) error {
return s.component.mutateSync(ctx, func(group repository.Group) error {
swapper, ok := group.Character.(repository.CharacterSlotSwapper)
if !ok {
return repository.ErrCharacterSlotSwapUnavailable
}
return swapper.SwapCharacterSlots(ctx, accountID, slotA, slotB)
})
}
func (s *characterStore) DeleteCharacterAndCompact(ctx context.Context, accountID string, slot int, name string) error {
return s.component.mutateSync(ctx, func(group repository.Group) error {
deleter, ok := group.Character.(repository.CharacterRosterDeleter)
if !ok {
return repository.ErrCharacterDeleteUnavailable
}
return deleter.DeleteCharacterAndCompact(ctx, accountID, slot, name)
})
}
func (s *characterStore) AdvanceStoryDigest(ctx context.Context, characterID string, level, migrationVersion uint32) error {
return s.component.mutateAsync(ctx, func(group repository.Group) error {
advancer, ok := group.Character.(repository.CharacterStoryDigestAdvancer)
if !ok {
return repository.ErrCharacterStoryDigestAdvanceUnavailable
}
return advancer.AdvanceStoryDigest(ctx, characterID, level, migrationVersion)
})
}
type dungeonPermissionStore struct{ component *Component }
func (s *dungeonPermissionStore) Check(ctx context.Context) error { return s.component.check(ctx) }
func (s *dungeonPermissionStore) Load(ctx context.Context, key string) (record repository.DungeonPermissionRecord, found bool, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var loadErr error
record, found, loadErr = group.DungeonPermission.Load(ctx, key)
return loadErr
})
return record, found, err
}
func (s *dungeonPermissionStore) UpsertMax(ctx context.Context, characterID string, dungeonID uint32, clearState byte) (entry repository.DungeonPermissionEntry, changed bool, err error) {
err = s.component.mutateAsync(ctx, func(group repository.Group) error {
var upsertErr error
entry, changed, upsertErr = group.DungeonPermission.UpsertMax(ctx, characterID, dungeonID, clearState)
return upsertErr
})
return entry, changed, err
}
type hellPartyPermissionStore struct{ component *Component }
func (s *hellPartyPermissionStore) Check(ctx context.Context) error { return s.component.check(ctx) }
func (s *hellPartyPermissionStore) Load(ctx context.Context, key string) (record repository.HellPartyPermissionRecord, found bool, err error) {
err = s.component.read(ctx, func(group repository.Group) error {
var loadErr error
record, found, loadErr = group.HellPartyPermission.Load(ctx, key)
return loadErr
})
return record, found, err
}
func (s *hellPartyPermissionStore) Upsert(ctx context.Context, characterID string, areaID, questID int64) (entry repository.HellPartyPermissionEntry, changed bool, err error) {
err = s.component.mutateAsync(ctx, func(group repository.Group) error {
var upsertErr error
entry, changed, upsertErr = group.HellPartyPermission.Upsert(ctx, characterID, areaID, questID)
return upsertErr
})
return entry, changed, err
}
func (s *hellPartyPermissionStore) Delete(ctx context.Context, characterID string, areaID int64) error {
return s.component.mutateAsync(ctx, func(group repository.Group) error {
deleter, ok := group.HellPartyPermission.(repository.HellPartyPermissionRestorer)
if !ok {
return fmt.Errorf("hell-party permission delete is unavailable")
}
return deleter.Delete(ctx, characterID, areaID)
})
}
func newPersistentStore[T any](component *Component, selector func(repository.Group) db.Store[T]) *persistentStore[T] {
return &persistentStore[T]{component: component, selectStore: selector}
}
func buildGroup(component *Component) repository.Group {
account := &accountStore{newPersistentStore(component, func(g repository.Group) db.Store[repository.AccountRecord] { return g.Account })}
character := &characterStore{newPersistentStore(component, func(g repository.Group) db.Store[repository.CharacterRecord] { return g.Character })}
quest := &questStore{newPersistentStore(component, func(g repository.Group) db.Store[repository.QuestRecord] { return g.Quest })}
transactions := &unitOfWork{component: component}
return repository.Group{
Account: account,
AccountInventory: newPersistentStore(component, func(g repository.Group) db.Store[repository.AccountInventoryRecord] { return g.AccountInventory }),
Character: character,
Inventory: newPersistentStore(component, func(g repository.Group) db.Store[repository.InventoryRecord] { return g.Inventory }),
Equipment: newPersistentStore(component, func(g repository.Group) db.Store[repository.EquipmentRecord] { return g.Equipment }),
Pet: newPersistentStore(component, func(g repository.Group) db.Store[repository.PetRecord] { return g.Pet }),
Quest: quest,
Skill: newPersistentStore(component, func(g repository.Group) db.Store[repository.SkillRecord] { return g.Skill }),
DungeonPermission: &dungeonPermissionStore{component: component},
HellPartyPermission: &hellPartyPermissionStore{component: component},
PacketTemplate: newPersistentStore(component, func(g repository.Group) db.Store[repository.PacketTemplateRecord] { return g.PacketTemplate }),
Settings: newPersistentStore(component, func(g repository.Group) db.Store[repository.SettingsRecord] { return g.Settings }),
Mailbox: newPersistentStore(component, func(g repository.Group) db.Store[repository.MailboxRecord] { return g.Mailbox }),
EpicBook: newPersistentStore(component, func(g repository.Group) db.Store[repository.EpicBookRecord] { return g.EpicBook }),
UnitedFriend: &unitedFriendStore{component: component},
CharacterCreate: transactions,
CharacterItems: transactions,
CharacterTrade: transactions,
AccountCharacterTrade: transactions,
AccountPairTrade: transactions,
CharacterPets: transactions,
AccountItems: transactions,
CharacterAssets: transactions,
MailboxAssets: transactions,
AccountAssets: transactions,
BoosterAssets: transactions,
RentalAssets: transactions,
CeraShopAssets: transactions,
CharacterSkills: transactions,
CharacterProgression: transactions,
CharacterSettlement: transactions,
EpicBookCraft: transactions,
RaidEntryCost: transactions,
RaidRewards: transactions,
}
}