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