服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
965 lines
33 KiB
Go
965 lines
33 KiB
Go
// 本文件验证 C# USERINFO legacy 表到旧客户端 NOTI2 body 的编码边界。
|
|
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
|
"longheng.io/server/internal/modules/dnf/premium"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
|
)
|
|
|
|
type fakeLegacyUserInfoRepo struct {
|
|
rows map[string][]dnfrepo.LegacyUserInfoRow
|
|
}
|
|
|
|
func (r fakeLegacyUserInfoRepo) Check(context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (r fakeLegacyUserInfoRepo) SelectRows(_ context.Context, _ string, tableSuffix string, columns []string, _ []string) ([]dnfrepo.LegacyUserInfoRow, error) {
|
|
src := r.rows[tableSuffix]
|
|
out := make([]dnfrepo.LegacyUserInfoRow, 0, len(src))
|
|
for _, row := range src {
|
|
cloned := make(dnfrepo.LegacyUserInfoRow, len(columns))
|
|
for _, column := range columns {
|
|
cloned[column] = row[column]
|
|
}
|
|
out = append(out, cloned)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r fakeLegacyUserInfoRepo) SelectOne(ctx context.Context, characterID string, tableSuffix string, columns []string) (dnfrepo.LegacyUserInfoRow, bool, error) {
|
|
rows, err := r.SelectRows(ctx, characterID, tableSuffix, columns, nil)
|
|
if err != nil || len(rows) == 0 {
|
|
return nil, false, err
|
|
}
|
|
return rows[0], true, nil
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfoSkipsWhenRepositoryMissing(t *testing.T) {
|
|
service := Service{characterStats: testCharacterStatTable(t)}
|
|
if body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, nil, 1, 0x017c); ok || body != nil {
|
|
t.Fatalf("legacy userinfo without repository = %x/%v, want skipped", body, ok)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfo23MatchesCSharpLayout(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo23_scalar_values": {
|
|
{"family": "direct", "scalar_index": "401", "source_order": "0", "value": "101"},
|
|
{"family": "e90", "scalar_index": "1", "source_order": "0", "value": "102"},
|
|
{"family": "e90", "scalar_index": "2", "source_order": "0", "value": "103"},
|
|
{"family": "e90", "scalar_index": "3", "source_order": "0", "value": "104"},
|
|
{"family": "direct", "scalar_index": "399", "source_order": "0", "value": "5"},
|
|
{"family": "e90", "scalar_index": "6", "source_order": "0", "value": "106"},
|
|
{"family": "direct", "scalar_index": "397", "source_order": "0", "value": "107"},
|
|
{"family": "e90", "scalar_index": "5", "source_order": "0", "value": "108"},
|
|
{"family": "e90", "scalar_index": "9", "source_order": "0", "value": "109"},
|
|
{"family": "direct", "scalar_index": "398", "source_order": "0", "value": "110"},
|
|
{"family": "e90", "scalar_index": "4", "source_order": "0", "value": "111"},
|
|
{"family": "e90", "scalar_index": "8", "source_order": "0", "value": "112"},
|
|
{"family": "e90", "scalar_index": "10", "source_order": "0", "value": "113"},
|
|
{"family": "e90", "scalar_index": "11", "source_order": "0", "value": "114"},
|
|
{"family": "e90", "scalar_index": "12", "source_order": "0", "value": "115"},
|
|
{"family": "e90", "scalar_index": "13", "source_order": "0", "value": "116"},
|
|
{"family": "e90", "scalar_index": "14", "source_order": "0", "value": "117"},
|
|
{"family": "db0", "scalar_index": "5", "source_order": "0", "value": "118"},
|
|
{"family": "d90", "scalar_index": "2", "source_order": "0", "value": "119"},
|
|
{"family": "d90", "scalar_index": "10", "source_order": "0", "value": "120"},
|
|
{"family": "d90", "scalar_index": "7", "source_order": "0", "value": "200"},
|
|
{"family": "d90", "scalar_index": "4", "source_order": "0", "value": "122"},
|
|
{"family": "d90", "scalar_index": "5", "source_order": "0", "value": "123"},
|
|
{"family": "d90", "scalar_index": "8", "source_order": "0", "value": "124"},
|
|
{"family": "d90", "scalar_index": "9", "source_order": "0", "value": "125"},
|
|
{"family": "d90", "scalar_index": "6", "source_order": "0", "value": "126"},
|
|
{"family": "d90", "scalar_index": "3", "source_order": "0", "value": "127"},
|
|
{"family": "d90", "scalar_index": "0", "source_order": "0", "value": "129"},
|
|
{"family": "d90", "scalar_index": "1", "source_order": "0", "value": "130"},
|
|
{"family": "d90", "scalar_index": "11", "source_order": "0", "value": "211"},
|
|
{"family": "db0", "scalar_index": "11", "source_order": "0", "value": "212"},
|
|
{"family": "e90", "scalar_index": "16", "source_order": "0", "value": "131"},
|
|
{"family": "e90", "scalar_index": "17", "source_order": "0", "value": "132"},
|
|
{"family": "e90", "scalar_index": "18", "source_order": "0", "value": "133"},
|
|
{"family": "e90", "scalar_index": "19", "source_order": "0", "value": "134"},
|
|
{"family": "e90", "scalar_index": "20", "source_order": "0", "value": "135"},
|
|
{"family": "d90", "scalar_index": "13", "source_order": "0", "value": "136"},
|
|
{"family": "e90", "scalar_index": "21", "source_order": "0", "value": "137"},
|
|
{"family": "e90", "scalar_index": "22", "source_order": "0", "value": "138"},
|
|
},
|
|
"legacy_character_userinfo23_fixed_values": {
|
|
{"section": "fixed4", "slot_index": "0", "value": "1"},
|
|
{"section": "fixed4", "slot_index": "1", "value": "2"},
|
|
{"section": "fixed4", "slot_index": "2", "value": "3"},
|
|
{"section": "fixed4", "slot_index": "3", "value": "4"},
|
|
},
|
|
"legacy_character_userinfo23_pair_entries": {
|
|
{"section": "map", "entry_key": "9", "value": "10"},
|
|
},
|
|
"legacy_character_userinfo23_object_rows": {{
|
|
"object_or_slot_id": "4660",
|
|
"value_a": "16909060",
|
|
"value_b": "5",
|
|
"value_c": "6",
|
|
"value_d": "7",
|
|
"value_e": "8",
|
|
}},
|
|
"legacy_character_userinfo_slot_control": {{
|
|
"group0_tail_refresh_value": "2864434397",
|
|
"mode_bits": "17",
|
|
"tail_value_a": "3735928559",
|
|
"tail_flag_a": "34",
|
|
"tail_bool_b": "51",
|
|
"tail_value_b": "1146447479",
|
|
"tail_pair_a": "305419896",
|
|
"tail_pair_b": "2596069104",
|
|
"tail_index6_value": "66",
|
|
}},
|
|
"legacy_character_userinfo_slot_group_entries": {
|
|
{"group_id": "0", "category": "0", "key_or_item_id": "1", "value": "2"},
|
|
{"group_id": "1", "category": "7", "key_or_item_id": "3", "value": "4"},
|
|
{"group_id": "2", "category": "2", "key_or_item_id": "5", "value": "6"},
|
|
},
|
|
}}
|
|
service := Service{characterStats: testCharacterStatTable(t)}
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x0023)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x23 skipped")
|
|
}
|
|
|
|
var want packetWriter
|
|
for _, value := range []uint32{101, 102, 103, 104} {
|
|
want.writeUint32(value)
|
|
}
|
|
want.writeByte(5)
|
|
for _, value := range []uint32{106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 86, 122, 123, 124, 125, 126, 127, 0, 129, 130} {
|
|
want.writeUint32(value)
|
|
}
|
|
want.writeByte(1)
|
|
want.writeUint32(1)
|
|
want.writeUint32(211)
|
|
want.writeByte(1)
|
|
want.writeUint32(1)
|
|
want.writeUint32(212)
|
|
for _, value := range []uint32{131, 132, 133, 134, 135, 136, 137, 138, 1, 2, 3, 4} {
|
|
want.writeUint32(value)
|
|
}
|
|
want.writeUint32(1)
|
|
want.writeUint32(9)
|
|
want.writeUint32(10)
|
|
want.writeByte(1)
|
|
want.writeUint16(4660)
|
|
want.writeUint32(16909060)
|
|
want.writeUint32(5)
|
|
want.writeUint16(6)
|
|
want.writeByte(7)
|
|
want.writeUint16(8)
|
|
want.writeByte(1)
|
|
want.writeUint32(1)
|
|
want.writeUint32(2)
|
|
for i := 1; i < 8; i++ {
|
|
want.writeByte(0)
|
|
}
|
|
want.writeUint32(2864434397)
|
|
for i := 0; i < 7; i++ {
|
|
want.writeByte(0)
|
|
}
|
|
want.writeByte(1)
|
|
want.writeUint32(3)
|
|
want.writeUint32(4)
|
|
for i := 0; i < 2; i++ {
|
|
want.writeByte(0)
|
|
}
|
|
want.writeByte(1)
|
|
want.writeUint32(5)
|
|
want.writeUint32(6)
|
|
for i := 3; i < 8; i++ {
|
|
want.writeByte(0)
|
|
}
|
|
want.writeUint32(17)
|
|
want.writeUint32(3735928559)
|
|
want.writeByte(34)
|
|
want.writeByte(51)
|
|
want.writeUint32(1146447479)
|
|
want.writeUint32(305419896)
|
|
want.writeUint32(2596069104)
|
|
want.writeUint32(66)
|
|
|
|
if !bytes.Equal(body, want.bytes()) {
|
|
t.Fatalf("0x23 body = %x, want %x", body, want.bytes())
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfoB6UsesTwelveValues(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfob6_rows": {{
|
|
"sort_order": "0",
|
|
"text_a": "A",
|
|
"flag_a": "1",
|
|
"flag_b": "2",
|
|
"flag_c": "3",
|
|
"text_b": "B",
|
|
"value": "4",
|
|
}},
|
|
"legacy_character_userinfob6_values": {
|
|
{"row_sort_order": "0", "value_index": "0", "value": "10"},
|
|
{"row_sort_order": "0", "value_index": "11", "value": "21"},
|
|
{"row_sort_order": "0", "value_index": "12", "value": "999"},
|
|
},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x00b6)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0xb6 skipped")
|
|
}
|
|
|
|
var want packetWriter
|
|
want.writeByte(1)
|
|
writeWideNullTerminatedString(&want, "A")
|
|
want.writeByte(1)
|
|
want.writeByte(2)
|
|
want.writeByte(3)
|
|
writeWideNullTerminatedString(&want, "B")
|
|
want.writeUint32(4)
|
|
for idx := 0; idx < 12; idx++ {
|
|
switch idx {
|
|
case 0:
|
|
want.writeUint32(10)
|
|
case 11:
|
|
want.writeUint32(21)
|
|
default:
|
|
want.writeUint32(0)
|
|
}
|
|
}
|
|
if !bytes.Equal(body, want.bytes()) {
|
|
t.Fatalf("0xb6 body = %x, want %x", body, want.bytes())
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfo22EUsesCSharpModeBranches(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo22e_state": {{
|
|
"object_key": "4660",
|
|
"mode": "1",
|
|
"value_a": "16909060",
|
|
"byte_a": "5",
|
|
"byte_b": "6",
|
|
}},
|
|
"legacy_character_userinfo22e_pairs": {
|
|
{"sort_order": "0", "value_a": "7", "value_b": "8"},
|
|
{"sort_order": "1", "value_a": "9", "value_b": "10"},
|
|
},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x022e)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x22e skipped")
|
|
}
|
|
|
|
var want packetWriter
|
|
want.writeUint16(4660)
|
|
want.writeByte(1)
|
|
want.writeUint32(16909060)
|
|
want.writeByte(5)
|
|
want.writeByte(6)
|
|
want.writeByte(2)
|
|
want.writeByte(7)
|
|
want.writeByte(8)
|
|
want.writeByte(9)
|
|
want.writeByte(10)
|
|
if !bytes.Equal(body, want.bytes()) {
|
|
t.Fatalf("0x22e body = %x, want %x", body, want.bytes())
|
|
}
|
|
|
|
repo.rows["legacy_character_userinfo22e_state"][0]["mode"] = "99"
|
|
body, ok = service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x022e)
|
|
if ok || body != nil {
|
|
t.Fatalf("invalid 0x22e mode emitted body %x/%v", body, ok)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfoNewDisplayPacketsSkipWithoutState(t *testing.T) {
|
|
var service Service
|
|
for _, msgID := range []uint16{
|
|
0x022d, 0x022e, 0x0237, 0x0238, 0x0253, 0x0254, 0x0255, 0x025b,
|
|
0x026e, 0x0274, 0x0275, 0x0276, 0x0287, 0x028a, 0x028b, 0x029f,
|
|
} {
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, fakeLegacyUserInfoRepo{}, 1, msgID)
|
|
if ok || body != nil {
|
|
t.Fatalf("0x%04x without state emitted body %x/%v", msgID, body, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfo287And29FRequireStateRows(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo287_rows": {{
|
|
"sort_order": "3",
|
|
"name": "A",
|
|
"byte_a": "1",
|
|
"byte_b": "2",
|
|
"packed_flag": "3",
|
|
"value_a": "4",
|
|
"value_b": "5",
|
|
"text": "B",
|
|
"value_c": "6",
|
|
"value_d": "7",
|
|
"word": "8",
|
|
"byte_c": "9",
|
|
}},
|
|
"legacy_character_userinfo287_extras": {{
|
|
"row_sort_order": "3",
|
|
"sort_order": "0",
|
|
"extra_index": "10",
|
|
"value": "11",
|
|
}},
|
|
"legacy_character_userinfo29f_rows": {{
|
|
"sort_order": "0",
|
|
"word": "12",
|
|
"category": "13",
|
|
"value": "14",
|
|
}},
|
|
}}
|
|
var service Service
|
|
if body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x0287); ok || body != nil {
|
|
t.Fatalf("0x287 without state emitted body %x/%v", body, ok)
|
|
}
|
|
if body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x029f); ok || body != nil {
|
|
t.Fatalf("0x29f without state emitted body %x/%v", body, ok)
|
|
}
|
|
|
|
repo.rows["legacy_character_userinfo287_state"] = []dnfrepo.LegacyUserInfoRow{{"note": "enabled"}}
|
|
repo.rows["legacy_character_userinfo29f_state"] = []dnfrepo.LegacyUserInfoRow{{"note": "enabled"}}
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x0287)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x287 skipped")
|
|
}
|
|
var want287 packetWriter
|
|
want287.writeByte(1)
|
|
writeWideNullTerminatedStringMax(&want287, "A", 31)
|
|
want287.writeByte(1)
|
|
want287.writeByte(2)
|
|
want287.writeByte(3)
|
|
want287.writeUint32(4)
|
|
want287.writeUint32(5)
|
|
writeWideNullTerminatedStringMax(&want287, "B", 30)
|
|
want287.writeUint32(6)
|
|
want287.writeUint32(7)
|
|
want287.writeUint16(8)
|
|
want287.writeByte(9)
|
|
want287.writeByte(1)
|
|
want287.writeByte(10)
|
|
want287.writeUint32(11)
|
|
if !bytes.Equal(body, want287.bytes()) {
|
|
t.Fatalf("0x287 body = %x, want %x", body, want287.bytes())
|
|
}
|
|
|
|
body, ok = service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x029f)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x29f skipped")
|
|
}
|
|
var want29F packetWriter
|
|
want29F.writeByte(1)
|
|
want29F.writeUint16(12)
|
|
want29F.writeByte(13)
|
|
want29F.writeUint32(14)
|
|
if !bytes.Equal(body, want29F.bytes()) {
|
|
t.Fatalf("0x29f body = %x, want %x", body, want29F.bytes())
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfoLatestRegistryAdditions(t *testing.T) {
|
|
raw124 := bytes.Repeat([]byte{0x12}, 124)
|
|
raw64 := bytes.Repeat([]byte{0x34}, 64)
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo2a9_state": {{
|
|
"header_word": "4660",
|
|
}},
|
|
"legacy_character_userinfo2a9_rows": {{
|
|
"sort_order": "0",
|
|
"byte_a": "1",
|
|
"word_a": "515",
|
|
"value": "67438087",
|
|
"word_b": "2057",
|
|
}},
|
|
"legacy_character_userinfo2a9_values": {
|
|
{"sort_order": "0", "value": "170"},
|
|
{"sort_order": "1", "value": "187"},
|
|
},
|
|
"legacy_character_userinfo34c_text_state": {{
|
|
"category": "4",
|
|
"text": "A",
|
|
}},
|
|
"legacy_character_userinfo34c_control": {{
|
|
"word": "65535",
|
|
"value_a": "1",
|
|
"value_b": "2",
|
|
}},
|
|
"legacy_character_userinfo37b_state": {{
|
|
"value": "287454020",
|
|
"raw124": string(raw124),
|
|
"raw64": string(raw64),
|
|
}},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x02a9)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x2a9 skipped")
|
|
}
|
|
var want2A9 packetWriter
|
|
want2A9.writeUint16(4660)
|
|
want2A9.writeByte(1)
|
|
want2A9.writeByte(1)
|
|
want2A9.writeUint16(515)
|
|
want2A9.writeUint32(67438087)
|
|
want2A9.writeUint16(2057)
|
|
want2A9.writeByte(2)
|
|
want2A9.writeByte(170)
|
|
want2A9.writeByte(187)
|
|
if !bytes.Equal(body, want2A9.bytes()) {
|
|
t.Fatalf("0x2a9 body = %x, want %x", body, want2A9.bytes())
|
|
}
|
|
|
|
body, ok = service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x034c)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x34c skipped")
|
|
}
|
|
var want34C packetWriter
|
|
want34C.writeByte(4)
|
|
writeWideNullTerminatedStringMax(&want34C, "A", 64)
|
|
if !bytes.Equal(body, want34C.bytes()) {
|
|
t.Fatalf("0x34c body = %x, want current text-state body %x", body, want34C.bytes())
|
|
}
|
|
|
|
body, ok = service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x037b)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x37b skipped")
|
|
}
|
|
var want37B packetWriter
|
|
want37B.writeUint32(287454020)
|
|
want37B.writeBytes(raw124)
|
|
want37B.writeBytes(raw64)
|
|
if !bytes.Equal(body, want37B.bytes()) {
|
|
t.Fatalf("0x37b body = %x, want %x", body, want37B.bytes())
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfoRawFallbacks(t *testing.T) {
|
|
fixed := []byte{1, 2, 3, 4}
|
|
variable := []byte{5, 6, 7, 8, 9}
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo_fixed_raws": {
|
|
{"noti_type": "967", "payload": string(fixed)},
|
|
{"noti_type": "1034", "payload": string(variable)},
|
|
},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x03c7)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x3c7 skipped")
|
|
}
|
|
if !bytes.Equal(body, fixed) {
|
|
t.Fatalf("0x3c7 body = %x, want %x", body, fixed)
|
|
}
|
|
|
|
body, ok = service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x040a)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x40a skipped")
|
|
}
|
|
if !bytes.Equal(body, variable) {
|
|
t.Fatalf("0x40a body = %x, want %x", body, variable)
|
|
}
|
|
}
|
|
|
|
func TestCSharpLegacyUserInfoDoesNotPushSlotUnlockOrContextRefresh(t *testing.T) {
|
|
forbidden := map[uint16]string{
|
|
0x015d: "slot unlock",
|
|
0x0161: "context sensitive",
|
|
0x01bf: "context sensitive",
|
|
0x01d4: "latest extracted passive state",
|
|
0x01d5: "latest extracted passive state",
|
|
0x01d6: "latest extracted passive state",
|
|
0x01d7: "latest extracted passive state",
|
|
0x01d8: "latest extracted passive state",
|
|
0x01d9: "latest extracted passive state",
|
|
0x0327: "context sensitive",
|
|
0x0329: "context sensitive",
|
|
0x0343: "latest extracted passive state",
|
|
0x0344: "latest extracted passive state",
|
|
0x0373: "latest extracted passive state",
|
|
0x0374: "context refresh",
|
|
0x0375: "latest extracted passive state",
|
|
0x0376: "latest extracted passive state",
|
|
0x0377: "latest extracted passive state",
|
|
0x0378: "latest extracted passive state",
|
|
0x0379: "context refresh",
|
|
0x037a: "context refresh",
|
|
}
|
|
for _, packet := range csharpLegacyUserInfoInitPackets() {
|
|
if reason, ok := forbidden[packet.msgID]; ok {
|
|
t.Fatalf("legacy userinfo init should not actively push 0x%04x (%s)", packet.msgID, reason)
|
|
}
|
|
}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, fakeLegacyUserInfoRepo{}, 1, 0x015d)
|
|
if ok || body != nil {
|
|
t.Fatalf("0x015d body = %x/%v, want skipped unless client triggers unlock flow", body, ok)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpSelectedUserInfoSubtype0UsesCharacterAndLegacyTail(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_subtype0_fields": {{
|
|
"name_tag_item_id": "287454020",
|
|
"creature_field1": "1",
|
|
"creature_field2": "2",
|
|
"creature_field3": "3",
|
|
"creature_field4": "4",
|
|
"creature_buffer": "ABCDEFGH",
|
|
"stamina": "5",
|
|
"fatigue_penalty": "1432778632",
|
|
"is_event_character": "1",
|
|
"pc_room_id": "65537",
|
|
"expert_job_type": "6",
|
|
"expert_job_exp": "16909060",
|
|
"extra46": "7",
|
|
"extra47": "84281096",
|
|
"extra51": "4386",
|
|
"user_state_bits": "3",
|
|
"return_user_flag": "1",
|
|
"channel_id": "2",
|
|
"link_slot_enabled": "1",
|
|
"aura_flag": "0",
|
|
"pvp_rank_point": "287454020",
|
|
"trailing_byte": "9",
|
|
}},
|
|
"legacy_character_subtype1_fields": {{
|
|
"progress1": "305419896",
|
|
"progress2": "2596069104",
|
|
"skill_tree_index": "12",
|
|
}},
|
|
}}
|
|
character := dnfrepo.CharacterRecord{
|
|
CharacterID: "77",
|
|
Name: "hero",
|
|
Job: "14",
|
|
Level: 56,
|
|
Stats: map[string]int64{
|
|
"grow_type": 2,
|
|
"pvp_grade": 3,
|
|
"pvp_rating_grade": 4,
|
|
"user_state": 5,
|
|
"aura_flag": 1,
|
|
"mood_value": 7,
|
|
},
|
|
}
|
|
var service Service
|
|
body := service.buildCSharpSelectedUserInfoBody(context.Background(), nil, repo, 0, character, true, 77, character.Name)
|
|
|
|
if len(body) != 126 {
|
|
t.Fatalf("subtype0 body len = %d, want 126", len(body))
|
|
}
|
|
if body[0] != 0 || binary.LittleEndian.Uint16(body[1:3]) != 1 || binary.LittleEndian.Uint16(body[3:5]) != 77 {
|
|
t.Fatalf("subtype0 header = %x", body[:5])
|
|
}
|
|
if got := string(body[9:13]); got != "hero" {
|
|
t.Fatalf("subtype0 name bytes = %q", got)
|
|
}
|
|
if got := []byte{body[13], body[14], body[15], body[16], body[17], body[18], body[19]}; !bytes.Equal(got, []byte{14, 2, 56, 3, 4, 5, 0}) {
|
|
t.Fatalf("subtype0 character bytes = %v", got)
|
|
}
|
|
tail := body[20:]
|
|
if binary.LittleEndian.Uint32(tail[0:4]) != 287454020 ||
|
|
tail[46] != 7 ||
|
|
binary.LittleEndian.Uint32(tail[47:51]) != 84281096 ||
|
|
binary.LittleEndian.Uint16(tail[51:53]) != 4386 ||
|
|
binary.LittleEndian.Uint32(tail[57:61]) != 305419896 ||
|
|
binary.LittleEndian.Uint32(tail[61:65]) != 2596069104 ||
|
|
tail[79] != 12 ||
|
|
tail[92] != 1 ||
|
|
binary.LittleEndian.Uint16(tail[84:86]) != 7 ||
|
|
tail[105] != 9 {
|
|
t.Fatalf("subtype0 tail key fields mismatch: %x", tail)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpSelectedUserInfoSubtype0ReloadsDurableAuraFlag(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "77",
|
|
AccountID: "account-1",
|
|
Name: "hero",
|
|
Job: "14",
|
|
Level: 56,
|
|
Stats: map[string]int64{"aura_flag": 1},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := Service{
|
|
repositoryProvider: func() (dnfrepo.Group, bool) {
|
|
return repositories, true
|
|
},
|
|
}
|
|
rosterCharacter := dnfrepo.CharacterRecord{
|
|
CharacterID: "77",
|
|
Name: "hero",
|
|
Job: "14",
|
|
Level: 56,
|
|
}
|
|
body := service.buildCSharpSelectedUserInfoBody(
|
|
ctx,
|
|
nil,
|
|
fakeLegacyUserInfoRepo{},
|
|
0,
|
|
rosterCharacter,
|
|
true,
|
|
77,
|
|
rosterCharacter.Name,
|
|
)
|
|
if len(body) < 14 || body[len(body)-14] != 1 {
|
|
t.Fatalf("repository-backed aura flag was not restored in subtype0: len=%d tail=%x", len(body), body)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpSelectedUserInfoSubtype0ProjectsDurablePrivateStoreContract(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
character := dnfrepo.CharacterRecord{
|
|
CharacterID: "77",
|
|
AccountID: "account-1",
|
|
Name: "hero",
|
|
Job: "14",
|
|
Level: 56,
|
|
Stats: map[string]int64{},
|
|
}
|
|
if err := repositories.Character.Save(ctx, character); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := Service{repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
|
|
session := &gameSession{accountID: character.AccountID, selectedCharacterID: 77}
|
|
build := func() []byte {
|
|
return service.buildCSharpSelectedUserInfoBody(
|
|
ctx, session, fakeLegacyUserInfoRepo{}, 0, character, true, 77, character.Name,
|
|
)
|
|
}
|
|
// "hero" and an empty appearance list place the subtype0 tail at byte 20;
|
|
// is_private_store is byte 26 inside that tail.
|
|
const privateStoreOffset = 20 + 26
|
|
if body := build(); len(body) <= privateStoreOffset || body[privateStoreOffset] != 0 {
|
|
t.Fatalf("private-store flag without contract body=%x", body)
|
|
}
|
|
|
|
for _, premiumType := range []int64{premium.TypePrivateStore, premium.TypeAuction} {
|
|
if err := repositories.Account.Save(ctx, dnfrepo.AccountRecord{
|
|
AccountID: character.AccountID,
|
|
Metadata: map[string]string{
|
|
premium.MetadataKey(premiumType): strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10),
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body := build(); len(body) <= privateStoreOffset || body[privateStoreOffset] != 1 {
|
|
t.Fatalf("private-store flag for premium type %d body=%x", premiumType, body)
|
|
}
|
|
}
|
|
|
|
if err := repositories.Account.Save(ctx, dnfrepo.AccountRecord{
|
|
AccountID: character.AccountID,
|
|
Metadata: map[string]string{
|
|
premium.MetadataKey(premium.TypePrivateStore): strconv.FormatInt(time.Now().Add(-time.Hour).Unix(), 10),
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body := build(); len(body) <= privateStoreOffset || body[privateStoreOffset] != 0 {
|
|
t.Fatalf("expired private-store contract remained projected body=%x", body)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpSelectedUserInfoSubtype0UsesSessionChannelContext(t *testing.T) {
|
|
character := dnfrepo.CharacterRecord{
|
|
CharacterID: "77",
|
|
Name: "hero",
|
|
Job: "14",
|
|
Level: 1,
|
|
Stats: map[string]int64{"channel_id": 2},
|
|
}
|
|
service := Service{}
|
|
session := &gameSession{channel: channelcatalog.Channel{ID: 19, Type: 3, Name: "ch.19", Port: 10019}}
|
|
|
|
body := service.buildCSharpSelectedUserInfoBody(context.Background(), session, fakeLegacyUserInfoRepo{}, 0, character, true, 77, character.Name)
|
|
tail := body[20:]
|
|
if got := binary.LittleEndian.Uint16(tail[74:76]); got != 19 {
|
|
t.Fatalf("subtype0 channel display mode = %d, want session channel 19", got)
|
|
}
|
|
if got := tail[76]; got != 3 {
|
|
t.Fatalf("subtype0 channel type = %d, want session type 3", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint16(tail[77:79]); got != 19 {
|
|
t.Fatalf("subtype0 channel id = %d, want session channel 19", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpSelectedUserInfoSubtype1UsesCSharpFieldOrder(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_subtype1_fields": {{
|
|
"stat_hp_max": "1000",
|
|
"stat_mp_max": "2000",
|
|
"stat_physical_attack": "30",
|
|
"active_status_resistance_16": "160",
|
|
"stat_inventory_limit": "24",
|
|
"stat_level": "56",
|
|
"name_tag_item_id": "287454020",
|
|
"name_tag_expire_time": "1432778632",
|
|
"skill_tree_index": "9",
|
|
"equipped_creature_level": "11",
|
|
"equip_list_trailing": "305419896",
|
|
"manage_level": "12",
|
|
"flag_byte": "13",
|
|
"guild_power_war": "16909060",
|
|
"server_timestamp": "84281096",
|
|
"quest_shop_count": "4660",
|
|
"progress1": "305419896",
|
|
"progress2": "2596069104",
|
|
}},
|
|
}}
|
|
character := dnfrepo.CharacterRecord{
|
|
CharacterID: "77",
|
|
Stats: map[string]int64{
|
|
"exp": 12345,
|
|
"ex_equip_slot_stat": 6,
|
|
},
|
|
}
|
|
var service Service
|
|
body := service.buildCSharpSelectedUserInfoBody(context.Background(), nil, repo, 1, character, true, 77, "hero")
|
|
|
|
if body[0] != 1 || binary.LittleEndian.Uint16(body[1:3]) != 1 || binary.LittleEndian.Uint16(body[3:5]) != 77 {
|
|
t.Fatalf("subtype1 header = %x", body[:5])
|
|
}
|
|
if got := binary.LittleEndian.Uint32(body[5:9]); got != 12345 {
|
|
t.Fatalf("subtype1 exp = %d", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(body[9:13]); got != 83 {
|
|
t.Fatalf("subtype1 stat block marker = %d", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint16(body[69:71]); got != 160 {
|
|
t.Fatalf("subtype1 active_status_resistance_16 = %d", got)
|
|
}
|
|
if got := body[96]; got != 6 {
|
|
t.Fatalf("subtype1 ex_equip_slot_stat = %d", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(body[98:102]); got != 305419896 {
|
|
t.Fatalf("subtype1 equip_list_trailing = %d", got)
|
|
}
|
|
if got := body[110]; got != 9 {
|
|
t.Fatalf("subtype1 skill_tree_index = %d", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(body[len(body)-8 : len(body)-4]); got != 305419896 {
|
|
t.Fatalf("subtype1 progress1 = %d", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(body[len(body)-4:]); got != 2596069104 {
|
|
t.Fatalf("subtype1 progress2 = %d", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpSelectedUserInfoUsesCreatedCharacterSeedWithoutLegacyRows(t *testing.T) {
|
|
req := createCharacterRequest{
|
|
job: 15,
|
|
nameRaw: []byte("hero"),
|
|
options: []byte{0xaa, 0xbb, 0xcc},
|
|
}
|
|
character := dnfrepo.CharacterRecord{
|
|
CharacterID: "77",
|
|
Name: "hero",
|
|
Job: "15",
|
|
Level: 1,
|
|
Stats: defaultCreatedCharacterStatsFromRequest(req),
|
|
}
|
|
service := Service{characterStats: testCharacterStatTable(t)}
|
|
|
|
subtype0 := service.buildCSharpSelectedUserInfoBody(context.Background(), nil, fakeLegacyUserInfoRepo{}, 0, character, true, 77, character.Name)
|
|
if len(subtype0) != 126 {
|
|
t.Fatalf("subtype0 body len = %d, want 126", len(subtype0))
|
|
}
|
|
tail := subtype0[20:]
|
|
if got := binary.LittleEndian.Uint32(tail[22:26]); got != 0x00010001 {
|
|
t.Fatalf("subtype0 pc_room_id = %#x, want 0x00010001", got)
|
|
}
|
|
if tail[65] != 3 || tail[73] != 1 || binary.LittleEndian.Uint16(tail[77:79]) != 2 {
|
|
t.Fatalf("subtype0 default tail mismatch: %x", tail)
|
|
}
|
|
|
|
subtype1 := service.buildCSharpSelectedUserInfoBody(context.Background(), nil, fakeLegacyUserInfoRepo{}, 1, character, true, 77, character.Name)
|
|
if got := binary.LittleEndian.Uint32(subtype1[9:13]); got != 83 {
|
|
t.Fatalf("subtype1 marker = %d, want 83", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(subtype1[13:17]); got != 11000 {
|
|
t.Fatalf("subtype1 hp max = %d, want 11000", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(subtype1[17:21]); got != 11800 {
|
|
t.Fatalf("subtype1 mp max = %d, want 11800", got)
|
|
}
|
|
if got := bodyInt16(subtype1[21:23]); got != 7 {
|
|
t.Fatalf("subtype1 physical attack = %d, want 7", got)
|
|
}
|
|
if got := bodyInt16(subtype1[25:27]); got != 6 {
|
|
t.Fatalf("subtype1 magical attack = %d, want 6", got)
|
|
}
|
|
if got := binary.LittleEndian.Uint32(subtype1[71:75]); got != 480000 {
|
|
t.Fatalf("subtype1 inventory limit = %d, want 480000", got)
|
|
}
|
|
if got := subtype1[95]; got != csharpSubtype1ProtocolStatLevel {
|
|
t.Fatalf("subtype1 protocol stat level = %d, want %d", got, csharpSubtype1ProtocolStatLevel)
|
|
}
|
|
if character.Stats["create_option_len"] != 3 ||
|
|
character.Stats["create_option_byte_00"] != 0xaa ||
|
|
character.Stats["create_option_byte_01"] != 0xbb ||
|
|
character.Stats["create_option_byte_02"] != 0xcc {
|
|
t.Fatalf("created option bytes not persisted in stats: %+v", character.Stats)
|
|
}
|
|
}
|
|
|
|
func bodyInt16(data []byte) int {
|
|
return int(int16(binary.LittleEndian.Uint16(data)))
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfo17CUsesCSharpLayout(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo17c_control": {{
|
|
"header_flag": "7",
|
|
}},
|
|
"legacy_character_userinfo17c_rows": {{
|
|
"selector": "2",
|
|
"item_or_key": "1000",
|
|
"value": "55",
|
|
}},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x017c)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x17c skipped")
|
|
}
|
|
want := []byte{7, 1, 0, 2, 0, 0xe8, 0x03, 0, 0, 55, 0, 0, 0}
|
|
if !bytes.Equal(body, want) {
|
|
t.Fatalf("0x17c body = %x, want %x", body, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfo327PreservesBlobPayload(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo327_blobs": {{
|
|
"blob_key": "9",
|
|
"payload": string([]byte{0, 1, 2, 0xff}),
|
|
}},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x0327)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x327 skipped")
|
|
}
|
|
want := []byte{1, 9, 4, 0, 0, 0, 0, 1, 2, 0xff}
|
|
if !bytes.Equal(body, want) {
|
|
t.Fatalf("0x327 body = %x, want %x", body, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfoRawFixedUsesNotiType(t *testing.T) {
|
|
payload := []byte{
|
|
0, 1, 2, 3, 4, 5, 6, 7,
|
|
8, 9, 10, 11, 12, 13, 14, 15,
|
|
16, 17, 18, 19, 20, 21, 22, 23,
|
|
}
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo_fixed_raws": {
|
|
{"noti_type": "835", "payload": string([]byte{0xee})},
|
|
{"noti_type": "836", "payload": string(payload)},
|
|
},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x0344)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x344 skipped")
|
|
}
|
|
if !bytes.Equal(body, payload) {
|
|
t.Fatalf("0x344 body = %x, want %x", body, payload)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfoRawByteCountList(t *testing.T) {
|
|
rowA := []byte{1, 2, 3, 4, 5, 6, 7, 8}
|
|
rowB := []byte{9, 10, 11, 12, 13, 14, 15, 16}
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo_byte_count_raw_rows": {
|
|
{"noti_type": "468", "sort_order": "1", "payload": string(rowA)},
|
|
{"noti_type": "468", "sort_order": "2", "payload": string(rowB)},
|
|
{"noti_type": "471", "sort_order": "1", "payload": string([]byte{0xee})},
|
|
},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x01d4)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x1d4 skipped")
|
|
}
|
|
want := append([]byte{2}, append(rowA, rowB...)...)
|
|
if !bytes.Equal(body, want) {
|
|
t.Fatalf("0x1d4 body = %x, want %x", body, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfo374UsesRawGroups(t *testing.T) {
|
|
header := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}
|
|
raw12 := []byte{20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
|
|
raw13 := []byte{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44}
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo374_state": {{
|
|
"header": string(header),
|
|
}},
|
|
"legacy_character_userinfo374_rows": {
|
|
{"group_kind": "raw12", "sort_order": "1", "payload": string(raw12)},
|
|
{"group_kind": "raw13", "sort_order": "1", "payload": string(raw13)},
|
|
},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x0374)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x374 skipped")
|
|
}
|
|
want := append([]byte{}, header...)
|
|
want = append(want, 1, 0, 0, 0)
|
|
want = append(want, raw12...)
|
|
want = append(want, 1, 0, 0, 0)
|
|
want = append(want, raw13...)
|
|
if !bytes.Equal(body, want) {
|
|
t.Fatalf("0x374 body = %x, want %x", body, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCSharpLegacyUserInfo37ARequiresValidHeaders(t *testing.T) {
|
|
repo := fakeLegacyUserInfoRepo{rows: map[string][]dnfrepo.LegacyUserInfoRow{
|
|
"legacy_character_userinfo37a_state": {{
|
|
"header1": string([]byte{1}),
|
|
"header33": string(bytes.Repeat([]byte{2}, 33)),
|
|
}},
|
|
}}
|
|
var service Service
|
|
body, ok := service.buildCSharpLegacyUserInfoBody(context.Background(), nil, repo, 1, 0x037a)
|
|
if !ok {
|
|
t.Fatal("legacy userinfo 0x37a skipped")
|
|
}
|
|
want := append([]byte{1}, bytes.Repeat([]byte{2}, 33)...)
|
|
for i := 0; i < 7; i++ {
|
|
want = append(want, 0, 0, 0, 0)
|
|
}
|
|
if !bytes.Equal(body, want) {
|
|
t.Fatalf("0x37a body = %x, want %x", body, want)
|
|
}
|
|
}
|