Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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)
}
}