fix: 装备属性面板避让逻辑 + 多项功能更新

- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
This commit is contained in:
shen
2026-09-21 16:38:59 -07:00
parent 1522a8a1a1
commit c93894313a
5498 changed files with 4558004 additions and 1442 deletions
+155 -3
View File
@@ -77,6 +77,16 @@ int main() {
CHECK(sync_echo.time == 2000u + 2u * 10u && sync_echo.delta == 0,
"later handshake time fields are converged");
CHECK(out[sizeof(Handshake)] == SEQUENCE_TABLE[0], "time-sync uses next sequence byte");
// The reference keeps the old active clock until the blank
// HANDSHAKE_OK arrives; the later handshake is only a pending proposal.
const uint32_t before_ok = s.server_frame_ms();
CHECK(before_ok < 100000u, "resync does not activate the new server clock early");
uint8_t ok = HDR_GC_TIME_SYNC;
s.feed(&ok, 1);
const uint32_t after_ok = s.server_frame_ms();
CHECK(after_ok >= 2000u && after_ok < 10000u,
"HANDSHAKE_OK applies the staged server clock correction");
}
// -------------------------------------------- server frame clock (§3.2)
@@ -191,6 +201,101 @@ int main() {
"dynamic: text tail present");
}
// ---------------------------------------------------------- static whisper head + text tail
{
ClassicStream s;
std::vector<uint8_t> headers;
s.on_packet = [&](uint8_t h, const uint8_t *, uint32_t) {
headers.push_back(h);
return true;
};
GCWhisperHead whisper{};
whisper.header = HDR_GC_WHISPER;
whisper.type = 1;
std::strcpy(whisper.name_from, "Sender");
const char *text = "hi";
whisper.size = static_cast<uint16_t>(sizeof(whisper) + std::strlen(text));
std::vector<uint8_t> wire = raw(whisper);
wire.insert(wire.end(), text, text + std::strlen(text));
GCCharacterDel next{HDR_GC_CHARACTER_DEL, 77};
auto next_wire = raw(next);
wire.insert(wire.end(), next_wire.begin(), next_wire.end());
s.feed(wire.data(), sizeof(GCWhisperHead) / 2);
CHECK(headers.empty(), "whisper partial fixed head waits");
s.feed(wire.data() + sizeof(GCWhisperHead) / 2,
wire.size() - sizeof(GCWhisperHead) / 2);
CHECK(headers.size() == 2 && headers[0] == HDR_GC_WHISPER &&
headers[1] == HDR_GC_CHARACTER_DEL,
"whisper text tail does not swallow the following static packet");
}
// The reference consumes HYBRIDCRYPT payloads in every phase and returns
// without sending them through the normal GC parser. They must not become a
// false unsupported-packet disconnect in the adapted client.
{
ClassicStream s;
int callbacks = 0;
int errors = 0;
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) {
++callbacks;
return true;
};
s.on_error = [&](const std::string &) { ++errors; };
DynHead key{HDR_GC_HYBRIDCRYPT_KEYS, static_cast<uint16_t>(sizeof(DynHead))};
auto b = raw(key);
s.feed(b.data(), b.size());
CHECK(callbacks == 0 && errors == 0,
"HYBRIDCRYPT_KEYS is consumed without normal parser callback");
}
// A phase packet returns from the current reference phase function. Bytes
// already buffered behind it stay for the next phase tick.
{
ClassicStream s;
int phases = 0;
int packets = 0;
s.on_phase = [&](uint8_t) { ++phases; };
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) {
++packets;
return true;
};
Phase_ phase{HDR_GC_PHASE, PHASE_GAME};
GCCharacterDel next{HDR_GC_CHARACTER_DEL, 42};
std::vector<uint8_t> burst = raw(phase);
auto next_bytes = raw(next);
burst.insert(burst.end(), next_bytes.begin(), next_bytes.end());
s.feed(burst.data(), burst.size());
CHECK(phases == 1 && packets == 0,
"phase boundary stops dispatch before the pipelined next packet");
s.feed(nullptr, 0);
CHECK(packets == 1,
"pipelined packet is dispatched on the next phase tick");
}
// GamePhase's bounded pump stops after the third packet while the receive
// buffer is below 8192 bytes. The fourth packet remains for the next tick.
{
ClassicStream s;
int packets = 0;
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) {
++packets;
return true;
};
Phase_ phase{HDR_GC_PHASE, PHASE_GAME};
s.feed(&phase, sizeof(phase));
GCCharacterDel del{HDR_GC_CHARACTER_DEL, 100};
std::vector<uint8_t> burst;
for (int i = 0; i < 4; ++i) {
auto bytes = raw(del);
bytes[1] = static_cast<uint8_t>(100 + i);
burst.insert(burst.end(), bytes.begin(), bytes.end());
}
s.feed(burst.data(), burst.size());
CHECK(packets == 3, "GamePhase bounded pump consumes three small packets");
s.feed(nullptr, 0);
CHECK(packets == 4, "GamePhase bounded pump resumes on the next tick");
}
// ---------------------------------------------------------- send seq sequencing
{
ClassicStream s;
@@ -234,6 +339,28 @@ int main() {
CHECK(hits == 1, "partial: delivered once the rest arrives");
}
// CheckPacket strips only a leading run of zero padding. A zero-only read
// is incomplete and must not be reported as an unknown packet.
{
ClassicStream s;
int hits = 0;
std::string err;
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) {
++hits;
return true;
};
s.on_error = [&](const std::string &e) { err = e; };
const uint8_t padding[] = {0, 0};
s.feed(padding, sizeof(padding));
CHECK(hits == 0 && err.empty(), "zero-only leading padding waits without error");
GCCharacterDel del{HDR_GC_CHARACTER_DEL, 100};
auto b = raw(del);
std::vector<uint8_t> padded = {0, 0};
padded.insert(padded.end(), b.begin(), b.end());
s.feed(padded.data(), padded.size());
CHECK(hits == 1 && err.empty(), "leading zero run is removed before valid packet");
}
// ---------------------------------------------------------- unknown header
{
ClassicStream s;
@@ -294,13 +421,21 @@ int main() {
ClassicStream s;
std::string err;
s.on_error = [&](const std::string &e) { err = e; };
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) { return false; };
int calls = 0;
s.on_packet = [&](uint8_t, const uint8_t *, uint32_t) {
++calls;
return calls > 1;
};
GCCharacterDel del{HDR_GC_CHARACTER_DEL, 99};
auto b = raw(del);
s.feed(b.data(), b.size());
std::vector<uint8_t> glued = b;
glued.insert(glued.end(), b.begin(), b.end());
s.feed(glued.data(), glued.size());
CHECK(err.find("packet handler rejected") != std::string::npos,
"rejected packet -> explicit on_error");
CHECK(s.state() == ClassicStream::State::Offline, "rejected packet -> disconnect");
CHECK(calls == 1, "rejected packet clears pipelined receive bytes");
s.feed(b.data(), b.size());
CHECK(calls == 2, "stream owner survives known handler failure");
}
// ---- GC_KEY_AGREEMENT: a partial packet just waits (no error, no crash) ----
@@ -315,6 +450,23 @@ int main() {
CHECK(err.empty(), "partial GC_KEY_AGREEMENT -> buffered, no error");
}
// ------------------------------------------------ non-blocking connect gate
{
// TEST-NET-1 gives us a deterministic unroutable destination on normal
// CI hosts. If the environment rejects it synchronously, keep the rest of
// the socket-free suite usable; when it is pending, limit_ms=0 proves that
// Connecting is not promoted to Online before writability/timeout.
ClassicStream s;
if (s.connect("192.0.2.1", 9, 0) && s.state() == ClassicStream::State::Connecting) {
s.process();
CHECK(s.state() == ClassicStream::State::Offline,
"pending classic connect times out before Online");
CHECK(s.last_error() == "connect timeout", "pending classic connect reports timeout");
} else {
std::puts("classic connect timeout probe skipped (connect was synchronous)");
}
}
if (g_fail) {
std::fprintf(stderr, "%d check(s) failed\n", g_fail);
return 1;