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,470 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
currentUseCharacterRenameCardBodySize = 2
|
||||
currentCharacterRenameMaxNameBytes = 30
|
||||
currentCharacterRenameMinimumLevel = 30
|
||||
// currentCharacterRenameCooldown is the operator-confirmed retail rule: after a
|
||||
// rename the character cannot be renamed again for 90 days. The other half of
|
||||
// the rule is currentCharacterRenameReservation below - the *old name* stays
|
||||
// reserved for 180 days so nobody else can take it.
|
||||
//
|
||||
// It was temporarily lifted on 2026-09-23 while the arbitrary-card flow was
|
||||
// being verified (a character renamed minutes earlier could not be used to test
|
||||
// a second card) and is restored here with the rest of the flow.
|
||||
currentCharacterRenameCooldown = 90 * 24 * time.Hour
|
||||
currentCharacterRenameReservation = 180 * 24 * time.Hour
|
||||
|
||||
currentCharacterRenameEntitlementStat = "character_rename_entitlement_count"
|
||||
currentCharacterRenameLastUnixStat = "character_rename_last_unix"
|
||||
// currentCharacterRenamePendingFlag is the value the current client reads
|
||||
// in the two roster bytes between the character-name DSTR and job/grow/level.
|
||||
// The 86-generation reference server (S4A12
|
||||
// Network/Builders/Init/AccountCharacterListBodyBuilder) writes this same
|
||||
// byte twice for a character whose rename card is in effect, and the
|
||||
// selection screen then renders that card as waiting for a rename instead
|
||||
// of letting the player enter the game.
|
||||
currentCharacterRenamePendingFlag byte = 0x01
|
||||
currentCharacterRenameReservationKey = "character_rename_reservation:"
|
||||
currentCharacterRenameRosterMarker = "stdnf"
|
||||
currentCharacterRenameRosterWideUnits = 12
|
||||
|
||||
currentCharacterRenameFailureNameTooLong byte = 1
|
||||
currentCharacterRenameFailureDatabase byte = 2
|
||||
currentCharacterRenameFailureUnavailable byte = 7
|
||||
currentCharacterRenameFailureInvalidName byte = 159
|
||||
)
|
||||
|
||||
var (
|
||||
errCurrentCharacterRenameMalformed = errors.New("character rename request is malformed")
|
||||
errCurrentCharacterRenameNameTooLong = errors.New("character rename name is too long")
|
||||
errCurrentCharacterRenameNameInvalid = errors.New("character rename name is invalid")
|
||||
errCurrentCharacterRenameOwnerInvalid = errors.New("character rename owner is invalid")
|
||||
errCurrentCharacterRenameCardInvalid = errors.New("character rename card is invalid")
|
||||
errCurrentCharacterRenameEntitlement = errors.New("character rename entitlement is unavailable")
|
||||
errCurrentCharacterRenameLevel = errors.New("character rename level is too low")
|
||||
errCurrentCharacterRenameCooldownActive = errors.New("character rename cooldown is active")
|
||||
errCurrentCharacterRenameDuplicate = errors.New("character rename target already exists")
|
||||
errCurrentCharacterRenameReserved = errors.New("character rename target is reserved")
|
||||
errCurrentCharacterRenameOldNameMismatch = errors.New("character rename current name does not match")
|
||||
)
|
||||
|
||||
type currentUseCharacterRenameCardRequest struct {
|
||||
Slot int16
|
||||
}
|
||||
|
||||
type currentCharacterRenameRequest struct {
|
||||
Selector byte
|
||||
OldRaw []byte
|
||||
NewRaw []byte
|
||||
State byte
|
||||
}
|
||||
|
||||
type currentUseCharacterRenameCardResult struct {
|
||||
Update currentItemListEntry
|
||||
}
|
||||
|
||||
func parseCurrentUseCharacterRenameCard(body []byte) (currentUseCharacterRenameCardRequest, error) {
|
||||
if len(body) != currentUseCharacterRenameCardBodySize {
|
||||
return currentUseCharacterRenameCardRequest{}, errCurrentCharacterRenameMalformed
|
||||
}
|
||||
return currentUseCharacterRenameCardRequest{Slot: int16(binary.LittleEndian.Uint16(body))}, nil
|
||||
}
|
||||
|
||||
func parseCurrentCharacterRename(body []byte) (currentCharacterRenameRequest, error) {
|
||||
if len(body) < 11 {
|
||||
return currentCharacterRenameRequest{}, errCurrentCharacterRenameMalformed
|
||||
}
|
||||
request := currentCharacterRenameRequest{Selector: body[0]}
|
||||
oldRaw, next, err := readCurrentCharacterRenameDSTR(body, 1)
|
||||
if err != nil {
|
||||
return currentCharacterRenameRequest{}, err
|
||||
}
|
||||
newRaw, next, err := readCurrentCharacterRenameDSTR(body, next)
|
||||
if err != nil {
|
||||
return currentCharacterRenameRequest{}, err
|
||||
}
|
||||
if next+1 != len(body) {
|
||||
return currentCharacterRenameRequest{}, errCurrentCharacterRenameMalformed
|
||||
}
|
||||
request.OldRaw = oldRaw
|
||||
request.NewRaw = newRaw
|
||||
request.State = body[next]
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func readCurrentCharacterRenameDSTR(body []byte, offset int) ([]byte, int, error) {
|
||||
if offset < 0 || offset+4 > len(body) {
|
||||
return nil, 0, errCurrentCharacterRenameMalformed
|
||||
}
|
||||
length := uint64(binary.LittleEndian.Uint32(body[offset : offset+4]))
|
||||
if length > currentCharacterRenameMaxNameBytes {
|
||||
return nil, 0, errCurrentCharacterRenameNameTooLong
|
||||
}
|
||||
start := offset + 4
|
||||
end64 := uint64(start) + length
|
||||
if length == 0 || end64 > uint64(len(body)) {
|
||||
return nil, 0, errCurrentCharacterRenameMalformed
|
||||
}
|
||||
end := int(end64)
|
||||
return append([]byte(nil), body[start:end]...), end, nil
|
||||
}
|
||||
|
||||
func normalizeLegacyCharacterRenameBody(body []byte) []byte {
|
||||
if _, err := parseCurrentCharacterRename(body); err == nil {
|
||||
return body
|
||||
}
|
||||
const trailerSize = 4
|
||||
if len(body) <= trailerSize {
|
||||
return body
|
||||
}
|
||||
semantic := body[:len(body)-trailerSize]
|
||||
if _, err := parseCurrentCharacterRename(semantic); err != nil {
|
||||
return body
|
||||
}
|
||||
return append([]byte(nil), semantic...)
|
||||
}
|
||||
|
||||
func validateCurrentCharacterRenameName(raw []byte) (string, error) {
|
||||
if len(raw) == 0 {
|
||||
return "", errCurrentCharacterRenameNameInvalid
|
||||
}
|
||||
if len(raw) > currentCharacterRenameMaxNameBytes {
|
||||
return "", errCurrentCharacterRenameNameTooLong
|
||||
}
|
||||
name, err := decodeValidatedCharacterName(raw)
|
||||
if err != nil {
|
||||
return "", errCurrentCharacterRenameNameInvalid
|
||||
}
|
||||
encoded, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(name))
|
||||
if len(encoded) > currentCharacterRenameMaxNameBytes {
|
||||
return "", errCurrentCharacterRenameNameTooLong
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// currentCharacterRenamePending reports whether this character owns an unused
|
||||
// rename entitlement: a rename card was bought (auto-use product) or used and
|
||||
// the rename itself has not been committed yet. It is the roster projection's
|
||||
// only source of truth for the pending-rename card state.
|
||||
func currentCharacterRenamePending(character dnfrepo.CharacterRecord) bool {
|
||||
return character.Stats != nil && character.Stats[currentCharacterRenameEntitlementStat] > 0
|
||||
}
|
||||
|
||||
func characterRenameReservationMetadataKey(name string) (string, error) {
|
||||
encoded, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(name))
|
||||
if err != nil || len(encoded) == 0 || len(encoded) > currentCharacterRenameMaxNameBytes {
|
||||
return "", errCurrentCharacterRenameNameInvalid
|
||||
}
|
||||
return currentCharacterRenameReservationKey + hex.EncodeToString(encoded), nil
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentUseCharacterRenameCard(session *gameSession, body []byte) error {
|
||||
request, err := parseCurrentUseCharacterRenameCard(body)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-character-rename-card-rejected", "body_len", len(body), "reason", err)
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketUseRenameCard), currentCharacterRenameFailureUnavailable)
|
||||
}
|
||||
catalog, err := s.currentCeraShopCatalog()
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketUseRenameCard), currentCharacterRenameFailureDatabase)
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.CeraShopAssets == nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketUseRenameCard), currentCharacterRenameFailureDatabase)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
s.ceraShopPurchaseMu.Lock()
|
||||
result, err := commitCurrentUseCharacterRenameCard(
|
||||
ctx, repositories, catalog,
|
||||
strings.TrimSpace(s.accountIDForSession(session)),
|
||||
selectedCharacterIDString(session), request, s.gameplayNow().UTC(),
|
||||
)
|
||||
s.ceraShopPurchaseMu.Unlock()
|
||||
if err != nil {
|
||||
code := currentCharacterRenameFailureDatabase
|
||||
if errors.Is(err, errCurrentCharacterRenameCardInvalid) ||
|
||||
errors.Is(err, errCurrentCharacterRenameOwnerInvalid) ||
|
||||
errors.Is(err, errCurrentCharacterRenameEntitlement) ||
|
||||
errors.Is(err, errCurrentCharacterRenameLevel) ||
|
||||
errors.Is(err, errCurrentCharacterRenameCooldownActive) {
|
||||
code = currentCharacterRenameFailureUnavailable
|
||||
}
|
||||
s.logGameEvent(session, "game-character-rename-card-rejected", "slot", request.Slot, "reason", err)
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketUseRenameCard), code)
|
||||
}
|
||||
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), buildCurrentItemUpdateBody(dnfrepo.MainInventoryListType, []currentItemListEntry{result.Update}), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketUseRenameCard), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-character-rename-card-accepted",
|
||||
"slot", request.Slot,
|
||||
"sequence", "item_update_sent_ack_sent_then_server_owned_return_to_select")
|
||||
// The card is consumed and the pending-rename entitlement is stored; the client
|
||||
// does not return to the character-select screen on its own, so the server does
|
||||
// it here, exactly like the 86-generation reference server does after
|
||||
// USE_RENAME_CARD.
|
||||
if err := s.returnToCharacterSelectAfterRenameCard(session); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func commitCurrentUseCharacterRenameCard(
|
||||
ctx context.Context,
|
||||
repositories dnfrepo.Group,
|
||||
catalog *pvfCeraShopCatalog,
|
||||
accountID string,
|
||||
characterID string,
|
||||
request currentUseCharacterRenameCardRequest,
|
||||
now time.Time,
|
||||
) (currentUseCharacterRenameCardResult, error) {
|
||||
if repositories.CeraShopAssets == nil || catalog == nil || catalog.items == nil || accountID == "" || characterID == "" || request.Slot < 0 {
|
||||
return currentUseCharacterRenameCardResult{}, errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
var result currentUseCharacterRenameCardResult
|
||||
err := repositories.CeraShopAssets.WithinCeraShopAssets(
|
||||
ctx, accountID, characterID, dnfrepo.CharacterContainerStateScope(characterID),
|
||||
func(accounts dnfrepo.AccountRepository, _ dnfrepo.AccountInventoryRepository, characters dnfrepo.CharacterRepository, inventories dnfrepo.InventoryRepository, _ dnfrepo.EquipmentRepository, _ dnfrepo.SettingsRepository) error {
|
||||
account, found, err := accounts.Load(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
character, found, err := characters.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || strings.TrimSpace(character.AccountID) != accountID || strings.TrimSpace(account.AccountID) != accountID {
|
||||
return errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
if character.Level < currentCharacterRenameMinimumLevel {
|
||||
return errCurrentCharacterRenameLevel
|
||||
}
|
||||
if character.Stats != nil && character.Stats[currentCharacterRenameEntitlementStat] != 0 {
|
||||
return errCurrentCharacterRenameEntitlement
|
||||
}
|
||||
// Arming the pending rename. The 90-day cooldown is checked here as well
|
||||
// as on the commit path: a player inside the window must not even be
|
||||
// able to consume a card, because the card is gone either way.
|
||||
lastRename := int64(0)
|
||||
if character.Stats != nil {
|
||||
lastRename = character.Stats[currentCharacterRenameLastUnixStat]
|
||||
}
|
||||
if lastRename < 0 || lastRename > 0 && now.Unix() < lastRename+int64(currentCharacterRenameCooldown/time.Second) {
|
||||
return errCurrentCharacterRenameCooldownActive
|
||||
}
|
||||
inventory, found, err := inventories.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || strings.TrimSpace(inventory.CharacterID) != characterID {
|
||||
return errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
key := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, request.Slot)
|
||||
stack, found := inventory.Slots[key]
|
||||
if !found || stack.ItemID <= 0 || stack.ItemID > math.MaxUint32 || stack.Count <= 0 {
|
||||
return errCurrentCharacterRenameCardInvalid
|
||||
}
|
||||
definition, err := catalog.items.ResolveItem(uint32(stack.ItemID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: item=%d: %v", errCurrentCharacterRenameCardInvalid, stack.ItemID, err)
|
||||
}
|
||||
if _, card := catalog.characterRenameCardKind(definition); !card {
|
||||
return fmt.Errorf("%w: item=%d path=%s", errCurrentCharacterRenameCardInvalid, stack.ItemID, definition.PVFPath)
|
||||
}
|
||||
stack.Count--
|
||||
if stack.Count == 0 {
|
||||
delete(inventory.Slots, key)
|
||||
result.Update.patchCore(request.Slot, math.MaxUint32, 0)
|
||||
} else {
|
||||
result.Update = currentItemListEntryFromStack(dnfrepo.MainInventoryListType, request.Slot, stack)
|
||||
stack.RawEntry = append([]byte(nil), result.Update.data[:]...)
|
||||
inventory.Slots[key] = stack
|
||||
}
|
||||
if character.Stats == nil {
|
||||
character.Stats = make(map[string]int64, 4)
|
||||
}
|
||||
character.Stats[currentCharacterRenameEntitlementStat] = 1
|
||||
character.UpdatedAt = now
|
||||
inventory.UpdatedAt = now
|
||||
if err := dnfrepo.SaveCharacterFields(ctx, characters, character, dnfrepo.CharacterFieldStats); err != nil {
|
||||
return err
|
||||
}
|
||||
return dnfrepo.SaveInventoryFields(ctx, inventories, inventory, dnfrepo.InventoryFieldSlots)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return currentUseCharacterRenameCardResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentChangeCharacterName(session *gameSession, body []byte) error {
|
||||
request, err := parseCurrentCharacterRename(body)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketChangeCharacName), currentCharacterRenameFailureCode(err))
|
||||
}
|
||||
oldName, err := validateCurrentCharacterRenameName(request.OldRaw)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketChangeCharacName), currentCharacterRenameFailureCode(err))
|
||||
}
|
||||
newName, err := validateCurrentCharacterRenameName(request.NewRaw)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketChangeCharacName), currentCharacterRenameFailureCode(err))
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.CeraShopAssets == nil || repositories.Character == nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketChangeCharacName), currentCharacterRenameFailureDatabase)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
targetCharacterID, err := currentCharacterRenameTargetID(ctx, repositories.Character, accountID, request.Selector)
|
||||
if err != nil {
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketChangeCharacName), currentCharacterRenameFailureCode(err))
|
||||
}
|
||||
s.ceraShopPurchaseMu.Lock()
|
||||
err = commitCurrentCharacterRename(ctx, repositories, accountID, targetCharacterID, oldName, newName, s.currentRegionID(), s.gameplayNow().UTC())
|
||||
s.ceraShopPurchaseMu.Unlock()
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-character-rename-rejected", "old_name", oldName, "new_name", newName, "selector", request.Selector, "state", request.State, "reason", err)
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketChangeCharacName), currentCharacterRenameFailureCode(err))
|
||||
}
|
||||
s.logGameEvent(session, "game-character-rename-committed", "old_name", oldName, "new_name", newName, "selector", request.Selector, "state", request.State)
|
||||
return s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketChangeCharacName), nil)
|
||||
}
|
||||
|
||||
func currentCharacterRenameTargetID(ctx context.Context, repository dnfrepo.CharacterRepository, accountID string, selector byte) (string, error) {
|
||||
if repository == nil || strings.TrimSpace(accountID) == "" {
|
||||
return "", errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
characters, err := repository.ListByAccount(ctx, accountID, dnfrepo.MaxCharacterSlotLimit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, character := range characters {
|
||||
if character.Slot == int(selector) && strings.TrimSpace(character.AccountID) == accountID && strings.TrimSpace(character.CharacterID) != "" {
|
||||
return strings.TrimSpace(character.CharacterID), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("%w: selector=%d", errCurrentCharacterRenameOwnerInvalid, selector)
|
||||
}
|
||||
|
||||
func commitCurrentCharacterRename(ctx context.Context, repositories dnfrepo.Group, accountID, characterID, oldName, newName string, serverID byte, now time.Time) error {
|
||||
if repositories.CeraShopAssets == nil || accountID == "" || characterID == "" || oldName == "" || newName == "" {
|
||||
return errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
return repositories.CeraShopAssets.WithinCeraShopAssets(
|
||||
ctx, accountID, characterID, dnfrepo.CharacterContainerStateScope(characterID),
|
||||
func(accounts dnfrepo.AccountRepository, _ dnfrepo.AccountInventoryRepository, characters dnfrepo.CharacterRepository, _ dnfrepo.InventoryRepository, _ dnfrepo.EquipmentRepository, _ dnfrepo.SettingsRepository) error {
|
||||
account, found, err := accounts.Load(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || strings.TrimSpace(account.AccountID) != accountID {
|
||||
return errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
character, found, err := characters.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || strings.TrimSpace(character.AccountID) != accountID {
|
||||
return errCurrentCharacterRenameOwnerInvalid
|
||||
}
|
||||
canonicalOldName := character.Name
|
||||
if oldName != canonicalOldName && oldName != currentCharacterRenameRosterName(character) {
|
||||
return errCurrentCharacterRenameOldNameMismatch
|
||||
}
|
||||
if character.Level < currentCharacterRenameMinimumLevel {
|
||||
return errCurrentCharacterRenameLevel
|
||||
}
|
||||
if character.Stats == nil || character.Stats[currentCharacterRenameEntitlementStat] <= 0 {
|
||||
return errCurrentCharacterRenameEntitlement
|
||||
}
|
||||
nowUnix := now.Unix()
|
||||
lastRename := character.Stats[currentCharacterRenameLastUnixStat]
|
||||
if lastRename > 0 && nowUnix < lastRename+int64(currentCharacterRenameCooldown/time.Second) {
|
||||
return errCurrentCharacterRenameCooldownActive
|
||||
}
|
||||
if existingID, duplicate, err := characters.FindIDByName(ctx, newName); err != nil {
|
||||
return err
|
||||
} else if duplicate {
|
||||
return fmt.Errorf("%w: character=%s", errCurrentCharacterRenameDuplicate, existingID)
|
||||
}
|
||||
newNameReservationKey, err := characterRenameReservationMetadataKey(newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reservedUntil, parseErr := strconv.ParseInt(strings.TrimSpace(account.Metadata[newNameReservationKey]), 10, 64); parseErr == nil && reservedUntil > nowUnix {
|
||||
return errCurrentCharacterRenameReserved
|
||||
}
|
||||
oldNameReservationKey, err := characterRenameReservationMetadataKey(canonicalOldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.Metadata == nil {
|
||||
account.Metadata = make(map[string]string, 4)
|
||||
}
|
||||
account.Metadata[oldNameReservationKey] = strconv.FormatInt(now.Add(currentCharacterRenameReservation).Unix(), 10)
|
||||
// Record the rename for the 角色名称变更记录 window. It rides the
|
||||
// account record this transaction already saves, so a rolled-back
|
||||
// rename cannot leave a dangling record behind.
|
||||
appendCurrentCharacterRenameLog(&account, characterID, canonicalOldName, serverID, now)
|
||||
character.Name = newName
|
||||
character.Stats[currentCharacterRenameEntitlementStat] = 0
|
||||
character.Stats[currentCharacterRenameLastUnixStat] = nowUnix
|
||||
account.UpdatedAt = now
|
||||
character.UpdatedAt = now
|
||||
if err := accounts.Save(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
return dnfrepo.SaveCharacterFields(ctx, characters, character, dnfrepo.CharacterFieldBase, dnfrepo.CharacterFieldStats)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func currentCharacterRenameFailureCode(err error) byte {
|
||||
switch {
|
||||
case errors.Is(err, errCurrentCharacterRenameNameTooLong):
|
||||
return currentCharacterRenameFailureNameTooLong
|
||||
case errors.Is(err, errCurrentCharacterRenameNameInvalid), errors.Is(err, errCurrentCharacterRenameMalformed):
|
||||
return currentCharacterRenameFailureInvalidName
|
||||
case errors.Is(err, errCurrentCharacterRenameDuplicate), errors.Is(err, errCurrentCharacterRenameReserved),
|
||||
errors.Is(err, errCurrentCharacterRenameOwnerInvalid), errors.Is(err, errCurrentCharacterRenameEntitlement),
|
||||
errors.Is(err, errCurrentCharacterRenameLevel), errors.Is(err, errCurrentCharacterRenameCooldownActive),
|
||||
errors.Is(err, errCurrentCharacterRenameOldNameMismatch):
|
||||
return currentCharacterRenameFailureUnavailable
|
||||
default:
|
||||
return currentCharacterRenameFailureDatabase
|
||||
}
|
||||
}
|
||||
|
||||
func selectedCharacterIDString(session *gameSession) string {
|
||||
if session == nil || session.selectedCharacterID == 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
}
|
||||
Reference in new issue
Block a user