mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
299 lines
12 KiB
C
299 lines
12 KiB
C
/*
|
|
* test_lobby_chat_ring.c — the lobby chat ring, without a lobby.
|
|
*
|
|
* The server echoes every line to everyone in the room INCLUDING the sender,
|
|
* and that echo is the copy the ring keeps: the client never appends its own
|
|
* send. So the order on screen is the room's order, "mine" is a question
|
|
* about player ids rather than about who called send, and the ring's only
|
|
* jobs are to keep the newest lines and to number them monotonically.
|
|
*
|
|
* This includes psx_lobby_client.c directly so it exercises the real
|
|
* chat_push / chat_get rather than a copy of them.
|
|
*
|
|
* Build/run: ctest -R lobby_chat_ring_test
|
|
* (registered in runtime/CMakeLists.txt, which owns the source list.)
|
|
*/
|
|
#ifndef _POSIX_C_SOURCE
|
|
# define _POSIX_C_SOURCE 200809L
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "../src/psx_lobby_client.c"
|
|
|
|
/* The real recomp_net and the real WS helpers are linked in (see
|
|
* runtime/CMakeLists.txt) rather than stubbed: nothing here reaches a socket,
|
|
* so a wall of fakes would buy nothing and could drift from what the runtime
|
|
* actually links. The one exception is the host clock, which lives in the
|
|
* runtime's own main TU. */
|
|
uint64_t psx_host_mono_ms(void);
|
|
uint64_t psx_host_mono_ms(void)
|
|
{
|
|
/* Monotonic and cheap. The ring does not read the clock; this only has to
|
|
* exist and never go backwards. */
|
|
static uint64_t t;
|
|
return ++t;
|
|
}
|
|
|
|
static int g_failures;
|
|
|
|
static void ck(int cond, const char *what)
|
|
{
|
|
if (!cond) { printf("FAIL: %s\n", what); g_failures++; }
|
|
}
|
|
|
|
static void case_room_order(void)
|
|
{
|
|
PsxLobbyChatMsg got;
|
|
printf(" room order\n");
|
|
memset(&g_lc, 0, sizeof(g_lc));
|
|
snprintf(g_lc.player_id, sizeof(g_lc.player_id), "%s", "me");
|
|
|
|
chat_push("them", "Them", "first", NULL, NULL, 0);
|
|
chat_push("me", "Me", "second", NULL, NULL, 0);
|
|
chat_push("", "", "third", NULL, NULL, 1);
|
|
|
|
ck(psx_lobby_chat_count() == 3, "three lines land");
|
|
ck(psx_lobby_chat_get(0, &got) && strcmp(got.text, "first") == 0,
|
|
"index 0 is the oldest");
|
|
ck(psx_lobby_chat_get(2, &got) && strcmp(got.text, "third") == 0,
|
|
"index 2 is the newest");
|
|
|
|
ck(psx_lobby_chat_get(0, &got) && got.is_local == 0,
|
|
"a peer's line is not local");
|
|
ck(psx_lobby_chat_get(1, &got) && got.is_local == 1,
|
|
"our own echoed line is local");
|
|
ck(psx_lobby_chat_get(2, &got) && got.is_system == 1 && got.is_local == 0,
|
|
"a system line is neither ours nor a player's");
|
|
|
|
{
|
|
uint32_t a, b;
|
|
(void)psx_lobby_chat_get(0, &got); a = got.seq;
|
|
(void)psx_lobby_chat_get(2, &got); b = got.seq;
|
|
ck(b > a, "seq increases with arrival order");
|
|
}
|
|
ck(!psx_lobby_chat_get(3, &got), "reading past the end is refused");
|
|
ck(!psx_lobby_chat_get(-1, &got), "so is a negative index");
|
|
}
|
|
|
|
static void case_wrap_drops_oldest(void)
|
|
{
|
|
PsxLobbyChatMsg got;
|
|
char buf[32];
|
|
int i;
|
|
printf(" wrap\n");
|
|
memset(&g_lc, 0, sizeof(g_lc));
|
|
snprintf(g_lc.player_id, sizeof(g_lc.player_id), "%s", "me");
|
|
|
|
/* Overfill by ten. The ring must drop the OLDEST: a chat that discards
|
|
* what was just said is worse than no chat at all. */
|
|
for (i = 0; i < PSX_LOBBY_CHAT_RING + 10; ++i) {
|
|
snprintf(buf, sizeof(buf), "line%d", i);
|
|
chat_push("them", "Them", buf, NULL, NULL, 0);
|
|
}
|
|
ck(psx_lobby_chat_count() == PSX_LOBBY_CHAT_RING,
|
|
"the ring stops at its capacity");
|
|
ck(psx_lobby_chat_get(0, &got) && strcmp(got.text, "line10") == 0,
|
|
"the oldest surviving line is the 11th sent");
|
|
snprintf(buf, sizeof(buf), "line%d", PSX_LOBBY_CHAT_RING + 9);
|
|
ck(psx_lobby_chat_get(PSX_LOBBY_CHAT_RING - 1, &got) &&
|
|
strcmp(got.text, buf) == 0,
|
|
"the newest line is the last one sent");
|
|
}
|
|
|
|
static void case_empty_and_clear(void)
|
|
{
|
|
PsxLobbyChatMsg m;
|
|
uint32_t before;
|
|
printf(" empty/clear\n");
|
|
memset(&g_lc, 0, sizeof(g_lc));
|
|
|
|
chat_push("them", "Them", "", NULL, NULL, 0);
|
|
chat_push("them", "Them", NULL, NULL, NULL, 0);
|
|
ck(psx_lobby_chat_count() == 0, "an empty line is not a line");
|
|
|
|
chat_push("them", "Them", "hello", NULL, NULL, 0);
|
|
ck(psx_lobby_chat_count() == 1, "a real line is");
|
|
(void)psx_lobby_chat_get(0, &m);
|
|
before = m.seq;
|
|
|
|
psx_lobby_chat_clear();
|
|
ck(psx_lobby_chat_count() == 0, "clear empties the room log");
|
|
|
|
chat_push("them", "Them", "new room", NULL, NULL, 0);
|
|
(void)psx_lobby_chat_get(0, &m);
|
|
/* seq must NOT restart. A UI tracking "newest seen" would otherwise
|
|
* mistake the first line of a new room for one it had already scrolled
|
|
* past, and never scroll to it. */
|
|
ck(m.seq > before, "seq keeps counting across a clear");
|
|
}
|
|
|
|
static void case_long_line_is_truncated_not_dropped(void)
|
|
{
|
|
PsxLobbyChatMsg got;
|
|
char big[PSX_LOBBY_CHAT_TEXT_LEN * 2];
|
|
printf(" long line\n");
|
|
memset(&g_lc, 0, sizeof(g_lc));
|
|
memset(big, 'x', sizeof(big) - 1);
|
|
big[sizeof(big) - 1] = '\0';
|
|
|
|
chat_push("them", "Them", big, NULL, NULL, 0);
|
|
ck(psx_lobby_chat_count() == 1, "an over-long line still arrives");
|
|
ck(psx_lobby_chat_get(0, &got), "and reads back");
|
|
ck(strlen(got.text) == PSX_LOBBY_CHAT_TEXT_LEN - 1,
|
|
"clipped to the field, not past it");
|
|
ck(got.text[PSX_LOBBY_CHAT_TEXT_LEN - 1] == '\0',
|
|
"and still NUL-terminated");
|
|
}
|
|
|
|
static void case_mod_caps_roundtrip(void)
|
|
{
|
|
PsxLobbyMatchCaps caps = {0};
|
|
char wire[4096];
|
|
caps.valid = 1;
|
|
caps.aspect_num = 4;
|
|
caps.aspect_den = 3;
|
|
caps.rollback = 1;
|
|
caps.mod_count = 2;
|
|
memset(caps.mod_plan_fp, 'a', 64);
|
|
strcpy(caps.mods[0].id, "content.cars");
|
|
strcpy(caps.mods[0].ver, "1.0.0");
|
|
strcpy(caps.mods[0].name, "Cars \"Original\"");
|
|
strcpy(caps.mods[0].feats, "roster,view=mode~fit");
|
|
strcpy(caps.mods[1].id, "content.arena");
|
|
strcpy(caps.mods[1].ver, "0.1.0");
|
|
strcpy(caps.mods[1].feats, "arena");
|
|
ck(append_match_caps_json(wire, sizeof(wire), &caps), "serialize host mod caps");
|
|
ingest_match_caps_from_json(wire);
|
|
ck(g_lc.match_caps.valid && g_lc.match_caps.rollback &&
|
|
g_lc.match_caps.mod_count == 2, "roundtrip two required packages");
|
|
ck(!strcmp(g_lc.match_caps.mod_plan_fp, caps.mod_plan_fp) &&
|
|
!strcmp(g_lc.match_caps.mods[0].id, "content.cars") &&
|
|
!strcmp(g_lc.match_caps.mods[0].feats, "roster,view=mode~fit") &&
|
|
!strcmp(g_lc.match_caps.mods[1].ver, "0.1.0"),
|
|
"roundtrip fingerprint, versions, features and options");
|
|
ck(strstr(wire, "resources") == NULL && strstr(wire, "path") == NULL,
|
|
"lobby plan carries no donor paths or asset bytes");
|
|
{
|
|
PsxLobbyMatchCaps received = {0};
|
|
char tiny[32];
|
|
ck(psx_lobby_encode_match_caps(wire, sizeof(wire), &caps), "LAN uses same host caps serializer");
|
|
ck(psx_lobby_decode_match_caps(wire, &received) && received.mod_count == 2 &&
|
|
!strcmp(received.mod_plan_fp, caps.mod_plan_fp) &&
|
|
!strcmp(received.mods[0].feats, caps.mods[0].feats), "LAN preserves host selections and options");
|
|
ck(!psx_lobby_encode_match_caps(tiny, sizeof(tiny), &caps), "truncated LAN plans fail closed");
|
|
wire[strlen(wire) - 2] = '\0';
|
|
ck(!psx_lobby_decode_match_caps(wire, &received), "incomplete LAN caps rejected");
|
|
psx_lobby_set_local_match_caps(&caps);
|
|
ck(psx_lobby_match_caps()->mod_count == 2, "LAN plan supplied to launch provider");
|
|
psx_lobby_set_local_match_caps(NULL);
|
|
ck(!psx_lobby_match_caps()->valid, "leaving LAN retires host plan");
|
|
}
|
|
parse_match_caps_object("{\"mod_plan_fp\":\"invalid!\",\"mods\":[]}", &caps);
|
|
ck(!caps.mod_plan_fp[0], "invalid fingerprint rejected");
|
|
}
|
|
|
|
static void case_profile_settings_roundtrip(void)
|
|
{
|
|
PsxLobbyMatchCaps caps = {0}, received = {0};
|
|
char wire[4096];
|
|
caps.valid = 1;
|
|
caps.aspect_num = 4;
|
|
caps.aspect_den = 3;
|
|
caps.mod_count = 1;
|
|
strcpy(caps.mods[0].id, "runtime.test");
|
|
strcpy(caps.mods[0].ver, "1.0.0");
|
|
strcpy(caps.mods[0].feats, "vblank-plugin=cameras~split");
|
|
ck(psx_lobby_encode_match_caps(wire, sizeof(wire), &caps) &&
|
|
!psx_lobby_decode_match_caps(wire, &received), "settings without an identity cannot enter a LAN session");
|
|
strcpy(caps.mod_plan_fp, "24a3aeaf6034d1a59df3ac27334227343237550d4938b7b00c6ae77dfec21367");
|
|
ck(psx_lobby_encode_match_caps(wire, sizeof(wire), &caps) &&
|
|
psx_lobby_decode_match_caps(wire, &received) && received.mod_count == 1 &&
|
|
!strcmp(received.mod_plan_fp, caps.mod_plan_fp) &&
|
|
!strcmp(received.mods[0].feats, caps.mods[0].feats), "trusted settings survive the real LAN codec");
|
|
ingest_match_caps_from_json(wire);
|
|
ck(g_lc.match_caps.mod_count == 1 &&
|
|
!strcmp(g_lc.match_caps.mod_plan_fp, caps.mod_plan_fp) &&
|
|
!strcmp(g_lc.match_caps.mods[0].feats, caps.mods[0].feats), "trusted settings survive the online lobby codec");
|
|
}
|
|
|
|
static void case_full_title_profile_version(void)
|
|
{
|
|
static char version[64];
|
|
static PSXModNetplayProfile profile = {0};
|
|
char wire[768];
|
|
memset(version, 'v', sizeof(version) - 1);
|
|
version[sizeof(version) - 1] = '\0';
|
|
profile.plugin_id = "lobby.profile.test";
|
|
profile.compatibility_id = version;
|
|
ck(psx_mod_register_netplay_profile(&profile), "register maximum-length title profile");
|
|
psx_lobby_set_game_identity("Profile Fixture", version);
|
|
ck(!strcmp(g_lc.filter_game_version, version), "retain all 63 profile bytes in local metadata");
|
|
/* Suppress discovery: this fixture checks the native codec/filter only. */
|
|
g_list_rtt_active = 1;
|
|
snprintf(wire, sizeof(wire), "{\"op\":\"lobby_list\",\"lobbies\":[{"
|
|
"\"lobby_id\":\"profile-room\",\"name\":\"Profile Room\","
|
|
"\"game_name\":\"Profile Fixture\",\"game_version\":\"%s\"}]}", version);
|
|
handle_server_json(wire);
|
|
ck(g_lc.list_count == 1, "matching full profile remains visible");
|
|
ck(!strcmp(g_lc.list[0].game_version, version), "room metadata retains full profile");
|
|
/* A difference beyond the former 31-byte prefix must still reject. */
|
|
char *last = strstr(wire, version);
|
|
if (last) last[62] = 'w';
|
|
ck(last != NULL, "locate profile in fixture");
|
|
handle_server_json(wire);
|
|
ck(g_lc.list_count == 0, "reject profile differing only in final byte");
|
|
g_list_rtt_active = 0;
|
|
}
|
|
|
|
static void case_start_retains_relay_path(void)
|
|
{
|
|
PsxLobbyMatchCaps caps = {0}, decoded;
|
|
char wire[4096];
|
|
int prefer, host;
|
|
const int old_pref = g_relay_via_ice_pref;
|
|
printf(" START keeps the advertised relay path locally\n");
|
|
for (prefer = 0; prefer < 2; ++prefer) {
|
|
for (host = 0; host < 2; ++host) {
|
|
memset(&g_lc, 0, sizeof(g_lc));
|
|
/* An unhandshaken fixture queues START without touching a socket. */
|
|
g_lc.connected = g_lc.in_lobby = g_lc.is_host = 1;
|
|
g_lc.fd = 0;
|
|
caps.valid = 1;
|
|
caps.relay_host = host;
|
|
caps.relay_via_ice = 0; /* fresh launcher caps, as in the regression */
|
|
strcpy(caps.session_bios, "openbios");
|
|
psx_lobby_set_relay_via_ice_pref(prefer);
|
|
ck(psx_lobby_request_start(&caps) == 0, "queue host START");
|
|
ck(psx_lobby_encode_match_caps(wire, sizeof(wire), &caps) &&
|
|
psx_lobby_decode_match_caps(wire, &decoded), "decode advertised START caps");
|
|
ck(g_lc.match_caps.relay_via_ice == decoded.relay_via_ice,
|
|
"local ICE path matches the wire before LAUNCH arrives");
|
|
ck(room_relays_via_ice() == (host && prefer && rnet_host_ice_available()),
|
|
"waiting-room agents remain active for an ICE host START");
|
|
}
|
|
}
|
|
memset(&g_lc, 0, sizeof(g_lc));
|
|
g_lc.fd = -1;
|
|
psx_lobby_set_relay_via_ice_pref(old_pref);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
case_room_order();
|
|
case_wrap_drops_oldest();
|
|
case_empty_and_clear();
|
|
case_long_line_is_truncated_not_dropped();
|
|
case_mod_caps_roundtrip();
|
|
case_profile_settings_roundtrip();
|
|
case_full_title_profile_version();
|
|
case_start_retains_relay_path();
|
|
if (g_failures == 0) {
|
|
printf("lobby_chat_ring_test: ok\n");
|
|
return 0;
|
|
}
|
|
fprintf(stderr, "lobby_chat_ring_test: %d failure(s)\n", g_failures);
|
|
return 1;
|
|
}
|