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,449 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
// scopedRecordRef identifies one aggregate record that a synchronous unit of
|
||||
// work is allowed to mutate. Keeping this list explicit prevents a small item
|
||||
// transaction from cloning, checksumming and writing the complete runtime
|
||||
// snapshot while it owns SQLite's mutation gate.
|
||||
type scopedRecordRef struct {
|
||||
kind string
|
||||
key string
|
||||
}
|
||||
|
||||
func (c *Component) mutateCharacterItemsScopedSync(
|
||||
ctx context.Context,
|
||||
characterID string,
|
||||
apply func(repository.InventoryRepository, repository.EquipmentRepository) error,
|
||||
) error {
|
||||
characterID = strings.TrimSpace(characterID)
|
||||
if characterID == "" || apply == nil {
|
||||
return repository.ErrCharacterItemTransactionUnavailable
|
||||
}
|
||||
return c.mutateRecordScopeSync(ctx, []scopedRecordRef{
|
||||
{kind: kindInventory, key: characterID},
|
||||
{kind: kindEquipment, key: characterID},
|
||||
}, func(group repository.Group) error {
|
||||
return group.CharacterItems.WithinCharacterItems(ctx, characterID, apply)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Component) mutateCharacterPetsScopedSync(
|
||||
ctx context.Context,
|
||||
characterID string,
|
||||
apply func(repository.InventoryRepository, repository.EquipmentRepository, repository.PetRepository) error,
|
||||
) error {
|
||||
characterID = strings.TrimSpace(characterID)
|
||||
if characterID == "" || apply == nil {
|
||||
return repository.ErrCharacterPetTransactionUnavailable
|
||||
}
|
||||
return c.mutateRecordScopeSync(ctx, []scopedRecordRef{
|
||||
{kind: kindInventory, key: characterID},
|
||||
{kind: kindEquipment, key: characterID},
|
||||
{kind: kindPet, key: characterID},
|
||||
}, func(group repository.Group) error {
|
||||
return group.CharacterPets.WithinCharacterPets(ctx, characterID, apply)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Component) mutateRentalAssetsScopedSync(
|
||||
ctx context.Context,
|
||||
accountID string,
|
||||
characterID string,
|
||||
apply func(repository.AccountRepository, repository.CharacterRepository, repository.InventoryRepository, repository.EquipmentRepository) error,
|
||||
) error {
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
characterID = strings.TrimSpace(characterID)
|
||||
if accountID == "" || characterID == "" || apply == nil {
|
||||
return repository.ErrRentalAssetTransactionUnavailable
|
||||
}
|
||||
return c.mutateRecordScopeSync(ctx, []scopedRecordRef{
|
||||
{kind: kindAccount, key: accountID},
|
||||
{kind: kindCharacter, key: characterID},
|
||||
{kind: kindInventory, key: characterID},
|
||||
{kind: kindEquipment, key: characterID},
|
||||
}, func(group repository.Group) error {
|
||||
return group.RentalAssets.WithinRentalAssets(ctx, accountID, characterID, apply)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Component) mutateRecordScopeSync(
|
||||
ctx context.Context,
|
||||
refs []scopedRecordRef,
|
||||
apply func(repository.Group) error,
|
||||
) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
refs, err := normalizeScopedRecordRefs(refs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if apply == nil {
|
||||
return fmt.Errorf("sqlite scoped mutation callback is nil")
|
||||
}
|
||||
if err := c.acquireMutation(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.releaseMutation()
|
||||
|
||||
c.mu.RLock()
|
||||
if !c.started || c.runtime == nil {
|
||||
c.mu.RUnlock()
|
||||
return ErrDatabaseRequired
|
||||
}
|
||||
group := c.runtime.Group()
|
||||
persistedBefore, err := selectScopedState(c.persisted, refs)
|
||||
dirtyBefore := c.dirty
|
||||
c.mu.RUnlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runtimeBefore, err := loadScopedRuntimeState(ctx, group, refs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apply(group); err != nil {
|
||||
return err
|
||||
}
|
||||
runtimeAfter, err := loadScopedRuntimeState(ctx, group, refs)
|
||||
if err != nil {
|
||||
return errors.Join(err, restoreScopedRuntimeState(group, refs, runtimeBefore))
|
||||
}
|
||||
if err := persistScopedStateTransition(ctx, c.db, persistedBefore, runtimeAfter); err != nil {
|
||||
return errors.Join(err, restoreScopedRuntimeState(group, refs, runtimeBefore))
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
replaceScopedState(&c.persisted, refs, runtimeAfter)
|
||||
// An earlier asynchronous mutation may still own an unrelated dirty
|
||||
// record. Never clear that signal merely because this scope is durable.
|
||||
c.dirty = dirtyBefore
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeScopedRecordRefs(refs []scopedRecordRef) ([]scopedRecordRef, error) {
|
||||
out := make([]scopedRecordRef, 0, len(refs))
|
||||
seen := make(map[string]struct{}, len(refs))
|
||||
for _, ref := range refs {
|
||||
ref.kind = strings.TrimSpace(ref.kind)
|
||||
ref.key = strings.TrimSpace(ref.key)
|
||||
if ref.key == "" {
|
||||
return nil, fmt.Errorf("sqlite scoped mutation record key is empty")
|
||||
}
|
||||
switch ref.kind {
|
||||
case kindAccount, kindCharacter, kindInventory, kindEquipment, kindPet:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported sqlite scoped mutation kind %q", ref.kind)
|
||||
}
|
||||
identity := ref.kind + "\x00" + ref.key
|
||||
if _, exists := seen[identity]; exists {
|
||||
continue
|
||||
}
|
||||
seen[identity] = struct{}{}
|
||||
out = append(out, ref)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("sqlite scoped mutation records are empty")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func loadScopedRuntimeState(ctx context.Context, group repository.Group, refs []scopedRecordRef) (memory.State, error) {
|
||||
state := memory.State{Version: memory.StateVersion}
|
||||
for _, ref := range refs {
|
||||
switch ref.kind {
|
||||
case kindAccount:
|
||||
record, found, err := group.Account.Load(ctx, ref.key)
|
||||
if err != nil {
|
||||
return memory.State{}, err
|
||||
}
|
||||
if found {
|
||||
state.Accounts = append(state.Accounts, repository.CloneAccount(record))
|
||||
}
|
||||
case kindCharacter:
|
||||
record, found, err := group.Character.Load(ctx, ref.key)
|
||||
if err != nil {
|
||||
return memory.State{}, err
|
||||
}
|
||||
if found {
|
||||
state.Characters = append(state.Characters, repository.CloneCharacter(record))
|
||||
}
|
||||
case kindInventory:
|
||||
record, found, err := group.Inventory.Load(ctx, ref.key)
|
||||
if err != nil {
|
||||
return memory.State{}, err
|
||||
}
|
||||
if found {
|
||||
state.Inventories = append(state.Inventories, repository.CloneInventory(record))
|
||||
}
|
||||
case kindEquipment:
|
||||
record, found, err := group.Equipment.Load(ctx, ref.key)
|
||||
if err != nil {
|
||||
return memory.State{}, err
|
||||
}
|
||||
if found {
|
||||
state.Equipment = append(state.Equipment, repository.CloneEquipment(record))
|
||||
}
|
||||
case kindPet:
|
||||
record, found, err := group.Pet.Load(ctx, ref.key)
|
||||
if err != nil {
|
||||
return memory.State{}, err
|
||||
}
|
||||
if found {
|
||||
state.Pets = append(state.Pets, repository.ClonePet(record))
|
||||
}
|
||||
}
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func selectScopedState(state memory.State, refs []scopedRecordRef) (memory.State, error) {
|
||||
// The persisted snapshot is immutable while the mutation gate is held. A
|
||||
// tiny memory runtime gives us the same clone and key-validation behavior
|
||||
// as live repositories without serializing unrelated records.
|
||||
temporary, err := memory.NewRuntimeFromState(context.Background(), stateForScopedRefs(state, refs))
|
||||
if err != nil {
|
||||
return memory.State{}, err
|
||||
}
|
||||
return loadScopedRuntimeState(context.Background(), temporary.Group(), refs)
|
||||
}
|
||||
|
||||
func stateForScopedRefs(state memory.State, refs []scopedRecordRef) memory.State {
|
||||
out := memory.State{Version: memory.StateVersion}
|
||||
for _, ref := range refs {
|
||||
switch ref.kind {
|
||||
case kindAccount:
|
||||
for _, record := range state.Accounts {
|
||||
if record.AccountID == ref.key {
|
||||
out.Accounts = append(out.Accounts, repository.CloneAccount(record))
|
||||
break
|
||||
}
|
||||
}
|
||||
case kindCharacter:
|
||||
for _, record := range state.Characters {
|
||||
if record.CharacterID == ref.key {
|
||||
out.Characters = append(out.Characters, repository.CloneCharacter(record))
|
||||
break
|
||||
}
|
||||
}
|
||||
case kindInventory:
|
||||
for _, record := range state.Inventories {
|
||||
if record.CharacterID == ref.key {
|
||||
out.Inventories = append(out.Inventories, repository.CloneInventory(record))
|
||||
break
|
||||
}
|
||||
}
|
||||
case kindEquipment:
|
||||
for _, record := range state.Equipment {
|
||||
if record.CharacterID == ref.key {
|
||||
out.Equipment = append(out.Equipment, repository.CloneEquipment(record))
|
||||
break
|
||||
}
|
||||
}
|
||||
case kindPet:
|
||||
for _, record := range state.Pets {
|
||||
if record.CharacterID == ref.key {
|
||||
out.Pets = append(out.Pets, repository.ClonePet(record))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func persistScopedStateTransition(ctx context.Context, database *sql.DB, before, after memory.State) error {
|
||||
oldRecords, err := stateRecords(before)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newRecords, err := stateRecords(after)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return persistRecordTransition(ctx, database, oldRecords, newRecords)
|
||||
}
|
||||
|
||||
func replaceScopedState(target *memory.State, refs []scopedRecordRef, replacement memory.State) {
|
||||
if target == nil {
|
||||
return
|
||||
}
|
||||
for _, ref := range refs {
|
||||
switch ref.kind {
|
||||
case kindAccount:
|
||||
target.Accounts = removeAccountStateRecord(target.Accounts, ref.key)
|
||||
case kindCharacter:
|
||||
target.Characters = removeCharacterStateRecord(target.Characters, ref.key)
|
||||
case kindInventory:
|
||||
target.Inventories = removeInventoryStateRecord(target.Inventories, ref.key)
|
||||
case kindEquipment:
|
||||
target.Equipment = removeEquipmentStateRecord(target.Equipment, ref.key)
|
||||
case kindPet:
|
||||
target.Pets = removePetStateRecord(target.Pets, ref.key)
|
||||
}
|
||||
}
|
||||
target.Accounts = append(target.Accounts, replacement.Accounts...)
|
||||
target.Characters = append(target.Characters, replacement.Characters...)
|
||||
target.Inventories = append(target.Inventories, replacement.Inventories...)
|
||||
target.Equipment = append(target.Equipment, replacement.Equipment...)
|
||||
target.Pets = append(target.Pets, replacement.Pets...)
|
||||
}
|
||||
|
||||
func restoreScopedRuntimeState(group repository.Group, refs []scopedRecordRef, state memory.State) error {
|
||||
var restoreErrors []error
|
||||
for _, ref := range refs {
|
||||
switch ref.kind {
|
||||
case kindAccount:
|
||||
record, found := findAccountStateRecord(state.Accounts, ref.key)
|
||||
restoreErrors = appendRestoreError(restoreErrors, restoreStoreRecord(group.Account, ref.key, record, found))
|
||||
case kindCharacter:
|
||||
record, found := findCharacterStateRecord(state.Characters, ref.key)
|
||||
restoreErrors = appendRestoreError(restoreErrors, restoreStoreRecord(group.Character, ref.key, record, found))
|
||||
case kindInventory:
|
||||
record, found := findInventoryStateRecord(state.Inventories, ref.key)
|
||||
restoreErrors = appendRestoreError(restoreErrors, restoreStoreRecord(group.Inventory, ref.key, record, found))
|
||||
case kindEquipment:
|
||||
record, found := findEquipmentStateRecord(state.Equipment, ref.key)
|
||||
restoreErrors = appendRestoreError(restoreErrors, restoreStoreRecord(group.Equipment, ref.key, record, found))
|
||||
case kindPet:
|
||||
record, found := findPetStateRecord(state.Pets, ref.key)
|
||||
restoreErrors = appendRestoreError(restoreErrors, restoreStoreRecord(group.Pet, ref.key, record, found))
|
||||
}
|
||||
}
|
||||
return errors.Join(restoreErrors...)
|
||||
}
|
||||
|
||||
type deletableStore[T any] interface {
|
||||
Save(context.Context, T) error
|
||||
Delete(context.Context, string) error
|
||||
}
|
||||
|
||||
func restoreStoreRecord[T any](store interface {
|
||||
Save(context.Context, T) error
|
||||
}, key string, record T, found bool) error {
|
||||
if found {
|
||||
return store.Save(context.Background(), record)
|
||||
}
|
||||
deletable, ok := store.(deletableStore[T])
|
||||
if !ok {
|
||||
return fmt.Errorf("sqlite scoped rollback cannot delete %q", key)
|
||||
}
|
||||
return deletable.Delete(context.Background(), key)
|
||||
}
|
||||
|
||||
func appendRestoreError(errs []error, err error) []error {
|
||||
if err != nil {
|
||||
return append(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func findAccountStateRecord(records []repository.AccountRecord, key string) (repository.AccountRecord, bool) {
|
||||
for _, record := range records {
|
||||
if record.AccountID == key {
|
||||
return repository.CloneAccount(record), true
|
||||
}
|
||||
}
|
||||
return repository.AccountRecord{}, false
|
||||
}
|
||||
|
||||
func findCharacterStateRecord(records []repository.CharacterRecord, key string) (repository.CharacterRecord, bool) {
|
||||
for _, record := range records {
|
||||
if record.CharacterID == key {
|
||||
return repository.CloneCharacter(record), true
|
||||
}
|
||||
}
|
||||
return repository.CharacterRecord{}, false
|
||||
}
|
||||
|
||||
func findInventoryStateRecord(records []repository.InventoryRecord, key string) (repository.InventoryRecord, bool) {
|
||||
for _, record := range records {
|
||||
if record.CharacterID == key {
|
||||
return repository.CloneInventory(record), true
|
||||
}
|
||||
}
|
||||
return repository.InventoryRecord{}, false
|
||||
}
|
||||
|
||||
func findEquipmentStateRecord(records []repository.EquipmentRecord, key string) (repository.EquipmentRecord, bool) {
|
||||
for _, record := range records {
|
||||
if record.CharacterID == key {
|
||||
return repository.CloneEquipment(record), true
|
||||
}
|
||||
}
|
||||
return repository.EquipmentRecord{}, false
|
||||
}
|
||||
|
||||
func findPetStateRecord(records []repository.PetRecord, key string) (repository.PetRecord, bool) {
|
||||
for _, record := range records {
|
||||
if record.CharacterID == key {
|
||||
return repository.ClonePet(record), true
|
||||
}
|
||||
}
|
||||
return repository.PetRecord{}, false
|
||||
}
|
||||
|
||||
func removeAccountStateRecord(records []repository.AccountRecord, key string) []repository.AccountRecord {
|
||||
out := records[:0]
|
||||
for _, record := range records {
|
||||
if record.AccountID != key {
|
||||
out = append(out, record)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func removeCharacterStateRecord(records []repository.CharacterRecord, key string) []repository.CharacterRecord {
|
||||
out := records[:0]
|
||||
for _, record := range records {
|
||||
if record.CharacterID != key {
|
||||
out = append(out, record)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func removeInventoryStateRecord(records []repository.InventoryRecord, key string) []repository.InventoryRecord {
|
||||
out := records[:0]
|
||||
for _, record := range records {
|
||||
if record.CharacterID != key {
|
||||
out = append(out, record)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func removeEquipmentStateRecord(records []repository.EquipmentRecord, key string) []repository.EquipmentRecord {
|
||||
out := records[:0]
|
||||
for _, record := range records {
|
||||
if record.CharacterID != key {
|
||||
out = append(out, record)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func removePetStateRecord(records []repository.PetRecord, key string) []repository.PetRecord {
|
||||
out := records[:0]
|
||||
for _, record := range records {
|
||||
if record.CharacterID != key {
|
||||
out = append(out, record)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user