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,298 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/premium"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
func currentPremiumInventoryContracts(catalog *currentPremiumCatalog) map[int64]premium.InventoryContract {
|
||||
if catalog == nil || len(catalog.contractsByItem) == 0 {
|
||||
return nil
|
||||
}
|
||||
contracts := make(map[int64]premium.InventoryContract, len(catalog.contractsByItem))
|
||||
for itemID, info := range catalog.contractsByItem {
|
||||
contracts[itemID] = premium.InventoryContract{
|
||||
ItemID: info.ItemID,
|
||||
PremiumType: premium.CanonicalType(info.PremiumType),
|
||||
DurationSeconds: info.DurationSeconds,
|
||||
}
|
||||
}
|
||||
return contracts
|
||||
}
|
||||
|
||||
// activateCurrentPremiumInventoryContracts is the common acquisition
|
||||
// boundary for premiumlist contract items. The grant transaction may briefly
|
||||
// persist a recoverable item row, but before any item-list projection reaches
|
||||
// the client this transaction atomically removes every list-0 contract stack
|
||||
// and extends the account entitlement by PVF duration * stack count.
|
||||
func (s *Service) activateCurrentPremiumInventoryContracts(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
) (premium.InventoryActivationResult, error) {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 {
|
||||
return premium.InventoryActivationResult{}, fmt.Errorf("premium inventory activation requires a selected character")
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.RentalAssets == nil || repositories.Inventory == nil {
|
||||
return premium.InventoryActivationResult{}, fmt.Errorf("premium inventory activation repositories are unavailable")
|
||||
}
|
||||
accountID := strings.TrimSpace(s.accountIDForSession(session))
|
||||
if accountID == "" {
|
||||
return premium.InventoryActivationResult{}, fmt.Errorf("premium inventory activation account is unavailable")
|
||||
}
|
||||
characterID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
catalog, err := s.currentPremiumCatalog()
|
||||
if err != nil {
|
||||
return premium.InventoryActivationResult{}, err
|
||||
}
|
||||
contracts := currentPremiumInventoryContracts(catalog)
|
||||
if len(contracts) == 0 {
|
||||
return premium.InventoryActivationResult{}, nil
|
||||
}
|
||||
now := s.gameplayNow().UTC()
|
||||
var result premium.InventoryActivationResult
|
||||
err = repositories.RentalAssets.WithinRentalAssets(
|
||||
ctx,
|
||||
accountID,
|
||||
characterID,
|
||||
func(accounts dnfrepo.AccountRepository, _ dnfrepo.CharacterRepository, inventoryRepo dnfrepo.InventoryRepository, _ dnfrepo.EquipmentRepository) error {
|
||||
account, found, loadErr := accounts.Load(ctx, accountID)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("premium inventory activation account %q not found", accountID)
|
||||
}
|
||||
inventory, found, loadErr := inventoryRepo.Load(ctx, characterID)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("premium inventory activation inventory %q not found", characterID)
|
||||
}
|
||||
account = dnfrepo.CloneAccount(account)
|
||||
inventory = dnfrepo.CloneInventory(inventory)
|
||||
result, loadErr = premium.ActivateInventoryContracts(&account, &inventory, contracts, now)
|
||||
if loadErr != nil || !result.Changed() {
|
||||
return loadErr
|
||||
}
|
||||
account.UpdatedAt = now
|
||||
inventory.UpdatedAt = now
|
||||
if saveErr := accounts.Save(ctx, account); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
return dnfrepo.SaveInventoryFields(ctx, inventoryRepo, inventory, dnfrepo.InventoryFieldSlots)
|
||||
},
|
||||
)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// currentPremiumContractActivation converts explicit contract units into account
|
||||
// entitlement inside a caller-owned transaction. The magic-box owner calls it
|
||||
// through alignedcmd.MagicBoxContractActivator so a rolled premium contract
|
||||
// never takes a bag cell; returning an error makes the owner fall back to
|
||||
// ordinary placement, which keeps the reward recoverable.
|
||||
func (s *Service) currentPremiumContractActivation(
|
||||
account *dnfrepo.AccountRecord,
|
||||
units map[int64]int64,
|
||||
now time.Time,
|
||||
) (premium.InventoryActivationResult, error) {
|
||||
if s == nil || account == nil || len(units) == 0 {
|
||||
return premium.InventoryActivationResult{}, nil
|
||||
}
|
||||
catalog, err := s.currentPremiumCatalog()
|
||||
if err != nil {
|
||||
return premium.InventoryActivationResult{}, err
|
||||
}
|
||||
contracts := currentPremiumInventoryContracts(catalog)
|
||||
if len(contracts) == 0 {
|
||||
return premium.InventoryActivationResult{}, nil
|
||||
}
|
||||
return premium.ActivateContractUnits(account, contracts, units, now)
|
||||
}
|
||||
|
||||
// logCurrentPremiumInventoryActivation logs one activation sweep.
|
||||
func (s *Service) logCurrentPremiumInventoryActivation(
|
||||
session *gameSession,
|
||||
result premium.InventoryActivationResult,
|
||||
source string,
|
||||
) {
|
||||
if !result.Changed() {
|
||||
return
|
||||
}
|
||||
s.logGameEvent(session, "game-premium-contract-inventory-auto-activated",
|
||||
"source", source,
|
||||
"char_id", session.selectedCharacterID,
|
||||
"removed_slots", fmt.Sprint(result.RemovedSlots),
|
||||
"removed_rows", len(result.RemovedSlots),
|
||||
"activation_count", len(result.Activations),
|
||||
"activations", fmt.Sprint(result.Activations),
|
||||
"state_source", "runtime_pvf_premiumlist_item_type_term_and_account_expiry")
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentPremiumInventoryActivations(
|
||||
session *gameSession,
|
||||
result premium.InventoryActivationResult,
|
||||
source string,
|
||||
) error {
|
||||
if !result.Changed() {
|
||||
return nil
|
||||
}
|
||||
nowUnix := s.gameplayNow().UTC().Unix()
|
||||
refreshCrystal := false
|
||||
refreshBlackDiamond := false
|
||||
for _, activation := range result.Activations {
|
||||
remaining := activation.ExpireAt - nowUnix
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
wireType, canNotify := currentPremiumActivationWireType(activation.PremiumType)
|
||||
if canNotify {
|
||||
if err := s.sendGameUpperRawClassCodec(
|
||||
session,
|
||||
currentPremiumActivatedMsgID,
|
||||
buildCurrentPremiumActivatedBody(wireType, remaining),
|
||||
0,
|
||||
true,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if activation.PremiumType == premium.TypeCrystal {
|
||||
refreshCrystal = true
|
||||
}
|
||||
if premium.IsBlackDiamondStorageType(activation.PremiumType) {
|
||||
refreshBlackDiamond = true
|
||||
}
|
||||
}
|
||||
s.logCurrentPremiumInventoryActivation(session, result, source)
|
||||
if refreshBlackDiamond {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
err := s.refreshCurrentBlackDiamondAfterActivation(ctx, session, source+"_after_black_diamond_activation")
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if refreshCrystal {
|
||||
return s.sendCurrentCrystalContractState(session, source+"_after_type97_auto_activation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentPremiumContractCandidateItemUpdate(body []byte, catalog *currentPremiumCatalog) bool {
|
||||
if catalog == nil || len(body) < 3 || body[0] != dnfrepo.MainInventoryListType {
|
||||
return false
|
||||
}
|
||||
count := int(binary.LittleEndian.Uint16(body[1:3]))
|
||||
if count <= 0 || len(body) != 3+count*currentItemListEntryWireSize {
|
||||
return false
|
||||
}
|
||||
for index := 0; index < count; index++ {
|
||||
offset := 3 + index*currentItemListEntryWireSize
|
||||
itemID := int64(binary.LittleEndian.Uint32(body[offset+2 : offset+6]))
|
||||
if _, exists := catalog.contractsByItem[itemID]; exists {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func currentPremiumContractDeletionItemUpdate(
|
||||
body []byte,
|
||||
removedSlots []int16,
|
||||
) ([]byte, bool) {
|
||||
if len(body) < 3 || body[0] != dnfrepo.MainInventoryListType {
|
||||
return body, false
|
||||
}
|
||||
count := int(binary.LittleEndian.Uint16(body[1:3]))
|
||||
if len(body) != 3+count*currentItemListEntryWireSize {
|
||||
return body, false
|
||||
}
|
||||
entries := make([]currentItemListEntry, 0, count+len(removedSlots))
|
||||
bySlot := make(map[int16]int, count+len(removedSlots))
|
||||
for index := 0; index < count; index++ {
|
||||
offset := 3 + index*currentItemListEntryWireSize
|
||||
var entry currentItemListEntry
|
||||
copy(entry.data[:], body[offset:offset+currentItemListEntryWireSize])
|
||||
slot := int16(binary.LittleEndian.Uint16(entry.data[0:2]))
|
||||
bySlot[slot] = len(entries)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
for _, slot := range removedSlots {
|
||||
var deletion currentItemListEntry
|
||||
deletion.patchCore(slot, math.MaxUint32, 0)
|
||||
if index, exists := bySlot[slot]; exists {
|
||||
entries[index] = deletion
|
||||
continue
|
||||
}
|
||||
bySlot[slot] = len(entries)
|
||||
entries = append(entries, deletion)
|
||||
}
|
||||
sortCurrentItemListEntries(entries)
|
||||
return buildCurrentItemUpdateBody(dnfrepo.MainInventoryListType, entries), true
|
||||
}
|
||||
|
||||
// prepareCurrentPremiumContractItemUpdate intercepts only a class0/op14 list0
|
||||
// body that itself contains a PVF contract item. After the durable activation,
|
||||
// every consumed slot is projected as a deletion row, so the awarded contract
|
||||
// never appears in the client bag even though a crash before this boundary
|
||||
// would still leave a recoverable item for the login migration.
|
||||
func (s *Service) prepareCurrentPremiumContractItemUpdate(
|
||||
session *gameSession,
|
||||
body []byte,
|
||||
) ([]byte, error) {
|
||||
catalog, err := s.currentPremiumCatalog()
|
||||
if err != nil || !currentPremiumContractCandidateItemUpdate(body, catalog) {
|
||||
return body, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), currentRepositorySnapshotTimeout)
|
||||
defer cancel()
|
||||
result, err := s.activateCurrentPremiumInventoryContracts(ctx, session)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-premium-contract-item-update-auto-activation-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"reason", err,
|
||||
"fallback", "retain_recoverable_inventory_item_and_original_op14")
|
||||
return body, nil
|
||||
}
|
||||
if !result.Changed() {
|
||||
return body, nil
|
||||
}
|
||||
if err := s.sendCurrentPremiumInventoryActivations(session, result, "op14_contract_grant_before_inventory_projection"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated, ok := currentPremiumContractDeletionItemUpdate(body, result.RemovedSlots)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("premium contract item update body became invalid after activation")
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *Service) reconcileCurrentPremiumInventoryBeforeList(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
source string,
|
||||
notify bool,
|
||||
) error {
|
||||
result, err := s.activateCurrentPremiumInventoryContracts(ctx, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Changed() {
|
||||
return nil
|
||||
}
|
||||
if notify {
|
||||
return s.sendCurrentPremiumInventoryActivations(session, result, source)
|
||||
}
|
||||
s.logCurrentPremiumInventoryActivation(session, result, source)
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user