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,236 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
)
|
||||
|
||||
func rentalGameplayModule() gameplayModuleDefinition {
|
||||
rentOpcode := uint16(dnfenum.CmdPacketRentEquipmentItem)
|
||||
chargeOpcode := uint16(dnfenum.CmdPacketChargeRentpoint)
|
||||
return gameplayModuleDefinition{
|
||||
Name: "equipment-rental",
|
||||
LegacyHandlers: map[uint16]gameplayHandler{
|
||||
rentOpcode: func(service *Service, session *gameSession, request gameplayRequest) error {
|
||||
return service.handleCurrentRentEquipment(session, request.Body)
|
||||
},
|
||||
chargeOpcode: func(service *Service, session *gameSession, request gameplayRequest) error {
|
||||
return service.handleCurrentChargeRentalPoint(session, request.Body)
|
||||
},
|
||||
},
|
||||
UpperHandlers: map[uint16]gameplayHandler{
|
||||
rentOpcode: func(service *Service, session *gameSession, request gameplayRequest) error {
|
||||
if !requireDefaultGameplayClassification(service, session, request, "game-upper-rental-equipment-request-blocked", "current_exe_op999_command_class_mismatch") {
|
||||
return nil
|
||||
}
|
||||
return service.handleCurrentRentEquipment(session, request.Body)
|
||||
},
|
||||
chargeOpcode: func(service *Service, session *gameSession, request gameplayRequest) error {
|
||||
if !requireDefaultGameplayClassification(service, session, request, "game-upper-rental-point-charge-request-blocked", "current_exe_op1000_command_class_mismatch") {
|
||||
return nil
|
||||
}
|
||||
return service.handleCurrentChargeRentalPoint(session, request.Body)
|
||||
},
|
||||
},
|
||||
LegacyNormalizers: map[uint16]gameplayLegacyNormalizer{
|
||||
rentOpcode: func(body []byte) []byte {
|
||||
return stripLegacyTransportTrailer(body, currentRentalRequestWireSize)
|
||||
},
|
||||
chargeOpcode: func(body []byte) []byte {
|
||||
return stripLegacyTransportTrailer(body, currentRentalRequestWireSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentRentEquipment(session *gameSession, body []byte) error {
|
||||
request, err := decodeCurrentRentEquipmentRequest(body)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "body_len", len(body), "reason", err)
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
repositories, characterID, character, err := s.currentRentalSelectedCharacter(ctx, session)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "item_id", request.ItemID, "reason", err)
|
||||
return nil
|
||||
}
|
||||
catalog, source, items, err := s.currentRentalSources(ctx)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "item_id", request.ItemID, "reason", err)
|
||||
return nil
|
||||
}
|
||||
jobValue := numericCharacterStat(character.Job)
|
||||
if jobValue < 0 || jobValue > math.MaxUint16 {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "item_id", request.ItemID, "job", character.Job, "reason", "job_out_of_u16_range")
|
||||
return nil
|
||||
}
|
||||
jobTag, err := currentRentalCharacterJobTag(source, byte(jobValue))
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "item_id", request.ItemID, "job", jobValue, "reason", err)
|
||||
return nil
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
effectiveLevel, overEquipContract := currentRentalEffectiveLevel(
|
||||
ctx,
|
||||
repositories.Account,
|
||||
s.accountIDForSession(session),
|
||||
character.Level,
|
||||
now,
|
||||
)
|
||||
tier, ok := catalog.tierForLevel(effectiveLevel)
|
||||
if !ok {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked",
|
||||
"item_id", request.ItemID,
|
||||
"level", character.Level,
|
||||
"effective_level", effectiveLevel,
|
||||
"over_equip_contract", overEquipContract,
|
||||
"reason", "no_pvf_tier")
|
||||
return nil
|
||||
}
|
||||
// 这一档声明的强化/锻造必须随发放行一起写下去。少了它,客户端租赁商店预览
|
||||
// 显示 +7/+3 而到手是 +0,且租赁武器 PVF 把强化与锻造都列为不可自行操作,
|
||||
// 玩家没有任何补救途径。
|
||||
tierRow, ok := catalog.tierRow(tier)
|
||||
if !ok {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked",
|
||||
"item_id", request.ItemID,
|
||||
"tier", tier,
|
||||
"reason", "no_pvf_tier_row")
|
||||
return nil
|
||||
}
|
||||
attributes := currentRentalItemAttributes{Reinforce: tierRow.Reinforce, Forge: tierRow.Forge}
|
||||
expectedPacked := uint32(uint16(jobValue)) | uint32(uint16(tier))<<16
|
||||
if request.PackedJobTier != expectedPacked {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked",
|
||||
"item_id", request.ItemID,
|
||||
"packed_job_tier", request.PackedJobTier,
|
||||
"expected_packed_job_tier", expectedPacked,
|
||||
"job", jobValue,
|
||||
"tier", tier,
|
||||
"level", character.Level,
|
||||
"effective_level", effectiveLevel,
|
||||
"over_equip_contract", overEquipContract,
|
||||
"reason", "current_exe_job_tier_key_mismatch")
|
||||
return nil
|
||||
}
|
||||
pointCost, ok := catalog.itemCost(jobTag, tier, request.ItemID)
|
||||
if !ok {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "item_id", request.ItemID, "job_tag", jobTag, "tier", tier, "reason", "item_not_in_current_pvf_group")
|
||||
return nil
|
||||
}
|
||||
definition, err := validateCurrentRentalItem(source, items, request.ItemID, jobTag)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "item_id", request.ItemID, "job_tag", jobTag, "tier", tier, "reason", err)
|
||||
return nil
|
||||
}
|
||||
owner, err := currentRentalAssetOwner(repositories)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-request-blocked", "item_id", request.ItemID, "reason", err)
|
||||
return nil
|
||||
}
|
||||
result, err := rentCurrentEquipment(ctx, owner, s.accountIDForSession(session), characterID, request.ItemID, pointCost, definition, attributes, now)
|
||||
if err != nil {
|
||||
resultCode := uint32(1)
|
||||
if errors.Is(err, errCurrentRentalInventoryFull) {
|
||||
resultCode = 2
|
||||
}
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-transaction-blocked",
|
||||
"character_id", characterID,
|
||||
"item_id", request.ItemID,
|
||||
"point_cost", pointCost,
|
||||
"result_code", resultCode,
|
||||
"reason", err)
|
||||
return s.sendCurrentRentEquipmentResult(session, resultCode)
|
||||
}
|
||||
if err := s.sendCurrentRentEquipmentResult(session, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.sendSelectedCurrentRentalPointState(session, "op999_after_atomic_rent", true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendSelectedCurrentRentalInventoryItemUpdate(session, repositories.Inventory, characterID, request.ItemID, result); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-upper-rental-equipment-applied",
|
||||
"character_id", characterID,
|
||||
"item_id", request.ItemID,
|
||||
"job", jobValue,
|
||||
"job_tag", jobTag,
|
||||
"tier", tier,
|
||||
"point_cost", pointCost,
|
||||
"remaining_points", result.Points,
|
||||
"slot", result.Slot,
|
||||
"equipped", result.Equipped,
|
||||
"expire_unix", result.ExpireAt.Unix(),
|
||||
"pvf_path", definition.PVFPath,
|
||||
"durability", definition.Durability,
|
||||
"reinforce", attributes.Reinforce,
|
||||
"forge", attributes.Forge)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentChargeRentalPoint(session *gameSession, body []byte) error {
|
||||
request, err := decodeCurrentChargeRentalPointRequest(body)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-point-charge-request-blocked", "body_len", len(body), "reason", err)
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
repositories, characterID, _, err := s.currentRentalSelectedCharacter(ctx, session)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-point-charge-request-blocked", "count", request.Count, "reason", err)
|
||||
return nil
|
||||
}
|
||||
catalog, _, _, err := s.currentRentalSources(ctx)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-point-charge-request-blocked", "count", request.Count, "reason", err)
|
||||
return nil
|
||||
}
|
||||
owner, err := currentRentalAssetOwner(repositories)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-point-charge-request-blocked", "count", request.Count, "reason", err)
|
||||
return nil
|
||||
}
|
||||
result, err := purchaseCurrentRentalPoints(ctx, owner, s.accountIDForSession(session), characterID, request.Count, catalog.Limit, catalog.GoldPerPoint, time.Now().UTC())
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-rental-point-charge-transaction-blocked",
|
||||
"character_id", characterID,
|
||||
"count", request.Count,
|
||||
"point_limit", catalog.Limit,
|
||||
"gold_per_point", catalog.GoldPerPoint,
|
||||
"reason", err)
|
||||
return s.sendGameUpperFailure(session, uint16(dnfenum.CmdPacketChargeRentpoint), 1)
|
||||
}
|
||||
var response packetWriter
|
||||
response.writeUint32(request.ChargeType)
|
||||
response.writeUint32(0)
|
||||
response.writeUint32(result.Points)
|
||||
if err := s.sendGameUpperSuccess(session, uint16(dnfenum.CmdPacketChargeRentpoint), response.bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.sendSelectedCurrentRentalPointState(session, "op1000_after_atomic_charge", true); err != nil {
|
||||
return err
|
||||
}
|
||||
// op1000 charges gold (gold_per_point), and the list-0 gold wallet row is the
|
||||
// client's only gold display. Without republishing it the balance stays at
|
||||
// the pre-purchase value until the next full list load, which is the reported
|
||||
// "not refreshed until I relog".
|
||||
if err := s.sendSelectedCurrentRentalGoldRefresh(session, "op1000_after_atomic_charge", result.Gold); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logGameEvent(session, "game-upper-rental-point-charge-applied",
|
||||
"character_id", characterID,
|
||||
"count", request.Count,
|
||||
"new_points", result.Points,
|
||||
"remaining_gold", result.Gold,
|
||||
"gold_per_point", catalog.GoldPerPoint,
|
||||
"point_limit", catalog.Limit)
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user