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

327 lines
10 KiB
Go

package mysql
import (
"context"
"database/sql"
"fmt"
"longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/platform/db"
"sort"
"strings"
"time"
)
const mysqlQuestTable = "quests"
type mysqlQuestStore struct {
mysqlStoreBase
}
func (s *mysqlQuestStore) Load(ctx context.Context, characterID string) (repository.QuestRecord, bool, error) {
table, err := s.router.readTable(mysqlQuestTable, characterID)
if err != nil {
return repository.QuestRecord{}, false, err
}
statesTable, err := s.router.readTable(mysqlQuestStatesTable, characterID)
if err != nil {
return repository.QuestRecord{}, false, err
}
extraTable, err := s.router.readTable(mysqlQuestExtraTable, characterID)
if err != nil {
return repository.QuestRecord{}, false, err
}
query := s.router.selectQuery("SELECT character_id, updated_at FROM " + table + " WHERE character_id = ?")
var record repository.QuestRecord
var updatedAt sql.NullTime
err = s.router.db.QueryRowContext(ctx, query, characterID).Scan(
&record.CharacterID,
&updatedAt,
)
if err != nil {
ok, scanErr := scanErr(err)
return repository.QuestRecord{}, ok, scanErr
}
record.States, err = loadQuestStates(ctx, s.router, statesTable, extraTable, characterID, "states")
if err != nil {
return repository.QuestRecord{}, false, err
}
record.Progress, err = loadQuestStates(ctx, s.router, statesTable, extraTable, characterID, "progress")
if err != nil {
return repository.QuestRecord{}, false, err
}
record.UpdatedAt = scanTime(updatedAt)
return repository.CloneQuest(record), true, nil
}
func (s *mysqlQuestStore) Save(ctx context.Context, record repository.QuestRecord) error {
return s.SaveFields(ctx, record, repository.AllQuestFields()...)
}
func (s *mysqlQuestStore) SaveFields(ctx context.Context, record repository.QuestRecord, fields ...repository.QuestField) error {
characterID, err := requireRecordKey(repository.QuestKey, record, "quest")
if err != nil {
return err
}
fields = repository.QuestFields.Normalize(fields)
if len(fields) == 0 {
return nil
}
table, err := s.router.writeTable(mysqlQuestTable, characterID)
if err != nil {
return err
}
statesTable, err := s.router.writeTable(mysqlQuestStatesTable, characterID)
if err != nil {
return err
}
extraTable, err := s.router.writeTable(mysqlQuestExtraTable, characterID)
if err != nil {
return err
}
columns := []string{"character_id", "updated_at"}
args := []any{characterID, timeOrNow(record.UpdatedAt, s.router.now)}
updates := []string{updateValue("updated_at")}
return withMySQLWriteExecutor(ctx, s.router.db, func(database SQLDB) error {
if _, execErr := database.ExecContext(ctx, buildUpsert(table, columns, updates), args...); execErr != nil {
return execErr
}
for _, field := range fields {
switch field {
case repository.QuestFieldStates:
if err := replaceQuestStates(ctx, database, statesTable, extraTable, characterID, "states", record.States); err != nil {
return err
}
case repository.QuestFieldProgress:
if err := replaceQuestStates(ctx, database, statesTable, extraTable, characterID, "progress", record.Progress); err != nil {
return err
}
}
}
return nil
})
}
// SaveQuestStateEntry updates one objective row and its extension fields.
// SET_TRIGGER is frequent; replacing every historical objective on each
// packet makes latency grow with the lifetime quest count.
func (s *mysqlQuestStore) 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
}
var stateGroup string
switch field {
case repository.QuestFieldStates:
stateGroup = "states"
case repository.QuestFieldProgress:
stateGroup = "progress"
default:
return fmt.Errorf("unsupported quest state field %q", field)
}
table, err := s.router.writeTable(mysqlQuestTable, characterID)
if err != nil {
return err
}
statesTable, err := s.router.writeTable(mysqlQuestStatesTable, characterID)
if err != nil {
return err
}
extraTable, err := s.router.writeTable(mysqlQuestExtraTable, characterID)
if err != nil {
return err
}
stateColumns := []string{
"character_id", "state_group", "quest_id", "status", "trigger_type",
"progress_value", "reward_select_index", "multiplier", "state_updated_at",
}
stateUpdates := make([]string, 0, len(stateColumns)-3)
for _, column := range stateColumns[3:] {
stateUpdates = append(stateUpdates, updateValue(column))
}
return withMySQLWriteExecutor(ctx, s.router.db, func(database SQLDB) error {
if _, execErr := database.ExecContext(
ctx,
buildUpsert(table, []string{"character_id", "updated_at"}, []string{updateValue("updated_at")}),
characterID,
timeOrNow(updatedAt, s.router.now),
); execErr != nil {
return execErr
}
if _, execErr := database.ExecContext(
ctx,
buildUpsert(statesTable, stateColumns, stateUpdates),
characterID, stateGroup, questID, state.Status, state.TriggerType,
state.ProgressValue, state.RewardSelectIndex, state.Multiplier,
sqlTime(state.UpdatedAt),
); execErr != nil {
return execErr
}
if _, execErr := database.ExecContext(
ctx,
"DELETE FROM "+extraTable+" WHERE character_id = ? AND state_group = ? AND quest_id = ?",
characterID, stateGroup, questID,
); execErr != nil {
return execErr
}
extraColumns := []string{"character_id", "state_group", "quest_id", "extra_key", "extra_value"}
extraKeys := sortedStringKeys(state.Extra)
for start := 0; start < len(extraKeys); start += mysqlReplaceBatchSize {
end := min(start+mysqlReplaceBatchSize, len(extraKeys))
chunk := extraKeys[start:end]
args := make([]any, 0, len(chunk)*len(extraColumns))
for _, key := range chunk {
args = append(args, characterID, stateGroup, questID, key, state.Extra[key])
}
if _, execErr := database.ExecContext(
ctx,
buildMySQLBulkInsert(extraTable, extraColumns, len(chunk)),
args...,
); execErr != nil {
return execErr
}
}
return nil
})
}
func loadQuestStates(
ctx context.Context,
router mysqlRouter,
statesTable, extraTable, characterID, stateGroup string,
) (map[int64]repository.QuestState, error) {
query := router.selectQuery("SELECT quest_id, status, trigger_type, progress_value, reward_select_index, multiplier, state_updated_at FROM " + statesTable + " WHERE character_id = ? AND state_group = ? ORDER BY quest_id")
rows, err := router.db.QueryContext(ctx, query, characterID, stateGroup)
if err != nil {
return nil, err
}
var states map[int64]repository.QuestState
for rows.Next() {
var questID int64
var state repository.QuestState
var updatedAt sql.NullTime
if err := rows.Scan(
&questID,
&state.Status,
&state.TriggerType,
&state.ProgressValue,
&state.RewardSelectIndex,
&state.Multiplier,
&updatedAt,
); err != nil {
rows.Close()
return nil, err
}
state.UpdatedAt = scanTime(updatedAt)
if states == nil {
states = make(map[int64]repository.QuestState)
}
states[questID] = state
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
extras, err := router.db.QueryContext(
ctx,
router.selectQuery("SELECT quest_id, extra_key, extra_value FROM "+extraTable+" WHERE character_id = ? AND state_group = ? ORDER BY quest_id, extra_key"),
characterID,
stateGroup,
)
if err != nil {
return nil, err
}
defer extras.Close()
for extras.Next() {
var questID int64
var key, value string
if err := extras.Scan(&questID, &key, &value); err != nil {
return nil, err
}
state, ok := states[questID]
if !ok {
continue
}
if state.Extra == nil {
state.Extra = make(map[string]string)
}
state.Extra[key] = value
states[questID] = state
}
if err := extras.Err(); err != nil {
return nil, err
}
return states, nil
}
func replaceQuestStates(
ctx context.Context,
database SQLDB,
statesTable, extraTable, characterID, stateGroup string,
states map[int64]repository.QuestState,
) error {
if _, err := database.ExecContext(ctx, "DELETE FROM "+extraTable+" WHERE character_id = ? AND state_group = ?", characterID, stateGroup); err != nil {
return err
}
if _, err := database.ExecContext(ctx, "DELETE FROM "+statesTable+" WHERE character_id = ? AND state_group = ?", characterID, stateGroup); err != nil {
return err
}
stateQuery := "INSERT INTO " + statesTable + " (character_id, state_group, quest_id, status, trigger_type, progress_value, reward_select_index, multiplier, state_updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
extraQuery := "INSERT INTO " + extraTable + " (character_id, state_group, quest_id, extra_key, extra_value) VALUES (?, ?, ?, ?, ?)"
questIDs := make([]int64, 0, len(states))
for questID := range states {
questIDs = append(questIDs, questID)
}
sort.Slice(questIDs, func(left, right int) bool { return questIDs[left] < questIDs[right] })
const batchSize = 128
for start := 0; start < len(questIDs); start += batchSize {
end := start + batchSize
if end > len(questIDs) {
end = len(questIDs)
}
stateArgs := make([]any, 0, (end-start)*9)
stateValues := make([]string, 0, end-start)
extraArgs := make([]any, 0, (end-start)*5)
extraValues := make([]string, 0, end-start)
for _, questID := range questIDs[start:end] {
state := states[questID]
stateValues = append(stateValues, "(?, ?, ?, ?, ?, ?, ?, ?, ?)")
stateArgs = append(stateArgs, characterID, stateGroup, questID, state.Status,
state.TriggerType, state.ProgressValue, state.RewardSelectIndex,
state.Multiplier, sqlTime(state.UpdatedAt))
for _, key := range sortedStringKeys(state.Extra) {
extraValues = append(extraValues, "(?, ?, ?, ?, ?)")
extraArgs = append(extraArgs, characterID, stateGroup, questID, key, state.Extra[key])
}
}
if len(stateValues) > 0 {
if _, err := database.ExecContext(ctx, stateQueryWithValues(stateQuery, stateValues), stateArgs...); err != nil {
return err
}
}
if len(extraValues) > 0 {
if _, err := database.ExecContext(ctx, extraQueryWithValues(extraQuery, extraValues), extraArgs...); err != nil {
return err
}
}
}
return nil
}
func stateQueryWithValues(query string, values []string) string {
return strings.Replace(query, "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", "VALUES "+strings.Join(values, ", "), 1)
}
func extraQueryWithValues(query string, values []string) string {
return strings.Replace(query, "VALUES (?, ?, ?, ?, ?)", "VALUES "+strings.Join(values, ", "), 1)
}