Files
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

299 lines
10 KiB
Go

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
}