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,253 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestResolveCurrentChronicleGrowthMetadata(t *testing.T) {
|
||||
source := dungeonDropCatalogTestSource{
|
||||
"monster/monster.lst": "",
|
||||
"stackable/stackable.lst": "10094062 `10094001/normal.stk`\n10094063 `10094001/advanced.stk`\n",
|
||||
"equipment/equipment.lst": "700 `weapon/test.equ`\n",
|
||||
currentChronicleGrowthCostPVFPath: "[consume item by rarity]\n5 3311 3 1.00\n[/consume item by rarity]\n" +
|
||||
"[weight by weapon]\n`weapon` 1.20 `coat` 1.00\n[/weight by weapon]\n" +
|
||||
"[weight by level]\n1 70 1.00 2.00 70 85 2.00 3.50 85 200 3.50 50.00\n[/weight by level]\n" +
|
||||
"[weight by upgrade]\n0 20 1.00 40.00\n[/weight by upgrade]\n" +
|
||||
"[weight by amplify]\n0 20 1.00 144.00\n[/weight by amplify]\n" +
|
||||
"[weight by seperate]\n0 7 1.00 3.00\n[/weight by seperate]\n",
|
||||
"stackable/10094001/normal.stk": "[name] `Normal`\n[stackable type] `[etc]` 1\n" +
|
||||
"[emancipate ticket] 5\n" +
|
||||
"[need material] 10093971 56 [/need material]\n[equipment level emancipate]\n" +
|
||||
"[probability] 75 100000 85 100000 [/probability]\n[equipment upgrade level] 3\n[equipment condition]\n" +
|
||||
"[rarity] 5\n[minimum level] 70\n[maximum level] 90\n[/equipment condition]\n[/equipment level emancipate]\n",
|
||||
"stackable/10094001/advanced.stk": "[name] `Advanced`\n[stackable type] `[etc]` 1\n" +
|
||||
"[equipment level emancipate]\n[probability] 75 100000 85 100000 [/probability]\n[equipment upgrade level] 5\n" +
|
||||
"[equipment condition]\n[rarity] 5\n[minimum level] 70\n[maximum level] 90\n[/equipment condition]\n[/equipment level emancipate]\n",
|
||||
"equipment/weapon/test.equ": "[name] `Test`\n[equipment type] `[weapon]` 17\n[rarity] 5\n[minimum level] 70\n[need material] 3311 169\n[/need material]\n",
|
||||
}
|
||||
catalog, err := newPVFDungeonDropCatalog(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolution, err := resolveCurrentChronicleGrowthMetadata(catalog, source, 10094062, 700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolution.MaterialItemID != 3311 || resolution.UpgradeLevel != 3 || resolution.TargetMinimumLevel != 70 ||
|
||||
resolution.ConditionMinimum != 70 || resolution.ConditionMaximum != 90 || resolution.TargetRarity != 5 ||
|
||||
resolution.EquipmentType != "weapon" || resolution.CostPolicy.BaseCount != 3 || len(resolution.Probability) != 2 {
|
||||
t.Fatalf("resolution = %+v", resolution)
|
||||
}
|
||||
if resolution.CostPolicy.MaterialItemID == 10093971 || resolution.CostPolicy.BaseCount == 169 {
|
||||
t.Fatalf("[need material] was confused with op271 cost: %+v", resolution.CostPolicy)
|
||||
}
|
||||
advanced, err := resolveCurrentChronicleGrowthMetadata(catalog, source, 10094063, 700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if advanced.UpgradeLevel != 5 || advanced.MaterialItemID != 3311 || advanced.ConditionMaximum != 90 {
|
||||
t.Fatalf("advanced resolution = %+v", advanced)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCurrentEquipmentEmancipationMetadata(t *testing.T) {
|
||||
source := dungeonDropCatalogTestSource{
|
||||
"monster/monster.lst": "",
|
||||
"stackable/stackable.lst": "10149520 `emancipate/ticket.stk`\n10149509 `material/relic.stk`\n",
|
||||
"equipment/equipment.lst": "104000224 `weapon/source.equ`\n104000504 `weapon/output.equ`\n",
|
||||
"stackable/emancipate/ticket.stk": "[name] `Relic Conversion`\n[stackable type] `[etc]` 1\n[emancipate ticket] 8\n",
|
||||
"stackable/material/relic.stk": "[name] `Relic Fragment`\n[stackable type] `[material]` 1\n",
|
||||
"equipment/weapon/source.equ": "[name] `Source`\n[equipment type] `[weapon]` 17\n[rarity] 3\n[minimum level] 15\n" +
|
||||
"[emancipate]\n[input] 10149509 10 10099773 12 10100116 24 10100115 36 [/input]\n" +
|
||||
"[output] 104000504 1 [/output]\n[emancipate type] 8\n[/emancipate]\n",
|
||||
"equipment/weapon/output.equ": "[name] `Relic Output`\n[equipment type] `[weapon]` 17\n[rarity] 4\n[grade] 50\n[minimum level] 15\n[durability] 35\n",
|
||||
}
|
||||
catalog, err := newPVFDungeonDropCatalog(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolution, err := resolveCurrentChronicleGrowthMetadata(catalog, source, 10149520, 104000224)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conversion := resolution.Conversion
|
||||
if conversion == nil || conversion.TicketType != 8 || conversion.OutputItemID != 104000504 || !conversion.BindOutput ||
|
||||
conversion.OutputRarity != 4 || conversion.OutputMinimumLevel != 15 || conversion.OutputDurability != 35 || len(conversion.Inputs) != 4 {
|
||||
t.Fatalf("conversion = %+v", conversion)
|
||||
}
|
||||
if conversion.Inputs[0].ItemID != 10149509 || conversion.Inputs[0].Count != 10 ||
|
||||
conversion.UpgradeMaximum != -1 || conversion.AmplifyMaximum != -1 || conversion.ForgingMaximum != -1 {
|
||||
t.Fatalf("conversion inputs/caps = %+v", conversion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCurrentChronicleGrowthCostPolicies(t *testing.T) {
|
||||
document, err := dnfpvf.Parse(currentChronicleGrowthCostPVFPath, "[consume item by rarity]\n5 3311 3 1.00\n[/consume item by rarity]\n[weight by weapon]\nweapon 1.20 coat 1.00 wrisat 1.00\n[/weight by weapon]\n[weight by level]\n1 70 1.00 2.00\n[/weight by level]\n[weight by upgrade]\n0 20 1.00 40.00\n[/weight by upgrade]\n[weight by amplify]\n0 20 1.00 144.00\n[/weight by amplify]\n[weight by seperate]\n0 7 1.00 3.00\n[/weight by seperate]\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policies, err := parseCurrentChronicleGrowthCostPolicies(document)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policy := policies[5]
|
||||
if policy.MaterialItemID != 3311 || policy.BaseCount != 3 || policy.Multiplier != 1 || policy.PartWeights["weapon"] != 1.2 || len(policy.ForgingWeights) != 1 {
|
||||
t.Fatalf("policy = %+v", policy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCurrentChronicleGrowthMetadataRealPVF(t *testing.T) {
|
||||
path := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if path == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify current chronicle growth data")
|
||||
}
|
||||
source, err := platformpvf.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := newPVFDungeonDropCatalog(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
ticket, target int64
|
||||
upgrade int
|
||||
}{{10094062, 116010135, 3}, {10094063, 100260612, 5}} {
|
||||
resolution, err := resolveCurrentChronicleGrowthMetadata(catalog, source, test.ticket, test.target)
|
||||
if err != nil {
|
||||
t.Fatalf("ticket=%d target=%d: %v", test.ticket, test.target, err)
|
||||
}
|
||||
if resolution.MaterialItemID != 3311 || resolution.UpgradeLevel != test.upgrade || resolution.TargetMinimumLevel != 70 ||
|
||||
resolution.ConditionMaximum != 90 || resolution.TargetRarity != 5 || resolution.EquipmentType == "" ||
|
||||
resolution.CostPolicy.BaseCount != 3 || len(resolution.Probability) != 2 {
|
||||
t.Fatalf("ticket=%d real PVF resolution = %+v", test.ticket, resolution)
|
||||
}
|
||||
}
|
||||
conversion, err := resolveCurrentChronicleGrowthMetadata(catalog, source, 10149520, 104000224)
|
||||
if err != nil {
|
||||
t.Fatalf("real PVF relic conversion: %v", err)
|
||||
}
|
||||
if conversion.Conversion == nil || conversion.Conversion.OutputItemID != 104000504 ||
|
||||
conversion.Conversion.TicketType != 8 || len(conversion.Conversion.Inputs) != 4 ||
|
||||
conversion.Conversion.Inputs[0].ItemID != 10149509 || conversion.Conversion.Inputs[0].Count != 10 {
|
||||
t.Fatalf("real PVF relic conversion = %+v", conversion.Conversion)
|
||||
}
|
||||
liveConversion, err := resolveCurrentChronicleGrowthMetadata(catalog, source, 10149529, 101030330)
|
||||
if err != nil {
|
||||
t.Fatalf("live Lv85 relic conversion: %v", err)
|
||||
}
|
||||
if liveConversion.Conversion == nil || len(liveConversion.Conversion.Inputs) != 4 ||
|
||||
liveConversion.Conversion.OutputItemID != 101030654 || liveConversion.Conversion.TicketType != 17 ||
|
||||
liveConversion.Conversion.Inputs[0].ItemID != 10149518 || liveConversion.Conversion.Inputs[0].Count != 10 ||
|
||||
liveConversion.Conversion.Inputs[1].Count != 12 || liveConversion.Conversion.Inputs[2].Count != 24 ||
|
||||
liveConversion.Conversion.Inputs[3].Count != 36 {
|
||||
t.Fatalf("live Lv85 relic conversion = %+v", liveConversion.Conversion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAlignedChronicleTargetRefreshBeforeAck(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
raw := make([]byte, currentItemListEntryWireSize)
|
||||
raw[0x58] = 3
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "25", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:25": {ItemID: 116010135, Count: 1, RawEntry: raw, Extra: map[string]string{"byte_58": "3"}},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{accountID: "dnf:1", gameUpperHeader: gameUpperHeaderServer16, gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
}
|
||||
session := &gameSession{conn: connection, connID: "chronicle-order", channel: channelcatalog.Channel{ID: 1}, selectedCharacterID: 25}
|
||||
ack := []byte{1, 1, 2, 0, 84, 0, 1, 0, 0, 0, 0, 160, 0, 7, 0, 0, 0}
|
||||
result := alignedcmd.Result{
|
||||
Operation: "upgrade_chronicle",
|
||||
PreResponseItemSlotRefreshes: []alignedcmd.ItemSlotRefresh{{ListType: 0, SlotIndex: 25}},
|
||||
UpperResponses: []alignedcmd.UpperResponse{{MsgID: uint16(dnfenum.CmdPacketUpgradeChronicle), Body: ack, Classification: dnfproto.DefaultChannelClassification, AllowCodec: true}},
|
||||
}
|
||||
if err := service.sendAlignedUpperResponses(session, result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refresh, rest := splitLongHengGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if refresh.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) || refresh.Header.Classification != 0 || refresh.Body[0] != 0 {
|
||||
t.Fatalf("first packet header=%+v body=%x", refresh.Header, refresh.Body)
|
||||
}
|
||||
response, trailing := splitLongHengGameServerUpperPacket(t, rest)
|
||||
if response.Header.MsgID != uint16(dnfenum.CmdPacketUpgradeChronicle) || !bytes.Equal(response.Body, ack) {
|
||||
t.Fatalf("second packet header=%+v body=%x", response.Header, response.Body)
|
||||
}
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("unexpected trailing bytes: %x", trailing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAlignedEquipmentEmancipationAckThenInvalidateThenRefresh(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
targetRaw := make([]byte, currentItemListEntryWireSize)
|
||||
binary.LittleEndian.PutUint16(targetRaw[0:2], 25)
|
||||
binary.LittleEndian.PutUint32(targetRaw[2:6], 104000504)
|
||||
binary.LittleEndian.PutUint32(targetRaw[6:10], 1)
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "26", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:25": {ItemID: 104000504, Count: 1, Bind: true, RawEntry: targetRaw, Extra: map[string]string{"item_kind": "equipment"}},
|
||||
"0:84": {ItemID: 10149520, Count: 1},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{accountID: "dnf:1", gameUpperHeader: gameUpperHeaderServer16, gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
}
|
||||
session := &gameSession{conn: connection, connID: "emancipation-order", channel: channelcatalog.Channel{ID: 1}, selectedCharacterID: 26}
|
||||
ack := []byte{1, 1, 1, 0, 84, 0, 1, 0, 0, 0}
|
||||
result := alignedcmd.Result{
|
||||
Operation: "upgrade_chronicle",
|
||||
UpperResponses: []alignedcmd.UpperResponse{{
|
||||
MsgID: uint16(dnfenum.CmdPacketUpgradeChronicle), Body: ack,
|
||||
Classification: dnfproto.DefaultChannelClassification, AllowCodec: true,
|
||||
}},
|
||||
PostResponseItemSlotClears: []alignedcmd.ItemSlotRefresh{{ListType: 0, SlotIndex: 25}},
|
||||
ItemSlotRefreshes: []alignedcmd.ItemSlotRefresh{
|
||||
{ListType: 0, SlotIndex: 25},
|
||||
{ListType: 0, SlotIndex: 84},
|
||||
},
|
||||
}
|
||||
if err := service.sendAlignedUpperResponses(session, result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, rest := splitLongHengGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if response.Header.MsgID != uint16(dnfenum.CmdPacketUpgradeChronicle) || response.Header.Classification != 1 || !bytes.Equal(response.Body, ack) {
|
||||
t.Fatalf("first packet header=%+v body=%x", response.Header, response.Body)
|
||||
}
|
||||
clear, rest := splitLongHengGameServerUpperPacket(t, rest)
|
||||
if clear.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) || clear.Header.Classification != 0 ||
|
||||
len(clear.Body) != 3+currentItemListEntryWireSize || binary.LittleEndian.Uint16(clear.Body[3:5]) != 25 ||
|
||||
binary.LittleEndian.Uint32(clear.Body[5:9]) != ^uint32(0) {
|
||||
t.Fatalf("second packet header=%+v body=%x", clear.Header, clear.Body)
|
||||
}
|
||||
refresh, trailing := splitLongHengGameServerUpperPacket(t, rest)
|
||||
if refresh.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) || refresh.Header.Classification != 0 ||
|
||||
len(refresh.Body) != 3+2*currentItemListEntryWireSize || binary.LittleEndian.Uint16(refresh.Body[3:5]) != 25 ||
|
||||
binary.LittleEndian.Uint32(refresh.Body[5:9]) != 104000504 {
|
||||
t.Fatalf("third packet header=%+v body=%x", refresh.Header, refresh.Body)
|
||||
}
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("unexpected trailing bytes: %x", trailing)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user