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,436 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
const (
|
||||
kindAccount = "account"
|
||||
kindAccountInventory = "account_inventory"
|
||||
kindCharacter = "character"
|
||||
kindInventory = "inventory"
|
||||
kindEquipment = "equipment"
|
||||
kindPet = "pet"
|
||||
kindQuest = "quest"
|
||||
kindSkill = "skill"
|
||||
kindDungeonPermission = "dungeon_permission"
|
||||
kindHellPartyPermission = "hell_party_permission"
|
||||
kindPacketTemplate = "packet_template"
|
||||
kindSettings = "settings"
|
||||
kindMailbox = "mailbox"
|
||||
kindEpicBook = "epic_book"
|
||||
kindUnitedFriend = "united_friend"
|
||||
)
|
||||
|
||||
type persistedRecord struct {
|
||||
kind string
|
||||
key string
|
||||
payload []byte
|
||||
checksum string
|
||||
}
|
||||
|
||||
func stateRecords(state memory.State) (map[string]persistedRecord, error) {
|
||||
out := make(map[string]persistedRecord)
|
||||
add := func(kind, key string, value any) error {
|
||||
record, err := makePersistedRecord(kind, key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out[record.kind+"\x00"+record.key] = record
|
||||
return nil
|
||||
}
|
||||
for _, record := range state.Accounts {
|
||||
if err := add(kindAccount, record.AccountID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.AccountInventories {
|
||||
if err := add(kindAccountInventory, record.AccountID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Characters {
|
||||
if err := add(kindCharacter, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Inventories {
|
||||
if err := add(kindInventory, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Equipment {
|
||||
if err := add(kindEquipment, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Pets {
|
||||
if err := add(kindPet, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Quests {
|
||||
if err := add(kindQuest, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Skills {
|
||||
if err := add(kindSkill, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.DungeonPermissions {
|
||||
if err := add(kindDungeonPermission, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.HellPartyPermissions {
|
||||
if err := add(kindHellPartyPermission, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.PacketTemplates {
|
||||
if err := add(kindPacketTemplate, record.TemplateID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Settings {
|
||||
if err := add(kindSettings, record.Scope, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.Mailboxes {
|
||||
if err := add(kindMailbox, record.CharacterID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.EpicBooks {
|
||||
if err := add(kindEpicBook, record.AccountID, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, record := range state.UnitedFriends {
|
||||
if err := add(kindUnitedFriend, record.OwnerName, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func makePersistedRecord(kind, key string, value any) (persistedRecord, error) {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return persistedRecord{}, fmt.Errorf("sqlite repository %s record key is empty", kind)
|
||||
}
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return persistedRecord{}, fmt.Errorf("marshal sqlite repository %s %q: %w", kind, key, err)
|
||||
}
|
||||
sum := sha256.Sum256(payload)
|
||||
return persistedRecord{
|
||||
kind: kind,
|
||||
key: key,
|
||||
payload: payload,
|
||||
checksum: hex.EncodeToString(sum[:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadState(ctx context.Context, database *sql.DB) (memory.State, error) {
|
||||
state := memory.State{Version: memory.StateVersion}
|
||||
var versionText string
|
||||
err := database.QueryRowContext(ctx, `SELECT meta_value FROM dnf_repository_meta WHERE meta_key = 'state_version'`).Scan(&versionText)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return memory.State{}, fmt.Errorf("read sqlite repository state version: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
version, parseErr := strconv.Atoi(versionText)
|
||||
if parseErr != nil || version != memory.StateVersion {
|
||||
return memory.State{}, fmt.Errorf("unsupported sqlite repository state version %q", versionText)
|
||||
}
|
||||
state.Version = version
|
||||
}
|
||||
rows, err := database.QueryContext(ctx, `SELECT record_kind, record_key, payload, checksum FROM dnf_repository_records ORDER BY record_kind, record_key`)
|
||||
if err != nil {
|
||||
return memory.State{}, fmt.Errorf("query sqlite repository records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var kind, key, wantChecksum string
|
||||
var payload []byte
|
||||
if err := rows.Scan(&kind, &key, &payload, &wantChecksum); err != nil {
|
||||
return memory.State{}, fmt.Errorf("scan sqlite repository record: %w", err)
|
||||
}
|
||||
sum := sha256.Sum256(payload)
|
||||
if got := hex.EncodeToString(sum[:]); !strings.EqualFold(got, wantChecksum) {
|
||||
return memory.State{}, fmt.Errorf("sqlite repository checksum mismatch for %s %q", kind, key)
|
||||
}
|
||||
if err := appendStateRecord(&state, kind, key, payload); err != nil {
|
||||
return memory.State{}, err
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return memory.State{}, fmt.Errorf("iterate sqlite repository records: %w", err)
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func appendStateRecord(state *memory.State, kind, key string, payload []byte) error {
|
||||
decode := func(target any) error {
|
||||
if err := json.Unmarshal(payload, target); err != nil {
|
||||
return fmt.Errorf("decode sqlite repository %s %q: %w", kind, key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch kind {
|
||||
case kindAccount:
|
||||
var record repository.AccountRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Accounts = append(state.Accounts, record)
|
||||
case kindAccountInventory:
|
||||
var record repository.AccountInventoryRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.AccountInventories = append(state.AccountInventories, record)
|
||||
case kindCharacter:
|
||||
var record repository.CharacterRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Characters = append(state.Characters, record)
|
||||
case kindInventory:
|
||||
var record repository.InventoryRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Inventories = append(state.Inventories, record)
|
||||
case kindEquipment:
|
||||
var record repository.EquipmentRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Equipment = append(state.Equipment, record)
|
||||
case kindPet:
|
||||
var record repository.PetRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Pets = append(state.Pets, record)
|
||||
case kindQuest:
|
||||
var record repository.QuestRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Quests = append(state.Quests, record)
|
||||
case kindSkill:
|
||||
var record repository.SkillRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Skills = append(state.Skills, record)
|
||||
case kindDungeonPermission:
|
||||
var record repository.DungeonPermissionRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.DungeonPermissions = append(state.DungeonPermissions, record)
|
||||
case kindHellPartyPermission:
|
||||
var record repository.HellPartyPermissionRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.HellPartyPermissions = append(state.HellPartyPermissions, record)
|
||||
case kindPacketTemplate:
|
||||
var record repository.PacketTemplateRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.PacketTemplates = append(state.PacketTemplates, record)
|
||||
case kindSettings:
|
||||
var record repository.SettingsRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Settings = append(state.Settings, record)
|
||||
case kindMailbox:
|
||||
var record repository.MailboxRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.Mailboxes = append(state.Mailboxes, record)
|
||||
case kindEpicBook:
|
||||
var record repository.EpicBookRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.EpicBooks = append(state.EpicBooks, record)
|
||||
case kindUnitedFriend:
|
||||
var record repository.UnitedFriendRecord
|
||||
if err := decode(&record); err != nil {
|
||||
return err
|
||||
}
|
||||
state.UnitedFriends = append(state.UnitedFriends, record)
|
||||
default:
|
||||
return fmt.Errorf("unsupported sqlite repository record kind %q", kind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func persistTransition(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)
|
||||
}
|
||||
|
||||
// persistCharacterAssetsTransition writes only the durable records covered by
|
||||
// CharacterAssetUnitOfWork. The old side intentionally comes from the last
|
||||
// persisted state rather than the live runtime: an earlier async mutation may
|
||||
// still be dirty, and this synchronous asset commit must include the current
|
||||
// character-owned values without flushing unrelated records.
|
||||
func persistCharacterAssetsTransition(
|
||||
ctx context.Context,
|
||||
database *sql.DB,
|
||||
persisted memory.State,
|
||||
after characterAssetsSnapshot,
|
||||
) error {
|
||||
oldRecords, err := characterAssetsStateRecords(persisted, after.character.CharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newRecords, err := characterAssetsSnapshotRecords(after)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return persistRecordTransition(ctx, database, oldRecords, newRecords)
|
||||
}
|
||||
|
||||
func characterAssetsStateRecords(state memory.State, characterID string) (map[string]persistedRecord, error) {
|
||||
characterID = strings.TrimSpace(characterID)
|
||||
if characterID == "" {
|
||||
return nil, fmt.Errorf("sqlite character-asset record key is empty")
|
||||
}
|
||||
out := make(map[string]persistedRecord, 3)
|
||||
for _, record := range state.Characters {
|
||||
if record.CharacterID != characterID {
|
||||
continue
|
||||
}
|
||||
persisted, err := makePersistedRecord(kindCharacter, record.CharacterID, record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[persisted.kind+"\x00"+persisted.key] = persisted
|
||||
break
|
||||
}
|
||||
for _, record := range state.Inventories {
|
||||
if record.CharacterID != characterID {
|
||||
continue
|
||||
}
|
||||
persisted, err := makePersistedRecord(kindInventory, record.CharacterID, record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[persisted.kind+"\x00"+persisted.key] = persisted
|
||||
break
|
||||
}
|
||||
for _, record := range state.Equipment {
|
||||
if record.CharacterID != characterID {
|
||||
continue
|
||||
}
|
||||
persisted, err := makePersistedRecord(kindEquipment, record.CharacterID, record)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[persisted.kind+"\x00"+persisted.key] = persisted
|
||||
break
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func characterAssetsSnapshotRecords(snapshot characterAssetsSnapshot) (map[string]persistedRecord, error) {
|
||||
out := make(map[string]persistedRecord, 3)
|
||||
appendRecord := func(kind string, found bool, key string, value any) error {
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
persisted, err := makePersistedRecord(kind, key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out[persisted.kind+"\x00"+persisted.key] = persisted
|
||||
return nil
|
||||
}
|
||||
if err := appendRecord(kindCharacter, snapshot.characterFound, snapshot.character.CharacterID, snapshot.character); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := appendRecord(kindInventory, snapshot.inventoryFound, snapshot.inventory.CharacterID, snapshot.inventory); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := appendRecord(kindEquipment, snapshot.equipmentFound, snapshot.equipment.CharacterID, snapshot.equipment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func persistRecordTransition(
|
||||
ctx context.Context,
|
||||
database *sql.DB,
|
||||
oldRecords map[string]persistedRecord,
|
||||
newRecords map[string]persistedRecord,
|
||||
) error {
|
||||
tx, err := database.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin sqlite repository transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
for id, oldRecord := range oldRecords {
|
||||
if _, ok := newRecords[id]; ok {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM dnf_repository_records WHERE record_kind = ? AND record_key = ?`, oldRecord.kind, oldRecord.key); err != nil {
|
||||
return fmt.Errorf("delete sqlite repository %s %q: %w", oldRecord.kind, oldRecord.key, err)
|
||||
}
|
||||
}
|
||||
updatedAt := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
for id, record := range newRecords {
|
||||
if oldRecord, ok := oldRecords[id]; ok && oldRecord.checksum == record.checksum {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO dnf_repository_records (record_kind, record_key, payload, checksum, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(record_kind, record_key) DO UPDATE SET
|
||||
payload = excluded.payload,
|
||||
checksum = excluded.checksum,
|
||||
updated_at = excluded.updated_at`, record.kind, record.key, record.payload, record.checksum, updatedAt); err != nil {
|
||||
return fmt.Errorf("upsert sqlite repository %s %q: %w", record.kind, record.key, err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO dnf_repository_meta (meta_key, meta_value) VALUES ('state_version', ?)
|
||||
ON CONFLICT(meta_key) DO UPDATE SET meta_value = excluded.meta_value`, strconv.Itoa(memory.StateVersion)); err != nil {
|
||||
return fmt.Errorf("write sqlite repository state version: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit sqlite repository transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user