fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
@@ -57,15 +57,20 @@ int main() {
|
||||
add.angle = 90.0f;
|
||||
add.moving_speed = 120;
|
||||
add.attack_speed = 135;
|
||||
add.state_flag = 0x5a;
|
||||
add.affect_flag[0] = 0x11223344;
|
||||
add.affect_flag[1] = 0x55667788;
|
||||
b = pkt(add);
|
||||
es.apply(GC_CHARACTER_ADD, b.data(), (uint16_t)b.size());
|
||||
CHECK(es.get(2) != nullptr, "entity 2 spawned");
|
||||
CHECK(es.get(2)->x == 500 && es.get(2)->y == 500, "entity 2 pos");
|
||||
CHECK(es.get(2)->moving_speed == 120 && es.get(2)->attack_speed == 135,
|
||||
"entity 2 movement + attack speed");
|
||||
CHECK(es.get(2)->state_flags == 0x5a &&
|
||||
es.get(2)->affect_flags == 0x5566778811223344ULL,
|
||||
"entity 2 state and affect flags from character add");
|
||||
CHECK(es.size() == 2, "2 entities");
|
||||
|
||||
// change events: 1 spawn (main) + 1 spawn (entity 2) (+ MainSet dedupe tolerated)
|
||||
// change events: 1 spawn (main) + 1 spawn (entity 2) + one main registration.
|
||||
{
|
||||
auto ch = es.drain_changes();
|
||||
int spawns = 0, mainsets = 0;
|
||||
@@ -78,9 +83,90 @@ int main() {
|
||||
}
|
||||
}
|
||||
CHECK(spawns == 2, "2 spawn changes");
|
||||
CHECK(mainsets == 1, "main character registration is emitted once");
|
||||
CHECK(es.drain_changes().empty(), "changes drained");
|
||||
}
|
||||
|
||||
// --- GC_CHARACTER_ADD2 carries the complete actor row in one packet ---
|
||||
{
|
||||
GCCharacterAdd2 add2{};
|
||||
add2.header = GC_CHARACTER_ADD2;
|
||||
add2.length = sizeof(add2);
|
||||
add2.vid = 3;
|
||||
std::strcpy(add2.name, "Add2Actor");
|
||||
add2.angle = 27.0f;
|
||||
add2.x = 700;
|
||||
add2.y = 800;
|
||||
add2.z = 9;
|
||||
add2.type = 2;
|
||||
add2.race = 101;
|
||||
add2.parts[0] = 401;
|
||||
add2.parts[1] = 402;
|
||||
add2.moving_speed = 111;
|
||||
add2.attack_speed = 122;
|
||||
add2.state_flag = 0x6b;
|
||||
add2.affect_flag[0] = 0x01020304;
|
||||
add2.affect_flag[1] = 0xa0b0c0d0;
|
||||
add2.empire = 2;
|
||||
add2.guild = 17;
|
||||
add2.alignment = -321;
|
||||
add2.pk_mode = 4;
|
||||
add2.mount_vnum = 20114;
|
||||
es.apply(GC_CHARACTER_ADD2, pkt(add2).data(), (uint16_t)sizeof(add2));
|
||||
const Entity *e = es.get(3);
|
||||
CHECK(e && e->name == "Add2Actor" && e->race == 101 && e->ch_type == 2 &&
|
||||
e->x == 700.f && e->y == 800.f && e->angle == 27.f,
|
||||
"add2: identity and transform copied");
|
||||
CHECK(e && e->parts[0] == 401 && e->parts[1] == 402 && e->moving_speed == 111 &&
|
||||
e->attack_speed == 122 && e->state_flags == 0x6b &&
|
||||
e->affect_flags == 0xa0b0c0d001020304ULL,
|
||||
"add2: equipment, speeds, state and affect copied");
|
||||
CHECK(e && e->empire == 2 && e->guild == 17 && e->alignment == -321 &&
|
||||
e->pk_mode == 4 && e->mount_vnum == 20114,
|
||||
"add2: empire, guild, alignment, pk mode and mount copied");
|
||||
int add2_spawns = 0;
|
||||
for (const auto &c : es.drain_changes()) {
|
||||
if (c.kind == EntityStore::ChangeKind::Spawn && c.vid == 3) {
|
||||
++add2_spawns;
|
||||
}
|
||||
}
|
||||
CHECK(add2_spawns == 1, "add2: one complete spawn change");
|
||||
es.mut_despawn(3);
|
||||
es.drain_changes();
|
||||
}
|
||||
|
||||
// Invisible races are filtered by the 40250 IsInvisibleRace gate.
|
||||
{
|
||||
GCCharacterAdd2 hidden{};
|
||||
hidden.header = GC_CHARACTER_ADD2;
|
||||
hidden.length = sizeof(hidden);
|
||||
hidden.vid = 4;
|
||||
hidden.race = 20025;
|
||||
es.apply(GC_CHARACTER_ADD2, pkt(hidden).data(), (uint16_t)sizeof(hidden));
|
||||
CHECK(es.get(4) == nullptr, "add2: invisible race is not spawned");
|
||||
}
|
||||
|
||||
// Add2 was drained above; no stale change may leak into the next packet.
|
||||
{
|
||||
auto ch = es.drain_changes();
|
||||
CHECK(ch.empty() && es.drain_changes().empty(), "changes drained");
|
||||
}
|
||||
|
||||
// Re-announcing the existing main VID must produce one MainSet edge, not a
|
||||
// duplicate pair. This mirrors NetworkActorManager::SetMainActorVID /
|
||||
// __AppendCharacterManagerActor and prevents duplicate model/UI rebuilds.
|
||||
es.mut_spawn_main(1, 4, "Hero", 1000, 2000, 5);
|
||||
{
|
||||
auto ch = es.drain_changes();
|
||||
int mainsets = 0;
|
||||
for (auto &c : ch) {
|
||||
if (c.kind == EntityStore::ChangeKind::MainSet && c.vid == 1) {
|
||||
++mainsets;
|
||||
}
|
||||
}
|
||||
CHECK(mainsets == 1, "existing main re-registration emits one MainSet");
|
||||
}
|
||||
|
||||
// --- GC_CHAR_ADD_INFO fills name / level / guild / mount for entity 2 ---
|
||||
{
|
||||
GCCharAddInfo ai{};
|
||||
@@ -120,13 +206,19 @@ int main() {
|
||||
up.guild_id = 9;
|
||||
up.moving_speed = 140;
|
||||
up.attack_speed = 155;
|
||||
up.mount_vnum = 0; // dismount
|
||||
up.state_flag = 0x7c;
|
||||
up.affect_flag[0] = 0x0badcafe;
|
||||
up.affect_flag[1] = 0x10203040;
|
||||
up.mount_vnum = 0; // dismount
|
||||
b = pkt(up);
|
||||
es.apply(GC_CHARACTER_UPDATE, b.data(), (uint16_t)b.size());
|
||||
CHECK(es.get(2)->parts[1] == 222 && es.get(2)->guild == 9 &&
|
||||
es.get(2)->moving_speed == 140 && es.get(2)->attack_speed == 155,
|
||||
"update: parts + guild + speeds");
|
||||
CHECK(es.get(2)->mount_vnum == 0, "update: dismounted");
|
||||
CHECK(es.get(2)->mount_vnum == 0, "update: dismounted");
|
||||
CHECK(es.get(2)->state_flags == 0x7c &&
|
||||
es.get(2)->affect_flags == 0x102030400badcafeULL,
|
||||
"update: state and affect flags copied");
|
||||
auto mc = es.drain_mount_changes();
|
||||
CHECK(mc.size() == 1 && mc[0] == 2, "update: mount change emitted");
|
||||
int uinfo = 0;
|
||||
@@ -136,6 +228,38 @@ int main() {
|
||||
}
|
||||
}
|
||||
CHECK(uinfo == 1, "update: parts changed -> Info change (costume/equip re-read)");
|
||||
|
||||
// A state/affect-only update still has one coherent UpdateActor/Info edge;
|
||||
// it must not require an equipment change to reach the scene bridge.
|
||||
GCCharacterUpdate state_only = up;
|
||||
state_only.state_flag = 0x7d;
|
||||
state_only.affect_flag[0] = 0xabcdef01;
|
||||
state_only.affect_flag[1] = 0x23456789;
|
||||
es.apply(GC_CHARACTER_UPDATE, pkt(state_only).data(), (uint16_t)sizeof(state_only));
|
||||
CHECK(es.get(2)->state_flags == 0x7d &&
|
||||
es.get(2)->affect_flags == 0x23456789abcdef01ULL,
|
||||
"update: affect-only state reaches entity row");
|
||||
int state_infos = 0;
|
||||
for (const auto &c : es.drain_changes()) {
|
||||
if (c.kind == EntityStore::ChangeKind::Info && c.vid == 2) {
|
||||
++state_infos;
|
||||
}
|
||||
}
|
||||
CHECK(state_infos == 1, "update: state/affect-only change emits one Info");
|
||||
}
|
||||
|
||||
// Additional-info must complete a known/staged actor; it must not create an
|
||||
// unknown half-entity when the preceding add was lost or reordered.
|
||||
{
|
||||
GCCharAddInfo unknown{};
|
||||
unknown.header = GC_CHAR_ADD_INFO;
|
||||
unknown.length = sizeof(unknown);
|
||||
unknown.vid = 9999;
|
||||
std::strcpy(unknown.name, "orphan");
|
||||
const size_t before = es.size();
|
||||
es.apply(GC_CHAR_ADD_INFO, pkt(unknown).data(), (uint16_t)sizeof(unknown));
|
||||
CHECK(es.size() == before && es.get(9999) == nullptr,
|
||||
"add_info: unknown VID is ignored without creating a partial entity");
|
||||
}
|
||||
|
||||
// --- entity 2 gets a MOVE: (500,500) -> (1500,500) over 1000ms starting now(=0) ---
|
||||
@@ -221,6 +345,15 @@ int main() {
|
||||
es.apply(GC_SYNC_POSITION, sync.data(), (uint16_t)sync.size());
|
||||
CHECK(es.get(2)->x == 2222 && es.get(2)->y == 3333 && !es.get(2)->moving,
|
||||
"movement: sync position snaps known entity");
|
||||
CHECK(es.get(2)->sync_push_serial == 1 && es.get(2)->sync_push_from_x == 1700.0f
|
||||
&& es.get(2)->sync_push_from_y == 500.0f && es.get(2)->sync_push_large
|
||||
&& !es.get(2)->sync_push_skill_active,
|
||||
"movement: sync position records TEMP_Push source and large correction");
|
||||
es.mut_network_state_gate(2, true, true, false, true);
|
||||
es.mut_snap_position(2, 2222.0f, 3333.0f);
|
||||
CHECK(es.get(2)->sync_push_serial == 2 && !es.get(2)->sync_push_large
|
||||
&& es.get(2)->sync_push_skill_active,
|
||||
"movement: zero correction keeps skill-active TEMP_Push suppression state");
|
||||
}
|
||||
|
||||
// --- chat (variable length) ---
|
||||
@@ -299,8 +432,63 @@ int main() {
|
||||
b = pkt(pc);
|
||||
es.apply(GC_PLAYER_POINT_CHANGE, b.data(), (uint16_t)b.size());
|
||||
CHECK(es.get(2)->hp == 80, "entity 2 hp from point_change");
|
||||
auto point_events = es.drain_point_changes();
|
||||
CHECK(point_events.size() == 1 && point_events[0].vid == 2
|
||||
&& point_events[0].type == POINT_HP && point_events[0].value == 80
|
||||
&& point_events[0].amount == -20,
|
||||
"typed point_change event keeps vid/type/value/amount");
|
||||
auto v = es.drain_vitals();
|
||||
CHECK(v.size() == 1 && v[0] == 2, "vitals: entity 2 marked");
|
||||
|
||||
// A positive HP update is the 40250 revive boundary: clear the dead
|
||||
// state so a later death can emit entity_dead again.
|
||||
GCPointChange revive = pc;
|
||||
revive.amount = 20;
|
||||
revive.value = 100;
|
||||
b = pkt(revive);
|
||||
es.apply(GC_PLAYER_POINT_CHANGE, b.data(), (uint16_t)b.size());
|
||||
CHECK(es.get(2)->hp == 100 && !es.get(2)->dead,
|
||||
"positive HP clears entity dead state");
|
||||
CHECK(es.drain_point_changes().size() == 1, "revive point_change event emitted");
|
||||
CHECK(es.drain_vitals().size() == 1, "revive marks vitals for consumers");
|
||||
}
|
||||
|
||||
// 40250 RecvPointChange ordering / branches: the typed event is available
|
||||
// before the aggregate snapshot, positive EXP/GOLD amounts survive, and
|
||||
// zero energy clears POINT_ENERGY_END_TIME.
|
||||
{
|
||||
GCPointChange exp{};
|
||||
exp.header = GC_PLAYER_POINT_CHANGE;
|
||||
exp.length = sizeof(exp);
|
||||
exp.vid = 1;
|
||||
exp.type = POINT_EXP;
|
||||
exp.amount = 321;
|
||||
exp.value = 4321;
|
||||
b = pkt(exp);
|
||||
es.apply(GC_PLAYER_POINT_CHANGE, b.data(), (uint16_t)b.size());
|
||||
GCPointChange gold = exp;
|
||||
gold.type = POINT_GOLD;
|
||||
gold.amount = 777;
|
||||
gold.value = 10776;
|
||||
b = pkt(gold);
|
||||
es.apply(GC_PLAYER_POINT_CHANGE, b.data(), (uint16_t)b.size());
|
||||
GCPointChange energy = exp;
|
||||
energy.type = POINT_ENERGY;
|
||||
energy.amount = 0;
|
||||
energy.value = 0;
|
||||
b = pkt(energy);
|
||||
es.apply(GC_PLAYER_POINT_CHANGE, b.data(), (uint16_t)b.size());
|
||||
const auto points = es.drain_point_changes();
|
||||
CHECK(points.size() == 3, "three typed point changes drained in packet order");
|
||||
CHECK(points.size() == 3 && points[0].type == POINT_EXP && points[0].amount == 321
|
||||
&& points[1].type == POINT_GOLD && points[1].amount == 777
|
||||
&& points[2].type == POINT_ENERGY,
|
||||
"typed point changes preserve EXP/GOLD/ENERGY order");
|
||||
CHECK(es.take_exp_gain() == 321, "positive EXP amount remains immediate gain");
|
||||
CHECK(es.points().energy() == 0 && es.points().energy_end_time() == 0,
|
||||
"zero energy clears energy effect end time");
|
||||
CHECK(es.take_points_dirty(), "point changes mark aggregate points dirty");
|
||||
CHECK(!es.take_points_dirty(), "aggregate points dirty flag consumed");
|
||||
}
|
||||
|
||||
// GC_DAMAGE_INFO: a crit for 137 over entity 2
|
||||
@@ -385,6 +573,7 @@ int main() {
|
||||
s.vnum = 19;
|
||||
s.count = 1;
|
||||
s.flags = 0x2;
|
||||
s.highlight = 1;
|
||||
s.sockets[0] = 28960;
|
||||
s.attrs[0] = {5, 30}; // type 5, +30
|
||||
s.attrs[1] = {7, 12};
|
||||
@@ -395,6 +584,9 @@ int main() {
|
||||
CHECK(es.inv_slot(5).attrs[1].type == 7 && es.inv_slot(5).attrs[1].value == 12, "item: attr kept");
|
||||
auto ic = es.drain_inv();
|
||||
CHECK(ic.size() == 1 && ic[0].window == WINDOW_INVENTORY && ic[0].cell == 5, "item: inv change emitted");
|
||||
auto highlights = es.drain_item_highlights();
|
||||
CHECK(highlights.size() == 1 && highlights[0].window == WINDOW_INVENTORY &&
|
||||
highlights[0].cell == 5, "item: Set2 highlight emitted separately from inventory change");
|
||||
}
|
||||
|
||||
// GC_ITEM_SET to EQUIPMENT wear slot 1 (body armor)
|
||||
@@ -437,14 +629,22 @@ int main() {
|
||||
|
||||
// GC_ITEM_UPDATE: cell 5 count -> 3
|
||||
{
|
||||
es.drain_inv(); // discard the preceding equipment/belt Set notifications
|
||||
GCItemUpdate u{};
|
||||
u.header = GC_ITEM_UPDATE;
|
||||
u.length = sizeof(u);
|
||||
u.cell = {WINDOW_INVENTORY, 5};
|
||||
u.count = 3;
|
||||
u.sockets[1] = 29001;
|
||||
u.attrs[0] = {8, 22};
|
||||
b = pkt(u);
|
||||
es.apply(GC_ITEM_UPDATE, b.data(), (uint16_t)b.size());
|
||||
CHECK(es.inv_slot(5).count == 3 && es.inv_slot(5).vnum == 19, "item: update count, vnum kept");
|
||||
auto update_changes = es.drain_inv();
|
||||
CHECK(update_changes.size() == 2 && update_changes[0].item.count == 3 &&
|
||||
update_changes[0].item.sockets[1] == 0 && update_changes[0].item.attrs[0].value == 30 &&
|
||||
update_changes[1].item.sockets[1] == 29001 && update_changes[1].item.attrs[0].value == 22,
|
||||
"item: update emits count refresh before final socket/attribute refresh");
|
||||
}
|
||||
|
||||
// GC_ITEM_DEL: clear cell 5
|
||||
@@ -456,6 +656,47 @@ int main() {
|
||||
b = pkt(dl);
|
||||
es.apply(GC_ITEM_DEL, b.data(), (uint16_t)b.size());
|
||||
CHECK(es.inv_slot(5).empty(), "item: cell 5 cleared");
|
||||
es.drain_inv(); // discard the delete notification before the next packet
|
||||
|
||||
// ClientVS22 SetItemCount/SetItemMetinSocket/SetItemAttribute update a
|
||||
// valid slot even when its vnum is currently zero; the packet is not
|
||||
// rejected merely because the slot is empty.
|
||||
GCItemUpdate empty_update{};
|
||||
empty_update.header = GC_ITEM_UPDATE;
|
||||
empty_update.length = sizeof(empty_update);
|
||||
empty_update.cell = {WINDOW_INVENTORY, 6};
|
||||
empty_update.count = 4;
|
||||
empty_update.sockets[1] = 29000;
|
||||
empty_update.attrs[0] = {9, 17};
|
||||
b = pkt(empty_update);
|
||||
es.apply(GC_ITEM_UPDATE, b.data(), (uint16_t)b.size());
|
||||
CHECK(es.inv_slot(6).empty() && es.inv_slot(6).count == 4 &&
|
||||
es.inv_slot(6).sockets[1] == 29000 && es.inv_slot(6).attrs[0].value == 17,
|
||||
"item: update valid empty cell keeps packet state");
|
||||
auto empty_update_changes = es.drain_inv();
|
||||
CHECK(empty_update_changes.size() == 2 && empty_update_changes[0].item.count == 4 &&
|
||||
empty_update_changes[0].item.sockets[1] == 0 &&
|
||||
empty_update_changes[1].item.sockets[1] == 29000 &&
|
||||
empty_update_changes[1].item.attrs[0].value == 17,
|
||||
"item: empty-cell update preserves both refresh snapshots");
|
||||
}
|
||||
|
||||
// GC_ITEM_GROUND_ADD / DEL
|
||||
// CPythonPlayer::SetItemData rejects an unknown ItemManager vnum before
|
||||
// touching the slot, and wiring the validator later also purges an item
|
||||
// accepted during the loading burst before item_proto was available.
|
||||
{
|
||||
EntityStore guarded;
|
||||
guarded.set_item_vnum_validator([](uint32_t vnum) { return vnum == 19; });
|
||||
guarded.mut_item_set(WINDOW_INVENTORY, 0, 999999, 1, 0, 0, nullptr, nullptr, true);
|
||||
CHECK(guarded.inv_slot(0).empty() && guarded.drain_inv().empty(),
|
||||
"item: unknown vnum rejected without change/highlight");
|
||||
guarded.mut_item_set(WINDOW_INVENTORY, 0, 19, 1, 0, 0, nullptr, nullptr);
|
||||
CHECK(guarded.inv_slot(0).vnum == 19, "item: known vnum accepted by validator");
|
||||
guarded.set_item_vnum_validator([](uint32_t) { return false; });
|
||||
CHECK(guarded.inv_slot(0).empty(), "item: late validator purges pre-existing unknown vnum");
|
||||
CHECK(guarded.drain_inv().size() == 2,
|
||||
"item: late validator emits accepted set and purge snapshots");
|
||||
}
|
||||
|
||||
// GC_ITEM_GROUND_ADD / DEL
|
||||
@@ -760,6 +1001,65 @@ int main() {
|
||||
&& q->counter_value == 3, "quest info fields (title/counter)");
|
||||
auto qch = es.drain_quest_changes();
|
||||
CHECK(qch.size() == 1 && qch[0] == 7, "quest change emitted");
|
||||
|
||||
// 40250 RecvQuestInfoPacket treats QUEST_SEND_IS_BEGIN + false as
|
||||
// QUEST_PACKET_TYPE_END and calls DeleteQuestInstance(index). The
|
||||
// pre-fix EntityStore kept the row with begin=false, so this assertion
|
||||
// deliberately fails until the delete branch is implemented.
|
||||
std::vector<uint8_t> qe(sizeof(GCQuestInfo) + 1, 0);
|
||||
GCQuestInfo end{};
|
||||
end.header = GC_QUEST_INFO;
|
||||
end.length = (uint16_t)qe.size();
|
||||
end.index = 7;
|
||||
end.flag = QUEST_SEND_IS_BEGIN;
|
||||
std::memcpy(qe.data(), &end, sizeof(end));
|
||||
es.apply(GC_QUEST_INFO, qe.data(), (uint16_t)qe.size());
|
||||
CHECK(es.quest(7) == nullptr, "quest end deletes the quest instance");
|
||||
auto end_changes = es.drain_quest_changes();
|
||||
CHECK(end_changes.size() == 1 && end_changes[0] == 7,
|
||||
"quest end emits one removal refresh");
|
||||
|
||||
// CPythonQuest keeps QuestInstance objects in insertion order. The
|
||||
// pre-fix EntityStore exposed unordered_map iteration order instead,
|
||||
// which made the quest-log panel reorder entries between updates.
|
||||
es.mut_quest_info(20, QUEST_SEND_IS_BEGIN, true, "Q20", "", 0, "", 0, "");
|
||||
es.mut_quest_info(5, QUEST_SEND_IS_BEGIN, true, "Q5", "", 0, "", 0, "");
|
||||
es.mut_quest_info(10, QUEST_SEND_IS_BEGIN, true, "Q10", "", 0, "", 0, "");
|
||||
auto quest_order = es.quest_indices();
|
||||
CHECK(quest_order.size() == 3 && quest_order[0] == 20 && quest_order[1] == 5
|
||||
&& quest_order[2] == 10, "quest indices preserve CPythonQuest vector order");
|
||||
|
||||
// A regular update keeps its position; a new BEGIN replaces the old
|
||||
// instance and moves that quest to the end, matching RegisterQuestInstance.
|
||||
es.mut_quest_info(5, QUEST_SEND_TITLE, false, "Q5 updated", "", 0, "", 0, "");
|
||||
quest_order = es.quest_indices();
|
||||
CHECK(quest_order.size() == 3 && quest_order[0] == 20 && quest_order[1] == 5
|
||||
&& quest_order[2] == 10, "quest update preserves insertion position");
|
||||
const QuestInfo *updated_quest = es.quest(5);
|
||||
CHECK(updated_quest && updated_quest->begin && updated_quest->title == "Q5 updated",
|
||||
"quest UPDATE keeps the instance active");
|
||||
es.mut_quest_info(20, QUEST_SEND_IS_BEGIN, true, "Q20 restarted", "", 0, "", 0, "");
|
||||
quest_order = es.quest_indices();
|
||||
CHECK(quest_order.size() == 3 && quest_order[0] == 5 && quest_order[1] == 10
|
||||
&& quest_order[2] == 20, "quest BEGIN replaces and appends instance");
|
||||
|
||||
// CPythonQuest starts a timer on BEGIN/MakeQuest and restarts it only
|
||||
// when QUEST_SEND_CLOCK_VALUE is received; an unrelated UPDATE must not
|
||||
// reset the countdown while the quest window is closed.
|
||||
es.set_now(1000);
|
||||
es.mut_quest_info(30, QUEST_SEND_IS_BEGIN | QUEST_SEND_CLOCK_VALUE, true,
|
||||
"Timed quest", "Time", 65, "", 0, "");
|
||||
const QuestInfo *timed = es.quest(30);
|
||||
CHECK(timed && timed->clock_start_ms == 1000, "quest BEGIN records clock start");
|
||||
es.set_now(2000);
|
||||
es.mut_quest_info(30, QUEST_SEND_TITLE, false, "Timed quest updated", "", 0, "", 0, "");
|
||||
timed = es.quest(30);
|
||||
CHECK(timed && timed->clock_start_ms == 1000, "quest UPDATE preserves clock start");
|
||||
es.set_now(3000);
|
||||
es.mut_quest_info(30, QUEST_SEND_CLOCK_VALUE, false, "", "", 40, "", 0, "");
|
||||
timed = es.quest(30);
|
||||
CHECK(timed && timed->clock_start_ms == 3000 && timed->clock_value == 40,
|
||||
"quest clock-value UPDATE restarts clock");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1075,6 +1375,19 @@ int main() {
|
||||
CHECK(warps.size() == 1 && warps[0].x == 12345 && warps[0].same_server(),
|
||||
"warp: queued, same-server");
|
||||
|
||||
// Reconnect handling consumes one replacement transport at a time. A
|
||||
// later warp must remain queued if the caller reconnects after the first.
|
||||
WarpCue first{100, 200, 0x0100007f, 13001, true};
|
||||
WarpCue second{300, 400, 0x0100007f, 13002, true};
|
||||
es.mut_warp(first.x, first.y, first.addr, first.port, first.reconnect);
|
||||
es.mut_warp(second.x, second.y, second.addr, second.port, second.reconnect);
|
||||
WarpCue next{};
|
||||
CHECK(es.take_next_warp(next) && next.x == first.x && next.port == first.port,
|
||||
"warp queue: first reconnect consumed in order");
|
||||
CHECK(es.take_next_warp(next) && next.x == second.x && next.port == second.port,
|
||||
"warp queue: second reconnect preserved");
|
||||
CHECK(!es.take_next_warp(next), "warp queue: empty after ordered consumption");
|
||||
|
||||
GCTime tm{GC_TIME, sizeof(GCTime), 1735689600};
|
||||
b = pkt(tm);
|
||||
es.apply(GC_TIME, b.data(), (uint16_t)b.size());
|
||||
@@ -1486,6 +1799,9 @@ int main() {
|
||||
cmd("combo 1");
|
||||
cmd("mobile_auth");
|
||||
cmd("gift");
|
||||
cmd("xmas_snow 1");
|
||||
cmd("xmas_snow 0");
|
||||
cmd("quit");
|
||||
cmd("kiss 1 2");
|
||||
cmd("slap 1 2");
|
||||
cmd("dance6 1");
|
||||
@@ -1493,7 +1809,9 @@ int main() {
|
||||
auto events = es.drain_server_commands();
|
||||
int denied = 0, safe = 0, mall = 0, ok = 0, fail = 0, open = 0, price = 0, block = 0;
|
||||
int observer = 0, observer_count = 0, stone = 0, stamina_start = 0, stamina_stop = 0;
|
||||
int mobile = 0, mobile_auth = 0, combo = 0, gift = 0;
|
||||
int mobile = 0, mobile_auth = 0, combo = 0, gift = 0, weather = 0;
|
||||
int snow_on = 0, snow_off = 0;
|
||||
int server_quit = 0;
|
||||
for (const auto &ev : events) {
|
||||
switch (ev.kind) {
|
||||
case ServerCommandEvent::PartyRequestDenied: ++denied; break;
|
||||
@@ -1536,12 +1854,28 @@ int main() {
|
||||
case ServerCommandEvent::GiftAvailable:
|
||||
++gift;
|
||||
break;
|
||||
case ServerCommandEvent::WeatherChanged:
|
||||
++weather;
|
||||
if (ev.value != 0) ++snow_on;
|
||||
else ++snow_off;
|
||||
break;
|
||||
case ServerCommandEvent::ServerQuitRequested:
|
||||
++server_quit;
|
||||
break;
|
||||
case ServerCommandEvent::LoverLogin:
|
||||
case ServerCommandEvent::LoverLogout:
|
||||
case ServerCommandEvent::LoverNear:
|
||||
case ServerCommandEvent::LoverFar:
|
||||
case ServerCommandEvent::LoverDivorce:
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(denied == 1 && safe == 1 && mall == 1 && ok == 1 && fail == 1 && open == 1
|
||||
&& price == 1 && block == 1 && observer == 1 && observer_count == 1
|
||||
&& stone == 1 && stamina_start == 1 && stamina_stop == 1 && mobile == 1
|
||||
&& mobile_auth == 1 && combo == 1 && gift == 1 && !es.stamina_consuming()
|
||||
&& mobile_auth == 1 && combo == 1 && gift == 1 && weather == 2
|
||||
&& snow_on == 1 && snow_off == 1 && server_quit == 1
|
||||
&& !es.stamina_consuming()
|
||||
&& es.current_stamina() == 55,
|
||||
"40250 command bus: callbacks surfaced");
|
||||
auto motions = es.drain_motions();
|
||||
@@ -1646,14 +1980,14 @@ int main() {
|
||||
}
|
||||
|
||||
// GC_CHARACTER_UPDATE for that same VID updates the row in place.
|
||||
d.mut_char_update(4242, nullptr, 40, 120, 7, -3, 1, 0);
|
||||
d.mut_char_update(4242, nullptr, 40, 120, 0, 0, 7, -3, 1, 0);
|
||||
orc = d.get(4242);
|
||||
CHECK(orc && orc->attack_speed == 120 && orc->moving_speed == 40 && orc->guild == 7,
|
||||
"§2.1 data layer: UPDATE mutates the existing row");
|
||||
|
||||
// Unknown VID: no create, no replay buffer, no size growth.
|
||||
const size_t n_before = d.size();
|
||||
d.mut_char_update(999999, nullptr, 10, 10, 0, 0, 0, 0);
|
||||
d.mut_char_update(999999, nullptr, 10, 10, 0, 0, 0, 0, 0, 0);
|
||||
d.mut_move(999999, 90.0f, 1, 5.0f, 5.0f, 200);
|
||||
d.mut_mount(999999, 20101);
|
||||
d.mut_char_info(999999, "ghost", nullptr, 1, 0, 5, 0, 0, 0);
|
||||
@@ -1667,6 +2001,10 @@ int main() {
|
||||
es.mut_ground_add(700, 27100, 12, 24, 0);
|
||||
es.mut_pvp(99, 1, 2);
|
||||
es.mut_target(99, 50);
|
||||
es.mut_fly(2, 99, 1);
|
||||
es.mut_fly_target(1, 99, 120, 240, false);
|
||||
es.mut_special_effect(99, 7);
|
||||
es.mut_set_point(POINT_LEVEL, 99, 99, 0);
|
||||
es.reset_for_map_change();
|
||||
CHECK(es.size() == 0 && es.main_vid() == 0, "map reset clears network entities");
|
||||
CHECK(es.ground_vids().empty() && es.pvp_relations().empty(),
|
||||
@@ -1674,6 +2012,10 @@ int main() {
|
||||
CHECK(es.target_vid() == 0 && es.take_target_dirty(), "map reset clears target state");
|
||||
CHECK(es.drain_changes().empty() && es.drain_ground().empty(),
|
||||
"map reset drops stale entity and ground deltas");
|
||||
CHECK(es.drain_fly_cues().empty() && es.drain_fly_target_cues().empty()
|
||||
&& es.drain_effect_cues().empty(),
|
||||
"map reset drops stale projectile, targeting, and effect cues");
|
||||
CHECK(es.drain_point_changes().empty(), "map reset drops stale point-change events");
|
||||
|
||||
if (g_fail) {
|
||||
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user