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,259 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
// newRenameRepairTestService builds the minimum service the repair needs: only
|
||||
// the repository provider is exercised, no PVF catalog and no packet logger.
|
||||
func newRenameRepairTestService(repositories dnfrepo.Group) *Service {
|
||||
return &Service{
|
||||
options: options{accountID: "account-1"},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
}
|
||||
}
|
||||
|
||||
func seedRenameRepairCharacter(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
repositories dnfrepo.Group,
|
||||
characterID string,
|
||||
stats map[string]int64,
|
||||
slots map[string]dnfrepo.ItemStack,
|
||||
) {
|
||||
t.Helper()
|
||||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: characterID,
|
||||
AccountID: "account-1",
|
||||
Level: 90,
|
||||
Stats: stats,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: characterID, Slots: slots}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadRenameRepairInventory(t *testing.T, ctx context.Context, repositories dnfrepo.Group, characterID string) dnfrepo.InventoryRecord {
|
||||
t.Helper()
|
||||
inventory, found, err := repositories.Inventory.Load(ctx, characterID)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory %s found=%t err=%v", characterID, found, err)
|
||||
}
|
||||
return inventory
|
||||
}
|
||||
|
||||
func loadRenameRepairCharacter(t *testing.T, ctx context.Context, repositories dnfrepo.Group, characterID string) dnfrepo.CharacterRecord {
|
||||
t.Helper()
|
||||
character, found, err := repositories.Character.Load(ctx, characterID)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character %s found=%t err=%v", characterID, found, err)
|
||||
}
|
||||
return character
|
||||
}
|
||||
|
||||
// TestCharacterRenameShopCardRepairDeletesTheBagCardAndArmsTheRename is the live
|
||||
// incident, reproduced: character 3333 bought commodity 100902 (item 2682290),
|
||||
// which landed in bag slot 0:87 because the product was not recognized as a
|
||||
// rename card. The repair must delete that row and arm the pending rename in one
|
||||
// step, and must not disturb any other row.
|
||||
func TestCharacterRenameShopCardRepairDeletesTheBagCardAndArmsTheRename(t *testing.T) {
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
ctx := context.Background()
|
||||
cardKey := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 87)
|
||||
otherKey := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 5)
|
||||
seedRenameRepairCharacter(t, ctx, repositories, "5",
|
||||
map[string]int64{"fatigue_battery_charge": 834},
|
||||
map[string]dnfrepo.ItemStack{
|
||||
cardKey: {ItemID: currentCharacterRenameShopCardItemID, Count: 1},
|
||||
otherKey: {ItemID: 2660671, Count: 400},
|
||||
},
|
||||
)
|
||||
|
||||
service := newRenameRepairTestService(repositories)
|
||||
if !service.repairCurrentCharacterRenameShopCardsForAccount(ctx, "account-1", []dnfrepo.CharacterRecord{{CharacterID: "5"}}) {
|
||||
t.Fatal("repair reported no change for a bag holding the shop rename card")
|
||||
}
|
||||
|
||||
inventory := loadRenameRepairInventory(t, ctx, repositories, "5")
|
||||
if _, stillThere := inventory.Slots[cardKey]; stillThere {
|
||||
t.Fatalf("shop rename card row %s survived the repair: %+v", cardKey, inventory.Slots[cardKey])
|
||||
}
|
||||
if other := inventory.Slots[otherKey]; other.ItemID != 2660671 || other.Count != 400 {
|
||||
t.Fatalf("repair touched an unrelated row: %+v", other)
|
||||
}
|
||||
character := loadRenameRepairCharacter(t, ctx, repositories, "5")
|
||||
if got := character.Stats[currentCharacterRenameEntitlementStat]; got != 1 {
|
||||
t.Fatalf("entitlement=%d want 1 (stats=%v)", got, character.Stats)
|
||||
}
|
||||
if got := character.Stats["fatigue_battery_charge"]; got != 834 {
|
||||
t.Fatalf("repair dropped an unrelated stat: %v", character.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCharacterRenameShopCardRepairIsIdempotentAndRemovesAStrayCardWhenArmed
|
||||
// locks two properties: a repaired character is not reported as changed on the
|
||||
// next login (so the roster is not re-read forever), and a stray shop card is
|
||||
// still deleted for a character that is already armed without the entitlement
|
||||
// being double counted.
|
||||
func TestCharacterRenameShopCardRepairIsIdempotentAndRemovesAStrayCardWhenArmed(t *testing.T) {
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
ctx := context.Background()
|
||||
cardKey := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 87)
|
||||
seedRenameRepairCharacter(t, ctx, repositories, "5", map[string]int64{},
|
||||
map[string]dnfrepo.ItemStack{cardKey: {ItemID: currentCharacterRenameShopCardItemID, Count: 2}},
|
||||
)
|
||||
|
||||
service := newRenameRepairTestService(repositories)
|
||||
characters := []dnfrepo.CharacterRecord{{CharacterID: "5"}}
|
||||
if !service.repairCurrentCharacterRenameShopCardsForAccount(ctx, "account-1", characters) {
|
||||
t.Fatal("first repair reported no change")
|
||||
}
|
||||
// Only lvl 90 / no cooldown, so the whole stack goes and the entitlement is
|
||||
// armed exactly once even though two cards were present.
|
||||
if _, stillThere := loadRenameRepairInventory(t, ctx, repositories, "5").Slots[cardKey]; stillThere {
|
||||
t.Fatal("a second card in the same stack survived")
|
||||
}
|
||||
if got := loadRenameRepairCharacter(t, ctx, repositories, "5").Stats[currentCharacterRenameEntitlementStat]; got != 1 {
|
||||
t.Fatalf("entitlement=%d want exactly 1", got)
|
||||
}
|
||||
if service.repairCurrentCharacterRenameShopCardsForAccount(ctx, "account-1", characters) {
|
||||
t.Fatal("second repair reported a change; a clean roster must not be re-read")
|
||||
}
|
||||
|
||||
// Already armed, stray card arrives later: the card goes, the entitlement
|
||||
// stays 1 (never incremented).
|
||||
seedRenameRepairCharacter(t, ctx, repositories, "6",
|
||||
map[string]int64{currentCharacterRenameEntitlementStat: 1},
|
||||
map[string]dnfrepo.ItemStack{cardKey: {ItemID: currentCharacterRenameShopCardItemID, Count: 1}},
|
||||
)
|
||||
if !service.repairCurrentCharacterRenameShopCardsForAccount(ctx, "account-1", []dnfrepo.CharacterRecord{{CharacterID: "6"}}) {
|
||||
t.Fatal("repair skipped a stray card on an armed character")
|
||||
}
|
||||
if _, stillThere := loadRenameRepairInventory(t, ctx, repositories, "6").Slots[cardKey]; stillThere {
|
||||
t.Fatal("stray card survived on an armed character")
|
||||
}
|
||||
if got := loadRenameRepairCharacter(t, ctx, repositories, "6").Stats[currentCharacterRenameEntitlementStat]; got != 1 {
|
||||
t.Fatalf("entitlement=%d want 1 (must not stack)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCharacterRenameShopCardRepairLeavesCharactersInsideTheCooldownAlone is the
|
||||
// operator's rule: inside the 90-day window nothing at all happens - the card is
|
||||
// not deleted and no entitlement is granted, because arming a character that is
|
||||
// forbidden to rename would lock the account out of the game.
|
||||
func TestCharacterRenameShopCardRepairLeavesCharactersInsideTheCooldownAlone(t *testing.T) {
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
ctx := context.Background()
|
||||
cardKey := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 87)
|
||||
now := time.Now().UTC()
|
||||
lastRename := now.Add(-30 * 24 * time.Hour).Unix()
|
||||
seedRenameRepairCharacter(t, ctx, repositories, "1",
|
||||
map[string]int64{currentCharacterRenameLastUnixStat: lastRename},
|
||||
map[string]dnfrepo.ItemStack{cardKey: {ItemID: currentCharacterRenameShopCardItemID, Count: 1}},
|
||||
)
|
||||
|
||||
service := newRenameRepairTestService(repositories)
|
||||
if service.repairCurrentCharacterRenameShopCardsForAccount(ctx, "account-1", []dnfrepo.CharacterRecord{{CharacterID: "1"}}) {
|
||||
t.Fatal("repair changed state for a character inside the cooldown")
|
||||
}
|
||||
if _, stillThere := loadRenameRepairInventory(t, ctx, repositories, "1").Slots[cardKey]; !stillThere {
|
||||
t.Fatal("repair deleted the card of a character inside the cooldown")
|
||||
}
|
||||
character := loadRenameRepairCharacter(t, ctx, repositories, "1")
|
||||
if got := character.Stats[currentCharacterRenameEntitlementStat]; got != 0 {
|
||||
t.Fatalf("repair armed a character inside the cooldown: entitlement=%d", got)
|
||||
}
|
||||
if got := character.Stats[currentCharacterRenameLastUnixStat]; got != lastRename {
|
||||
t.Fatalf("repair moved the rename clock: %d want %d", got, lastRename)
|
||||
}
|
||||
|
||||
// Past the window the very same character must be repaired on the next login.
|
||||
expired := now.Add(-(currentCharacterRenameCooldown + 24*time.Hour)).Unix()
|
||||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "1", AccountID: "account-1", Level: 90,
|
||||
Stats: map[string]int64{
|
||||
currentCharacterRenameLastUnixStat: expired,
|
||||
currentCharacterRenameEntitlementStat: 0,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !service.repairCurrentCharacterRenameShopCardsForAccount(ctx, "account-1", []dnfrepo.CharacterRecord{{CharacterID: "1"}}) {
|
||||
t.Fatal("repair did not run once the cooldown had passed")
|
||||
}
|
||||
if _, stillThere := loadRenameRepairInventory(t, ctx, repositories, "1").Slots[cardKey]; stillThere {
|
||||
t.Fatal("card survived after the cooldown passed")
|
||||
}
|
||||
if got := loadRenameRepairCharacter(t, ctx, repositories, "1").Stats[currentCharacterRenameEntitlementStat]; got != 1 {
|
||||
t.Fatalf("entitlement=%d want 1 after the cooldown passed", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCharacterRenameShopCardRepairIgnoresEverythingThatIsNotTheShopCard keeps
|
||||
// the repair surgical: the bag rename card the client can actually use
|
||||
// (2660698) and the account-bound event cards must not be touched, and the
|
||||
// personal cargo / warehouse copies are out of scope.
|
||||
func TestCharacterRenameShopCardRepairIgnoresEverythingThatIsNotTheShopCard(t *testing.T) {
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
ctx := context.Background()
|
||||
bagCardKey := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 117)
|
||||
cargoCardKey := currentCeraShopInventorySlotKey(currentExeInventoryPersonalCargoListType, 3)
|
||||
seedRenameRepairCharacter(t, ctx, repositories, "1", map[string]int64{},
|
||||
map[string]dnfrepo.ItemStack{
|
||||
// 490000735 is the account-bound bag card the operator holds 100 of.
|
||||
bagCardKey: {ItemID: 490000735, Count: 100},
|
||||
cargoCardKey: {ItemID: currentCharacterRenameShopCardItemID, Count: 1},
|
||||
},
|
||||
)
|
||||
|
||||
service := newRenameRepairTestService(repositories)
|
||||
if service.repairCurrentCharacterRenameShopCardsForAccount(ctx, "account-1", []dnfrepo.CharacterRecord{{CharacterID: "1"}}) {
|
||||
t.Fatal("repair ran for a bag that holds no shop rename card")
|
||||
}
|
||||
inventory := loadRenameRepairInventory(t, ctx, repositories, "1")
|
||||
if stack := inventory.Slots[bagCardKey]; stack.ItemID != 490000735 || stack.Count != 100 {
|
||||
t.Fatalf("repair touched the bag rename card: %+v", stack)
|
||||
}
|
||||
if stack := inventory.Slots[cargoCardKey]; stack.ItemID != currentCharacterRenameShopCardItemID {
|
||||
t.Fatalf("repair touched the personal cargo copy: %+v", stack)
|
||||
}
|
||||
if got := loadRenameRepairCharacter(t, ctx, repositories, "1").Stats[currentCharacterRenameEntitlementStat]; got != 0 {
|
||||
t.Fatalf("repair armed a character holding no shop card: entitlement=%d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCharacterRenameShopCardRepairTargetAndScopeAreLocked pins the two things
|
||||
// this repair keys on: the item id it hunts for (so a PVF change that renumbers
|
||||
// the shop product cannot leave it hunting a ghost) and the container scope (main
|
||||
// bag only, via the slot-key prefix).
|
||||
func TestCharacterRenameShopCardRepairTargetAndScopeAreLocked(t *testing.T) {
|
||||
if currentCharacterRenameShopCardItemID != 2682290 {
|
||||
t.Fatalf("shop rename card item id changed to %d; update the repair and this lock together",
|
||||
currentCharacterRenameShopCardItemID)
|
||||
}
|
||||
if !currentCharacterRenameAutoUsePVFPath("stackable/cash/chn_character_rename.stk") {
|
||||
t.Fatal("the repair's item id no longer belongs to the auto-use rename product path")
|
||||
}
|
||||
// The scope test is what keeps a personal-cargo copy out of the repair.
|
||||
mainBag := currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 87)
|
||||
cargo := currentCeraShopInventorySlotKey(currentExeInventoryPersonalCargoListType, 3)
|
||||
inventory := dnfrepo.InventoryRecord{CharacterID: "1", Slots: map[string]dnfrepo.ItemStack{
|
||||
mainBag: {ItemID: currentCharacterRenameShopCardItemID, Count: 1},
|
||||
cargo: {ItemID: currentCharacterRenameShopCardItemID, Count: 1},
|
||||
}}
|
||||
keys, count := currentCharacterRenameShopCardSlots(inventory)
|
||||
if count != 1 || len(keys) != 1 || keys[0] != mainBag {
|
||||
t.Fatalf("scope keys=%v count=%d want only %s", keys, count, mainBag)
|
||||
}
|
||||
if got := strconv.Itoa(int(dnfrepo.MainInventoryListType)); got != "0" {
|
||||
t.Fatalf("main inventory list type changed to %s; the scope prefix is stale", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user