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,300 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
dnfguild "longheng.io/server/internal/modules/dnf/guild"
|
||||
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 warOperationSource() dungeonDropCatalogTestSource {
|
||||
text := "[guardian list]\n[guardian]\n1\n[ap]\n50000\n[gp]\n1000\n"
|
||||
for i := 1; i <= 5; i++ {
|
||||
text += fmt.Sprintf("[skill_%d]\n0 1 5\n", i)
|
||||
}
|
||||
text += "[/guardian]\n[/guardian list]\n[guardian exp]\n1 18 0 2 83 0 3 237 20\n[/guardian exp]\n[guardian skill]\n1 1500 0 2 15000 200 3 50000 1000 4 100000 5000\n[/guardian skill]\n[shop list]\n201 202 211 212\n[/shop list]\n[building upgrade]\n[building]\n0 0 1000 0\n[/building]\n[mecenary]\n0 0 1000 0\n[/mecenary]\n[/building upgrade]"
|
||||
source := dungeonDropCatalogTestSource{
|
||||
"monster/monster.lst": "", "equipment/equipment.lst": "",
|
||||
"etc/agitwar.etc": text,
|
||||
"npc/npc.lst": "390 `workshop.npc` 389 `pub.npc`",
|
||||
"npc/workshop.npc": "[guild agit shop npc]\n`defence`\n[upgrade]\n`guild agit` 1 421 2 422\n[/upgrade]",
|
||||
"npc/pub.npc": "[guild agit shop npc]\n`mercenary`\n[upgrade]\n`guild agit` 1 411 2 412\n[/upgrade]",
|
||||
"itemshop/itemshop.lst": "201 `workshop.shp` 202 `workshop_2.shp` 211 `pub.shp` 212 `pub_2.shp`",
|
||||
"stackable/stackable.lst": "10161523 `cannon.stk` 10161524 `cannon2.stk`",
|
||||
"stackable/cannon.stk": "[name]\n`cannon`\n[stackable type]\n`[expert town potion]` 0\n[stack limit]\n2\n[require ap]\n7\n[minimum level]\n17",
|
||||
"stackable/cannon2.stk": "[name]\n`cannon2`\n[stackable type]\n`[expert town potion]` 0\n[stack limit]\n2\n[require ap]\n9\n[minimum level]\n17",
|
||||
}
|
||||
for _, row := range []struct {
|
||||
path string
|
||||
npc, item int
|
||||
}{{"workshop.shp", 421, 10161523}, {"workshop_2.shp", 422, 10161524}, {"pub.shp", 411, 10161523}, {"pub_2.shp", 412, 10161524}} {
|
||||
source["itemshop/"+row.path] = fmt.Sprintf("[NPC]\n%d\n[type]\n`[agitwar build shop]`\n[item list]\n%d\n[/item list]", row.npc, row.item)
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
func TestCurrentGuildWarOperationsCapturedRequests(t *testing.T) {
|
||||
for _, legacy := range []bool{false, true} {
|
||||
t.Run(fmt.Sprint(legacy), func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
fail := &guildFailSettlement{base: repos.CharacterSettlement}
|
||||
repos.CharacterSettlement = fail
|
||||
master := dnfrepo.CharacterRecord{CharacterID: "16", AccountID: "dnf:master", Name: "master", Level: 90, Stats: map[string]int64{"gold": 10000}}
|
||||
if err := repos.Character.Save(ctx, master); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "16", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := dnfguild.NewRegistry()
|
||||
g, err := r.Create(master, "war", serviceTestNow())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Guardians = []dnfguild.Guardian{{ID: 1, Level: 1}}
|
||||
g.ActiveGuardian = 1
|
||||
g.GuardianAP = 10000
|
||||
g.GuardianGP = 2000
|
||||
g.Mark = 13
|
||||
if err = dnfguild.Save(ctx, repos.Settings, r, serviceTestNow()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
items, err := newPVFDungeonDropCatalog(warOperationSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ss := &gameSession{conn: &bufferConn{}, selectedCharacterID: 16, accountID: master.AccountID, initialTownRouteCharacterID: 16, initialTownRouteStage: currentInitialTownRoutePlayerStateSent}
|
||||
s := &Service{options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain}, pvfItemCatalog: items, repositoryProvider: func() (dnfrepo.Group, bool) { return repos, true }}
|
||||
load := func() (*dnfguild.Guild, dnfrepo.InventoryRecord) {
|
||||
t.Helper()
|
||||
r, e := dnfguild.Load(ctx, repos.Settings)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
g, _ := r.GuildByID(g.ID)
|
||||
inv, ok, e := repos.Inventory.Load(ctx, "16")
|
||||
if e != nil || !ok {
|
||||
t.Fatal(e)
|
||||
}
|
||||
return g, inv
|
||||
}
|
||||
send := func(op uint16, body []byte, code byte) []byte {
|
||||
t.Helper()
|
||||
ss.conn.(*bufferConn).write.Reset()
|
||||
h := guildGameplayModule().UpperHandlers[op]
|
||||
if legacy {
|
||||
h = guildGameplayModule().LegacyHandlers[op]
|
||||
}
|
||||
if e := h(s, ss, gameplayRequest{Opcode: op, Classification: 1, Body: body}); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, ss.conn.(*bufferConn).write.Bytes())
|
||||
want := []byte{1}
|
||||
if code != 0 {
|
||||
want = []byte{0, code}
|
||||
}
|
||||
if ack.Header.MsgID != op || ack.Header.Classification != 1 || !bytes.Equal(ack.Body, want) {
|
||||
t.Fatalf("op%d ACK %x want %x", op, ack.Body, want)
|
||||
}
|
||||
if code != 0 && len(rest) > 0 {
|
||||
t.Fatal("failure sent state")
|
||||
}
|
||||
return rest
|
||||
}
|
||||
words := func(v ...uint32) []byte {
|
||||
b := make([]byte, 4*len(v))
|
||||
for i, n := range v {
|
||||
binary.LittleEndian.PutUint32(b[i*4:], n)
|
||||
}
|
||||
return b
|
||||
}
|
||||
feed, _ := hex.DecodeString("0000000001000000000000000b000000")
|
||||
skill, _ := hex.DecodeString("02000000010000000000000001000000")
|
||||
upgrade, _ := hex.DecodeString("438bdf8180f71a00b8f61a00c4860100000100000002000000")
|
||||
buy, _ := hex.DecodeString("730d9b0001000000c9000000a5010000")
|
||||
send(1350, feed, 0)
|
||||
send(1350, feed, 0)
|
||||
a, _ := load()
|
||||
if a.GuardianAP != 9989 || a.Guardians[0].APExp != 11 || a.Guardians[0].Level != 1 {
|
||||
t.Fatal("partial feed/retry")
|
||||
}
|
||||
send(1350, words(0, 1, 11, 7), 0)
|
||||
a, _ = load()
|
||||
if a.GuardianAP != 9982 || a.Guardians[0].APExp != 18 || a.Guardians[0].Level != 2 {
|
||||
t.Fatal("level transition lost cumulative experience")
|
||||
}
|
||||
send(1350, skill, 0)
|
||||
send(1350, skill, 0)
|
||||
a, _ = load()
|
||||
if a.Guardians[0].Skills[0] != 1 || a.GuardianAP != 9982 {
|
||||
t.Fatal("free initial skill unlock/retry")
|
||||
}
|
||||
send(1350, words(2, 1, 1, 2), 0)
|
||||
send(1350, words(2, 1, 1, 2), 0)
|
||||
a, _ = load()
|
||||
if a.GuardianAP != 8482 || a.Guardians[0].Skills[0] != 2 {
|
||||
t.Fatal("skill level cost/retry")
|
||||
}
|
||||
send(1350, words(3, 1, 0, 1), 3) // second skill needs the second star
|
||||
send(1350, words(0, 1, 0, 1), 3) // stale experience
|
||||
send(1350, words(0, 1, 18, 1000), 3) // past current threshold
|
||||
send(1350, words(2, 1, 2, 3), 22) // actual AP/GP cost exceeds balance
|
||||
before, invBefore := load()
|
||||
fail.fail = true
|
||||
send(1354, buy, 3)
|
||||
fail.fail = false
|
||||
a, inv := load()
|
||||
if !reflect.DeepEqual(a, before) || !reflect.DeepEqual(inv, invBefore) {
|
||||
t.Fatal("rolled-back purchase changed guild or inventory")
|
||||
}
|
||||
rest := send(1354, buy, 0)
|
||||
itemPacket, rest := splitGameServerUpperPacket(t, rest)
|
||||
state, tail := splitGameServerUpperPacket(t, rest)
|
||||
if itemPacket.Header.MsgID != 14 || state.Header.MsgID != 1297 || len(tail) != 0 {
|
||||
t.Fatal("purchase refresh order")
|
||||
}
|
||||
a, inv = load()
|
||||
count := int64(0)
|
||||
for _, item := range inv.Slots {
|
||||
if item.ItemID == 10161523 {
|
||||
count += item.Count
|
||||
}
|
||||
}
|
||||
if a.GuardianAP != 8475 || count != 1 {
|
||||
t.Fatal("purchase AP debit/grant")
|
||||
}
|
||||
// Failed upgrade rolls back level and balance; retry succeeds once.
|
||||
before, _ = load()
|
||||
fail.fail = true
|
||||
send(840, upgrade, 3)
|
||||
fail.fail = false
|
||||
a, _ = load()
|
||||
if !reflect.DeepEqual(a, before) {
|
||||
t.Fatal("failed upgrade persisted")
|
||||
}
|
||||
rest = send(840, upgrade, 0)
|
||||
state, rest = splitGameServerUpperPacket(t, rest)
|
||||
notice, tail := splitGameServerUpperPacket(t, rest)
|
||||
if state.Header.MsgID != 1297 || binary.LittleEndian.Uint16(state.Body[2166:]) != 2 || binary.LittleEndian.Uint16(state.Body[2168:]) != 1 || notice.Header.MsgID != 778 || !bytes.Equal(notice.Body, []byte{0, 2, 0, 0, 0}) || len(tail) != 0 {
|
||||
t.Fatal("native merchant level projection")
|
||||
}
|
||||
send(840, upgrade, 0)
|
||||
a, _ = load()
|
||||
if a.GuardianAP != 7475 || a.WarShopLevel(0) != 2 || a.Mark != 13 {
|
||||
t.Fatal("upgrade retry or guild state")
|
||||
}
|
||||
send(1354, buy, 3) // stale level-1 shop after upgrade
|
||||
send(1354, words(10161524, 3, 202, 422), 0)
|
||||
a, inv = load()
|
||||
count = 0
|
||||
for _, item := range inv.Slots {
|
||||
if item.ItemID == 10161524 {
|
||||
count += item.Count
|
||||
if item.Count > 2 {
|
||||
t.Fatal("stack overflow")
|
||||
}
|
||||
}
|
||||
}
|
||||
if a.GuardianAP != 7448 || count != 3 {
|
||||
t.Fatal("upgraded shop item grant")
|
||||
}
|
||||
// A partial existing stack must roll back too if the remainder has
|
||||
// no room. The failure ACK releases the native purchase-busy flag.
|
||||
inv.Slots = fullCurrentNPCShopWasteInventory()
|
||||
inv.Slots[currentCeraShopInventorySlotKey(0, 65)] = dnfrepo.ItemStack{ItemID: 10161524, Count: 1}
|
||||
if e := repos.Inventory.Save(ctx, inv); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
before, invBefore = load()
|
||||
send(1354, words(10161524, 3, 202, 422), 136)
|
||||
a, inv = load()
|
||||
if !reflect.DeepEqual(a, before) || !reflect.DeepEqual(inv, invBefore) {
|
||||
t.Fatal("full backpack purchase changed guild or partial stack")
|
||||
}
|
||||
for _, body := range [][]byte{nil, upgrade[:24], append(append([]byte{}, upgrade...), 0)} {
|
||||
send(840, body, 3)
|
||||
}
|
||||
send(1354, words(10161524, 0, 202, 422), 3)
|
||||
send(1354, words(10161524, 1, 202, 411), 3)
|
||||
ss.accountID = "dnf:wrong"
|
||||
send(1354, words(10161524, 1, 202, 422), 3)
|
||||
ss.accountID = master.AccountID
|
||||
stored, e := dnfguild.Load(ctx, repos.Settings)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
storedGuild, _ := stored.GuildByID(g.ID)
|
||||
member := storedGuild.Members["16"]
|
||||
member.Grade = 5
|
||||
storedGuild.Members["16"] = member
|
||||
if e = dnfguild.Save(ctx, repos.Settings, stored, serviceTestNow()); e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
send(1354, words(10161524, 1, 202, 422), 36)
|
||||
send(840, upgrade, 36)
|
||||
c, _, e := repos.Character.Load(ctx, "16")
|
||||
if e != nil || c.Stats["gold"] != 10000 {
|
||||
t.Fatal("guild shop changed gold")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentGuildWarRealPVFCatalog(t *testing.T) {
|
||||
name := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if name == "" {
|
||||
t.Skip("real PVF not selected")
|
||||
}
|
||||
archive, err := platformpvf.Open(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
growth, err := dnfguild.LoadGuardianGrowth(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(growth.Experience) != 49 || growth.Experience[1] != (dnfguild.GuardianCost{AP: 18}) || growth.Experience[20] != (dnfguild.GuardianCost{AP: 66247, GP: 604}) || growth.Skills[1].AP != 1500 || growth.Skills[0] != (dnfguild.GuardianCost{}) || len(growth.SkillLimits) != 2 {
|
||||
t.Fatal("actual guardian rules")
|
||||
}
|
||||
shops, err := dnfguild.LoadWarShops(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
items, err := newPVFDungeonDropCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for kind, merchant := range shops {
|
||||
if len(merchant.Levels) != 10 || merchant.Costs[2].AP != 1000 || merchant.Costs[10] != (dnfguild.GuardianCost{AP: 95000, GP: 5000}) {
|
||||
t.Fatalf("actual merchant%d", kind)
|
||||
}
|
||||
for _, shop := range merchant.Levels {
|
||||
for item := range shop.Items {
|
||||
def, e := items.ResolveItem(item)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if _, e = dnfguild.LoadWarProductCost(archive, def.PVFPath); e != nil {
|
||||
t.Fatalf("product%d: %v", item, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !shops[0].Levels[1].Items[10161523] || shops[0].Levels[1].NPC != 421 || shops[0].Levels[1].ID != 201 {
|
||||
t.Fatal("captured cannon context")
|
||||
}
|
||||
cost, err := dnfguild.LoadWarProductCost(archive, "stackable/10161001/10161523.stk")
|
||||
if err != nil || cost != (dnfguild.GuardianCost{AP: 7}) {
|
||||
t.Fatal("actual cannon price", cost, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user