DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,655 @@
package dnfbridge
import (
"context"
"encoding/binary"
"fmt"
"sort"
"strconv"
"time"
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/inventory"
"longheng.io/server/internal/modules/dnf/itemtrade"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const (
currentItemTradeListType byte = 4
currentItemTradeChangeMsg uint16 = 15
currentItemTradeCancelMsg uint16 = 16
currentItemTradeStateMsg uint16 = 17
currentItemTradeFinishMsg uint16 = 18
currentItemTradeMoveMsg uint16 = 19
currentItemTradeReadyMsg uint16 = uint16(dnfenum.CmdPacketSetItemtradeState)
currentItemTradeAddingDone byte = 1
currentItemTradeOfferAck byte = 2
currentItemTradeReady byte = 3
currentItemTradeAddRequest byte = 5
currentItemTradeOfferFirst uint16 = 3
currentItemTradeOfferLast uint16 = 11
)
type onlineItemTradeOffer struct {
tradeSlot uint16
sourceList byte
sourceSlot int16
count int32
itemID int64
stack dnfrepo.ItemStack
}
type onlineItemTrade struct {
participants [2]uint16
offers map[uint16]map[uint16]onlineItemTradeOffer
gold map[uint16]int64
states map[uint16]byte
committing bool
}
func (t *onlineItemTrade) other(characterID uint16) (uint16, bool) {
if t == nil {
return 0, false
}
if t.participants[0] == characterID {
return t.participants[1], true
}
if t.participants[1] == characterID {
return t.participants[0], true
}
return 0, false
}
func (s *Service) beginOnlineItemTrade(first, second *gameSession) {
if s == nil || first == nil || second == nil || first == second || first.selectedCharacterID == 0 || second.selectedCharacterID == 0 {
return
}
firstID, secondID := first.selectedCharacterID, second.selectedCharacterID
if s.onlinePlayers == nil || !s.onlinePlayers.PeerInSameArea(firstID, secondID) {
return
}
s.tradeMu.Lock()
defer s.tradeMu.Unlock()
if s.itemTrades == nil {
s.itemTrades = make(map[uint16]*onlineItemTrade)
}
for _, id := range []uint16{firstID, secondID} {
if old := s.itemTrades[id]; old != nil {
delete(s.itemTrades, old.participants[0])
delete(s.itemTrades, old.participants[1])
}
}
trade := &onlineItemTrade{
participants: [2]uint16{firstID, secondID},
offers: map[uint16]map[uint16]onlineItemTradeOffer{
firstID: make(map[uint16]onlineItemTradeOffer),
secondID: make(map[uint16]onlineItemTradeOffer),
},
gold: map[uint16]int64{firstID: 0, secondID: 0},
states: map[uint16]byte{firstID: 0, secondID: 0},
}
s.itemTrades[firstID] = trade
s.itemTrades[secondID] = trade
}
func (s *Service) handleOnlineItemTradeCommand(session *gameSession, typ uint16, body []byte) (bool, error) {
if s == nil || session == nil || session.selectedCharacterID == 0 {
return false, nil
}
trade := s.currentOnlineItemTrade(session.selectedCharacterID)
if trade == nil {
return false, nil
}
switch typ {
case currentItemTradeMoveMsg:
request, err := inventory.DecodeMoveItemspaceRequest(body)
if err != nil {
return true, s.sendGameUpperFailure(session, typ, 1)
}
if request.SourceListType != currentItemTradeListType && request.DestinationListType != currentItemTradeListType {
return false, nil
}
return true, s.handleOnlineItemTradeMove(session, request)
case currentItemTradeReadyMsg:
if len(body) != 1 {
return true, s.sendGameUpperFailure(session, typ, 1)
}
return true, s.handleOnlineItemTradeState(session, body[0])
default:
return false, nil
}
}
func (s *Service) currentOnlineItemTrade(characterID uint16) *onlineItemTrade {
s.tradeMu.Lock()
defer s.tradeMu.Unlock()
return s.itemTrades[characterID]
}
func (s *Service) handleOnlineItemTradeMove(session *gameSession, request inventory.MoveItemspaceRequest) error {
if request.DestinationListType == currentItemTradeListType && request.SourceListType != currentItemTradeListType {
if isOnlineItemTradeGoldDeposit(request) {
return s.addOnlineItemTradeGold(session, request)
}
return s.addOnlineItemTradeOffer(session, request)
}
if request.SourceListType == currentItemTradeListType && request.DestinationListType != currentItemTradeListType {
if isOnlineItemTradeGoldWithdrawal(request) {
return s.removeOnlineItemTradeGold(session, request)
}
return s.removeOnlineItemTradeOffer(session, request)
}
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
func isOnlineItemTradeGoldDeposit(request inventory.MoveItemspaceRequest) bool {
return request.SourceListType == dnfrepo.MainInventoryListType &&
request.SourceSlotIndex == 0 && request.SourceInstanceValue == 0 &&
request.MoveCount > 0 && request.DestinationListType == currentItemTradeListType &&
request.DestinationSlotIndex == 0
}
func isOnlineItemTradeGoldWithdrawal(request inventory.MoveItemspaceRequest) bool {
return request.SourceListType == currentItemTradeListType && request.SourceSlotIndex == 0 &&
request.DestinationListType == dnfrepo.MainInventoryListType && request.DestinationSlotIndex == 0
}
func (s *Service) addOnlineItemTradeGold(session *gameSession, request inventory.MoveItemspaceRequest) error {
repositories, ok := s.repositoryGroup()
if !ok || repositories.Character == nil {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
ctx, cancel := context.WithTimeout(context.Background(), currentRepositorySnapshotTimeout)
defer cancel()
character, found, err := repositories.Character.Load(ctx, strconv.Itoa(int(session.selectedCharacterID)))
if err != nil || !found || int64(request.MoveCount) > character.Stats["gold"] {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
s.tradeMu.Lock()
trade := s.itemTrades[session.selectedCharacterID]
peerID, inTrade := trade.other(session.selectedCharacterID)
valid := inTrade && !trade.committing && trade.states[session.selectedCharacterID] != currentItemTradeReady
previousGold := int64(0)
if valid {
previousGold = trade.gold[session.selectedCharacterID]
trade.gold[session.selectedCharacterID] = int64(request.MoveCount)
}
s.tradeMu.Unlock()
peer, peerOnline := s.onlineGameSession(peerID)
if !valid || !peerOnline || s.onlinePlayers == nil || !s.onlinePlayers.PeerInSameArea(session.selectedCharacterID, peerID) {
if valid {
s.tradeMu.Lock()
if current := s.itemTrades[session.selectedCharacterID]; current == trade && !current.committing {
current.gold[session.selectedCharacterID] = previousGold
}
s.tradeMu.Unlock()
}
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
if err := s.sendGameUpperSuccess(session, currentItemTradeMoveMsg, buildOnlineItemTradeMoveAck(request)); err != nil {
return err
}
entry := currentGoldWalletItemListEntry(int64(request.MoveCount))
if err := s.sendGameUpperRawClassCodec(peer, currentItemTradeChangeMsg, entry.data[:], 0, true); err != nil {
return err
}
s.logGameEvent(session, "game-item-trade-gold-added",
"char_id", session.selectedCharacterID, "peer_char_id", peerID, "gold", request.MoveCount)
return nil
}
func (s *Service) removeOnlineItemTradeGold(session *gameSession, request inventory.MoveItemspaceRequest) error {
s.tradeMu.Lock()
trade := s.itemTrades[session.selectedCharacterID]
peerID, inTrade := trade.other(session.selectedCharacterID)
gold := int64(0)
if inTrade && !trade.committing && trade.states[session.selectedCharacterID] != currentItemTradeReady {
gold = trade.gold[session.selectedCharacterID]
if gold > 0 {
trade.gold[session.selectedCharacterID] = 0
}
}
s.tradeMu.Unlock()
if gold <= 0 {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
if err := s.sendGameUpperSuccess(session, currentItemTradeMoveMsg, buildOnlineItemTradeMoveAck(request)); err != nil {
return err
}
peer, peerOnline := s.onlineGameSession(peerID)
if !peerOnline {
return nil
}
return s.sendGameUpperRawClassCodec(peer, currentItemTradeChangeMsg, buildOnlineItemTradeRemovedEntry(0), 0, true)
}
func (s *Service) addOnlineItemTradeOffer(session *gameSession, request inventory.MoveItemspaceRequest) error {
if request.MoveCount <= 0 || request.DestinationSlotIndex < 0 || request.SourceSlotIndex < 0 ||
(request.SourceListType != dnfrepo.MainInventoryListType && request.SourceListType != 1) {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Inventory == nil {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
ctx, cancel := context.WithTimeout(context.Background(), currentRepositorySnapshotTimeout)
defer cancel()
record, found, err := repositories.Inventory.Load(ctx, strconv.Itoa(int(session.selectedCharacterID)))
if err != nil || !found {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
sourceKey := fmt.Sprintf("%d:%d", request.SourceListType, request.SourceSlotIndex)
stack, found := record.Slots[sourceKey]
if !found || stack.ItemID <= 0 || stack.Count < int64(request.MoveCount) || stack.Bind ||
(!stack.ExpireAt.IsZero() && !stack.ExpireAt.After(time.Now())) ||
dnfrepo.IsAccountSharedInventorySlot(request.SourceListType, request.SourceSlotIndex) {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
// The request's source-instance field is an opaque value read from the
// current client's live item object. It is not the PVF item ID and is not
// stable across every equipment/stackable representation. The repository
// slot and authoritative count above are sufficient to prevent item
// injection, so a projection mismatch must not reject a valid trade.
projectedSourceValue := currentItemListStackAmount(request.SourceListType, stack)
if request.SourceInstanceValue != 0 && uint32(request.SourceInstanceValue) != projectedSourceValue {
s.logGameEvent(session, "game-item-trade-source-instance-projection-mismatch",
"char_id", session.selectedCharacterID,
"source_list", request.SourceListType,
"source_slot", request.SourceSlotIndex,
"client_value", request.SourceInstanceValue,
"projected_value", projectedSourceValue)
}
s.tradeMu.Lock()
trade := s.itemTrades[session.selectedCharacterID]
peerID, inTrade := trade.other(session.selectedCharacterID)
valid := inTrade && !trade.committing && trade.states[session.selectedCharacterID] != currentItemTradeReady
tradeSlot := uint16(0)
slotAvailable := false
if valid {
// Current NoPack's trade container has twelve native slots. Live current-
// EXE memory and sub_2339FB0 show 0..2 are occupied pseudo rows (gold and
// the other trade currencies); the nine visible item rows are 3..11.
// Targeting 0 renders a count-one item as +1 gold, while 1/2 collide with
// the remaining pseudo rows and leave the item list blank.
for slot := currentItemTradeOfferFirst; slot <= currentItemTradeOfferLast; slot++ {
if _, occupied := trade.offers[session.selectedCharacterID][slot]; !occupied {
tradeSlot = slot
slotAvailable = true
break
}
}
for _, existing := range trade.offers[session.selectedCharacterID] {
if existing.sourceList == request.SourceListType && existing.sourceSlot == request.SourceSlotIndex {
valid = false
break
}
}
}
valid = valid && slotAvailable
offer := onlineItemTradeOffer{
tradeSlot: tradeSlot, sourceList: request.SourceListType, sourceSlot: request.SourceSlotIndex,
count: request.MoveCount, itemID: stack.ItemID, stack: cloneOnlineItemTradeStack(stack),
}
if valid {
trade.offers[session.selectedCharacterID][tradeSlot] = offer
}
s.tradeMu.Unlock()
peer, peerOnline := s.onlineGameSession(peerID)
if !valid || !peerOnline || s.onlinePlayers == nil || !s.onlinePlayers.PeerInSameArea(session.selectedCharacterID, peerID) {
if valid {
s.removeOnlineItemTradeOfferState(session.selectedCharacterID, tradeSlot)
}
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
// The request always targets pseudo-container slot 0 because the client does
// not allocate the visible row itself. Echo the server-assigned native item
// row in the success ACK; 0..2 are pseudo rows and item rows are 3..11.
ackRequest := request
ackRequest.DestinationSlotIndex = int16(tradeSlot)
if err := s.sendGameUpperSuccess(session, currentItemTradeMoveMsg, buildOnlineItemTradeMoveAck(ackRequest)); err != nil {
return err
}
offer.stack.Count = int64(offer.count)
entry := currentItemListEntryFromStack(offer.sourceList, int16(offer.tradeSlot), offer.stack)
if err := s.sendGameUpperRawClassCodec(peer, currentItemTradeChangeMsg, entry.data[:], 0, true); err != nil {
return err
}
s.logGameEvent(session, "game-item-trade-offer-added",
"char_id", session.selectedCharacterID, "peer_char_id", peerID,
"source_list", offer.sourceList, "source_slot", offer.sourceSlot,
"trade_slot", offer.tradeSlot, "item_id", offer.itemID, "count", offer.count)
return nil
}
func (s *Service) removeOnlineItemTradeOffer(session *gameSession, request inventory.MoveItemspaceRequest) error {
tradeSlot := uint16(request.SourceSlotIndex)
s.tradeMu.Lock()
trade := s.itemTrades[session.selectedCharacterID]
peerID, inTrade := trade.other(session.selectedCharacterID)
var offer onlineItemTradeOffer
var found bool
if inTrade && !trade.committing && trade.states[session.selectedCharacterID] != currentItemTradeReady {
offer, found = trade.offers[session.selectedCharacterID][tradeSlot]
if found {
delete(trade.offers[session.selectedCharacterID], tradeSlot)
}
}
s.tradeMu.Unlock()
if !found || request.DestinationListType != offer.sourceList {
return s.sendGameUpperFailure(session, currentItemTradeMoveMsg, 1)
}
if err := s.sendGameUpperSuccess(session, currentItemTradeMoveMsg, buildOnlineItemTradeMoveAck(request)); err != nil {
return err
}
peer, peerOnline := s.onlineGameSession(peerID)
if !peerOnline {
return nil
}
return s.sendGameUpperRawClassCodec(peer, currentItemTradeChangeMsg, buildOnlineItemTradeRemovedEntry(tradeSlot), 0, true)
}
func buildOnlineItemTradeRemovedEntry(tradeSlot uint16) []byte {
removed := make([]byte, currentItemListEntryWireSize)
binary.LittleEndian.PutUint16(removed[0:2], tradeSlot)
binary.LittleEndian.PutUint32(removed[2:6], ^uint32(0))
return removed
}
func (s *Service) removeOnlineItemTradeOfferState(characterID, tradeSlot uint16) {
s.tradeMu.Lock()
defer s.tradeMu.Unlock()
if trade := s.itemTrades[characterID]; trade != nil && !trade.committing {
delete(trade.offers[characterID], tradeSlot)
}
}
func (s *Service) handleOnlineItemTradeState(session *gameSession, state byte) error {
if state != 0 && state != currentItemTradeAddingDone && state != currentItemTradeOfferAck && state != currentItemTradeReady && state != currentItemTradeAddRequest {
return s.sendGameUpperFailure(session, currentItemTradeReadyMsg, 1)
}
characterID := session.selectedCharacterID
s.tradeMu.Lock()
trade := s.itemTrades[characterID]
peerID, inTrade := trade.other(characterID)
if !inTrade || trade.committing {
s.tradeMu.Unlock()
return s.sendGameUpperFailure(session, currentItemTradeReadyMsg, 1)
}
peerState := trade.states[peerID]
if state == currentItemTradeOfferAck {
currentState := trade.states[characterID]
if currentState == currentItemTradeReady {
s.tradeMu.Unlock()
return s.sendGameUpperFailure(session, currentItemTradeReadyMsg, 1)
}
s.tradeMu.Unlock()
s.logGameEvent(session, "game-item-trade-offer-change-acknowledged",
"char_id", characterID,
"peer_char_id", peerID,
"retained_state", currentState,
"peer_state", peerState)
return s.sendGameUpperSuccess(session, currentItemTradeReadyMsg, nil)
}
projectedState := state
// Live current-EXE traffic distinguishes the two buttons even though the
// server-facing state table does not: "adding complete" submits 5 and must
// be projected to class0/op17 state 1; choosing Yes in resource 727 submits
// 3 and must be projected as state 3. A second value 5 is retained only as
// a compatibility spelling of final consent for older clients. Neither
// spelling may skip the shared state-1 confirmation boundary.
switch state {
case currentItemTradeAddRequest:
if trade.states[characterID] == 0 {
projectedState = currentItemTradeAddingDone
} else if trade.states[characterID] == currentItemTradeAddingDone &&
(peerState == currentItemTradeAddingDone || peerState == currentItemTradeReady) {
projectedState = currentItemTradeReady
} else {
s.tradeMu.Unlock()
return s.sendGameUpperFailure(session, currentItemTradeReadyMsg, 1)
}
case currentItemTradeReady:
if trade.states[characterID] != currentItemTradeAddingDone ||
(peerState != currentItemTradeAddingDone && peerState != currentItemTradeReady) {
s.tradeMu.Unlock()
return s.sendGameUpperFailure(session, currentItemTradeReadyMsg, 1)
}
case currentItemTradeAddingDone:
if trade.states[characterID] != 0 {
s.tradeMu.Unlock()
return s.sendGameUpperFailure(session, currentItemTradeReadyMsg, 1)
}
case 0:
if trade.states[characterID] == currentItemTradeReady {
s.tradeMu.Unlock()
return s.sendGameUpperFailure(session, currentItemTradeReadyMsg, 1)
}
}
trade.states[characterID] = projectedState
peerState = trade.states[peerID]
ready := projectedState == currentItemTradeReady && peerState == currentItemTradeReady
addingDone := projectedState == currentItemTradeAddingDone && peerState == currentItemTradeAddingDone
if ready {
trade.committing = true
}
s.tradeMu.Unlock()
s.logGameEvent(session, "game-item-trade-state-updated",
"char_id", characterID,
"peer_char_id", peerID,
"inbound_state", state,
"projected_state", projectedState,
"peer_state", peerState,
"both_adding_done", addingDone,
"both_ready", ready)
if err := s.sendGameUpperSuccess(session, currentItemTradeReadyMsg, nil); err != nil {
return err
}
// The current client moves itself to "waiting for the other participant's
// final confirmation" as soon as it submits state 3. Broadcasting op17/3
// here would make sub_2340B10 close resource 727 on the peer that has not
// answered yet, preventing the second consent. Keep final consent server-
// side only; the second consent is followed directly by the atomic op18
// completion (or the existing op16 rollback on failure).
if projectedState == currentItemTradeReady {
if ready {
return s.commitOnlineItemTrade(trade)
}
return nil
}
stateBody := buildOnlineItemTradeStateBody(characterID, projectedState)
if err := s.sendGameUpperRawClassCodec(session, currentItemTradeStateMsg, stateBody, 0, true); err != nil {
return err
}
peer, peerOnline := s.onlineGameSession(peerID)
if peerOnline {
if err := s.sendGameUpperRawClassCodec(peer, currentItemTradeStateMsg, stateBody, 0, true); err != nil {
return err
}
}
return nil
}
func (s *Service) commitOnlineItemTrade(trade *onlineItemTrade) error {
if trade == nil {
return nil
}
firstID, secondID := trade.participants[0], trade.participants[1]
firstOffers, secondOffers := snapshotOnlineItemTradeOffers(trade, firstID), snapshotOnlineItemTradeOffers(trade, secondID)
firstGold, secondGold := snapshotOnlineItemTradeGold(trade, firstID), snapshotOnlineItemTradeGold(trade, secondID)
repositories, ok := s.repositoryGroup()
if !ok {
s.cancelOnlineItemTrade(trade, "repository_unavailable")
return nil
}
owner, err := itemtrade.NewOwner(repositories)
if err != nil {
s.cancelOnlineItemTrade(trade, "transaction_owner_unavailable")
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), currentRepositorySnapshotTimeout)
defer cancel()
result, err := owner.Exchange(ctx,
itemtrade.Participant{CharacterID: strconv.Itoa(int(firstID)), Offers: itemTradeDomainOffers(firstOffers), Gold: firstGold},
itemtrade.Participant{CharacterID: strconv.Itoa(int(secondID)), Offers: itemTradeDomainOffers(secondOffers), Gold: secondGold},
)
if err != nil {
s.logInfo("dnfbridge item trade atomic commit rejected", "first_char_id", firstID, "second_char_id", secondID, "error", err)
s.cancelOnlineItemTrade(trade, "atomic_commit_rejected")
return nil
}
s.clearOnlineItemTrade(trade)
for _, characterID := range []uint16{firstID, secondID} {
session, online := s.onlineGameSession(characterID)
if !online {
continue
}
body := buildOnlineItemTradeFinishBody(result.Received[strconv.Itoa(int(characterID))])
if err := s.sendGameUpperRawClassCodec(session, currentItemTradeFinishMsg, body, 0, true); err != nil {
return err
}
if character, changed := result.Characters[strconv.Itoa(int(characterID))]; changed {
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), buildCurrentGoldStateBody(character.Stats["gold"]), 0); err != nil {
return err
}
}
if err := s.sendSelectedCurrentContainerListsRefresh(session, "current_exe_item_trade_atomic_commit"); err != nil {
return err
}
}
s.logInfo("dnfbridge item trade atomic commit completed", "first_char_id", firstID, "second_char_id", secondID,
"first_offer_count", len(firstOffers), "second_offer_count", len(secondOffers),
"first_gold", firstGold, "second_gold", secondGold)
return nil
}
func snapshotOnlineItemTradeGold(trade *onlineItemTrade, characterID uint16) int64 {
if trade == nil {
return 0
}
return trade.gold[characterID]
}
func snapshotOnlineItemTradeOffers(trade *onlineItemTrade, characterID uint16) []onlineItemTradeOffer {
offers := make([]onlineItemTradeOffer, 0, len(trade.offers[characterID]))
for _, offer := range trade.offers[characterID] {
offers = append(offers, offer)
}
sort.Slice(offers, func(i, j int) bool { return offers[i].tradeSlot < offers[j].tradeSlot })
return offers
}
func itemTradeDomainOffers(offers []onlineItemTradeOffer) []itemtrade.Offer {
rows := make([]itemtrade.Offer, 0, len(offers))
for _, offer := range offers {
rows = append(rows, itemtrade.Offer{TradeSlot: offer.tradeSlot, SourceList: offer.sourceList,
SourceSlot: offer.sourceSlot, Count: int64(offer.count), ExpectedItem: offer.itemID})
}
return rows
}
func (s *Service) clearOnlineItemTrade(trade *onlineItemTrade) {
s.tradeMu.Lock()
defer s.tradeMu.Unlock()
if trade != nil {
for _, characterID := range trade.participants {
if s.itemTrades[characterID] == trade {
delete(s.itemTrades, characterID)
}
}
}
}
func (s *Service) cancelOnlineItemTrade(trade *onlineItemTrade, reason string) {
if trade == nil {
return
}
firstID, secondID := trade.participants[0], trade.participants[1]
firstOffers, secondOffers := snapshotOnlineItemTradeOffers(trade, firstID), snapshotOnlineItemTradeOffers(trade, secondID)
firstGold, secondGold := snapshotOnlineItemTradeGold(trade, firstID), snapshotOnlineItemTradeGold(trade, secondID)
s.clearOnlineItemTrade(trade)
for _, row := range []struct {
characterID uint16
offers []onlineItemTradeOffer
gold int64
}{{firstID, firstOffers, firstGold}, {secondID, secondOffers, secondGold}} {
if session, online := s.onlineGameSession(row.characterID); online {
_ = s.sendGameUpperRawClassCodec(session, currentItemTradeCancelMsg, buildOnlineItemTradeCancelBody(row.offers, row.gold), 0, true)
}
}
s.logInfo("dnfbridge item trade cancelled", "first_char_id", firstID, "second_char_id", secondID, "reason", reason)
}
func (s *Service) detachOnlineItemTrade(session *gameSession, reason string) {
if s == nil || session == nil || session.selectedCharacterID == 0 {
return
}
s.tradeMu.Lock()
trade := s.itemTrades[session.selectedCharacterID]
s.tradeMu.Unlock()
if trade != nil {
s.cancelOnlineItemTrade(trade, reason)
}
}
func buildOnlineItemTradeMoveAck(request inventory.MoveItemspaceRequest) []byte {
var writer packetWriter
writer.writeByte(request.SourceListType)
writer.writeUint16(uint16(request.SourceSlotIndex))
writer.writeInt32(int(request.MoveCount))
writer.writeByte(request.DestinationListType)
writer.writeUint16(uint16(request.DestinationSlotIndex))
return writer.bytes()
}
func buildOnlineItemTradeStateBody(characterID uint16, state byte) []byte {
var writer packetWriter
writer.writeUint16(characterID)
writer.writeByte(state)
return writer.bytes()
}
func buildOnlineItemTradeCancelBody(offers []onlineItemTradeOffer, gold int64) []byte {
var writer packetWriter
count := len(offers)
if gold > 0 {
count++
}
writer.writeUint16(uint16(count))
if gold > 0 {
writer.writeUint16(0)
writer.writeByte(dnfrepo.MainInventoryListType)
writer.writeUint16(0)
}
for _, offer := range offers {
writer.writeUint16(offer.tradeSlot)
writer.writeByte(offer.sourceList)
writer.writeUint16(uint16(offer.sourceSlot))
}
return writer.bytes()
}
func buildOnlineItemTradeFinishBody(transfers []itemtrade.Transfer) []byte {
var writer packetWriter
writer.writeUint16(uint16(len(transfers)))
for _, transfer := range transfers {
writer.writeUint16(transfer.TradeSlot)
writer.writeUint16(uint16(transfer.DestinationSlot))
}
return writer.bytes()
}
func cloneOnlineItemTradeStack(stack dnfrepo.ItemStack) dnfrepo.ItemStack {
stack.RawEntry = append([]byte(nil), stack.RawEntry...)
if stack.Extra != nil {
extra := make(map[string]string, len(stack.Extra))
for key, value := range stack.Extra {
extra[key] = value
}
stack.Extra = extra
}
return stack
}