client connection
This commit is contained in:
parent
cacdf10229
commit
3cdff3c4c5
@ -16,6 +16,7 @@
|
||||
#include <uuids.h>
|
||||
#include <Knownfolders.h>
|
||||
#include <WinSock2.h>
|
||||
#include <Mswsock.h>
|
||||
#include <TlHelp32.h>
|
||||
#include <WS2tcpip.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
// NOTE: Burst messages with lengths exceeding packet size will degrade
|
||||
// into sequenced messages
|
||||
|
||||
#define NET_PacketSize 1024
|
||||
|
||||
#define NET_Ephemeral 0xFFFFFFFFFFFFFFFFull
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
@ -23,6 +19,17 @@ Struct(NET_Key)
|
||||
#define NET_NilKey ((NET_Key) { 0 })
|
||||
#define NET_IsKeyNil(k) (MatchStructZero(&k))
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//~ Send types
|
||||
|
||||
Enum(NET_SendFlag)
|
||||
{
|
||||
NET_SendFlag_None = 0,
|
||||
|
||||
// NOTE: Messages marked "raw" that exceed packet size will drop
|
||||
NET_SendFlag_Raw = (1 << 0),
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//~ Message types
|
||||
|
||||
@ -69,6 +76,6 @@ NET_PipeHandle NET_AcquirePipe(void);
|
||||
void NET_Bind(NET_PipeHandle pipe_handle, u64 port);
|
||||
|
||||
NET_MsgList NET_Swap(Arena *arena, NET_PipeHandle pipe_handle);
|
||||
void NET_Push(NET_PipeHandle pipe_handle, NET_Key dst, String data, b32 burst);
|
||||
void NET_Send(NET_PipeHandle pipe_handle, NET_Key dst, String data, NET_SendFlag flags);
|
||||
|
||||
NET_PipeStatistics NET_StatsFromPipe(NET_PipeHandle pipe_handle);
|
||||
|
||||
@ -46,8 +46,12 @@ struct sockaddr_in6 NET_W32_AddressFromKey(NET_Key key)
|
||||
|
||||
void NET_W32_SignalWorker(void)
|
||||
{
|
||||
// TODO: Only post if previous signal was seen
|
||||
i64 signal = Atomic64Fetch(&NET_W32.signal);
|
||||
if (Atomic64Fetch(&NET_W32.seen_signal) == signal)
|
||||
{
|
||||
Atomic64FetchAdd(&NET_W32.signal, 1);
|
||||
PostQueuedCompletionStatus(NET_W32.iocp, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
NET_W32_Peer *NET_W32_TouchPeerFromKey(NET_W32_Pipe *pipe, NET_Key key)
|
||||
@ -122,7 +126,7 @@ void NET_W32_PostRecv(NET_W32_Pipe *pipe)
|
||||
pipe->iocp_count += 1;
|
||||
|
||||
DWORD flags = 0;
|
||||
i32 ret = WSARecvFrom(
|
||||
WSARecvFrom(
|
||||
pipe->udp,
|
||||
&pipe->recv_wsabuff, 1,
|
||||
0,
|
||||
@ -249,7 +253,11 @@ NET_PipeHandle NET_AcquirePipe(void)
|
||||
void NET_Bind(NET_PipeHandle pipe_handle, u64 port)
|
||||
{
|
||||
NET_W32_Pipe *pipe = NET_W32_PipeFromHandle(pipe_handle);
|
||||
Atomic64Set(&pipe->desired_port, port);
|
||||
u64 prev_desired_port = Atomic64FetchSet(&pipe->desired_port, port);
|
||||
if (prev_desired_port != port)
|
||||
{
|
||||
NET_W32_SignalWorker();
|
||||
}
|
||||
}
|
||||
|
||||
NET_MsgList NET_Swap(Arena *arena, NET_PipeHandle pipe_handle)
|
||||
@ -270,7 +278,7 @@ NET_MsgList NET_Swap(Arena *arena, NET_PipeHandle pipe_handle)
|
||||
return msg_buff->msgs;
|
||||
}
|
||||
|
||||
void NET_Push(NET_PipeHandle pipe_handle, NET_Key dst, String data, b32 burst)
|
||||
void NET_Send(NET_PipeHandle pipe_handle, NET_Key dst, String data, NET_SendFlag flags)
|
||||
{
|
||||
NET_W32_Pipe *pipe = NET_W32_PipeFromHandle(pipe_handle);
|
||||
if (Atomic64Fetch(&pipe->desired_port) == 0)
|
||||
@ -283,7 +291,7 @@ void NET_Push(NET_PipeHandle pipe_handle, NET_Key dst, String data, b32 burst)
|
||||
NET_W32_Cmd *cmd = PushStruct(cmd_buff->arena, NET_W32_Cmd);
|
||||
cmd->key = dst;
|
||||
cmd->data = PushString(cmd_buff->arena, data);
|
||||
cmd->burst = burst;
|
||||
cmd->send_flags = flags;
|
||||
SllQueuePush(cmd_buff->cmds.first, cmd_buff->cmds.last, cmd);
|
||||
++cmd_buff->cmds.count;
|
||||
}
|
||||
@ -371,34 +379,33 @@ void NET_W32_TickForever(WaveLaneCtx *lane)
|
||||
i32 timeout_ms = MsFromNs(passive_run_threshold_ns);
|
||||
if (wake_count > 0)
|
||||
{
|
||||
// Keep processing without sleeping if we've already received a completion this frame
|
||||
// Continue processing without sleeping if we've already received a completion this frame
|
||||
timeout_ms = 0;
|
||||
}
|
||||
//
|
||||
// NOTE: We're really only using IOCP so that we have a way to wake
|
||||
// the worker mid-wait without having to use WSAPoll + a dummy socket.
|
||||
// This is because any data sent to that dummy socket will itself be
|
||||
// affected by network testing tools such as Clumsy, adding additional
|
||||
// unpredictability. E.g. if Clumsy is set to add 50ms of artificial
|
||||
// and drop 10% of packets on loopback, attempts to wake the worker
|
||||
// thread so that it can process queued cmds will be affected as well.
|
||||
// affected by network testing tools such as Clumsy.
|
||||
//
|
||||
ok = GetQueuedCompletionStatus(NET_W32.iocp, &len, &iocp_key, &ovl, timeout_ms);
|
||||
wake_count += 1;
|
||||
Atomic64Set(&NET_W32.seen_signal, Atomic64Fetch(&NET_W32.signal));
|
||||
}
|
||||
i32 err = GetLastError();
|
||||
if (!ok)
|
||||
{
|
||||
i32 err = GetLastError();
|
||||
if (err == WAIT_TIMEOUT)
|
||||
{
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ovl)
|
||||
{
|
||||
NET_W32_Pipe *pipe = (NET_W32_Pipe *)ovl;
|
||||
pipe->iocp_count -= 1;
|
||||
|
||||
if (ok)
|
||||
{
|
||||
String recv = STRING(len, (u8 *)pipe->recv_wsabuff.buf);
|
||||
@ -551,8 +558,11 @@ void NET_W32_TickForever(WaveLaneCtx *lane)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-post recv
|
||||
if (err != ERROR_OPERATION_ABORTED)
|
||||
{
|
||||
NET_W32_PostRecv(pipe);
|
||||
}
|
||||
}
|
||||
@ -586,6 +596,8 @@ void NET_W32_TickForever(WaveLaneCtx *lane)
|
||||
(pipe->last_attempted_bind_ns == 0 || now_ns - pipe->last_attempted_bind_ns > bind_retry_threshold_ns)
|
||||
)
|
||||
{
|
||||
// LogDebugF("Binding to port \"%F\"", FmtUint(desired_port));
|
||||
|
||||
b32 ok = 1;
|
||||
String port_str = StringF(scratch.arena, "%F", FmtUint(desired_port));
|
||||
char *port_cstr = CstrFromString(scratch.arena, port_str);
|
||||
@ -679,6 +691,32 @@ void NET_W32_TickForever(WaveLaneCtx *lane)
|
||||
ok = ioctlsocket(sock, FIONBIO, &nonblocking) == 0;
|
||||
}
|
||||
|
||||
//- Disable ICMP reporting
|
||||
{
|
||||
DWORD bytes = 0;
|
||||
b32 report = 0;
|
||||
int rc = WSAIoctl(
|
||||
sock,
|
||||
SIO_UDP_CONNRESET,
|
||||
&report, sizeof(report),
|
||||
0, 0,
|
||||
&bytes,
|
||||
0, 0
|
||||
);
|
||||
}
|
||||
{
|
||||
DWORD bytes = 0;
|
||||
b32 report = 0;
|
||||
int rc = WSAIoctl(
|
||||
sock,
|
||||
SIO_UDP_NETRESET,
|
||||
&report, sizeof(report),
|
||||
0, 0,
|
||||
&bytes,
|
||||
0, 0
|
||||
);
|
||||
}
|
||||
|
||||
//- Fetch bound port
|
||||
u64 bound_port = 0;
|
||||
{
|
||||
@ -715,6 +753,8 @@ void NET_W32_TickForever(WaveLaneCtx *lane)
|
||||
|
||||
// Post initial recv
|
||||
CreateIoCompletionPort((HANDLE)sock, NET_W32.iocp, 0, 0);
|
||||
|
||||
|
||||
NET_W32_PostRecv(pipe);
|
||||
}
|
||||
else
|
||||
@ -835,7 +875,7 @@ void NET_W32_TickForever(WaveLaneCtx *lane)
|
||||
i64 msg_seq = ++peer->msg_seq;
|
||||
// LogDebugF("Queued msg with seq %F", FmtSint(msg_seq));
|
||||
|
||||
// TODO: Burst packets
|
||||
// TODO: Raw packets
|
||||
|
||||
b32 is_msg_end = 0;
|
||||
while (!is_msg_end)
|
||||
|
||||
@ -7,7 +7,7 @@ Struct(NET_W32_Cmd)
|
||||
|
||||
NET_Key key;
|
||||
String data;
|
||||
b32 burst;
|
||||
NET_SendFlag send_flags;
|
||||
};
|
||||
|
||||
Struct(NET_W32_CmdList)
|
||||
@ -43,7 +43,7 @@ Enum(NET_W32_PacketFlag)
|
||||
|
||||
NET_W32_PacketFlag_EndMsg = (1 << 0),
|
||||
NET_W32_PacketFlag_Heartbeat = (1 << 2),
|
||||
// NET_W32_PacketFlag_Burst = (1 << 3),
|
||||
// NET_W32_PacketFlag_Raw = (1 << 3),
|
||||
};
|
||||
|
||||
Struct(NET_W32_PacketHeader)
|
||||
@ -136,7 +136,8 @@ Struct(NET_W32_Pipe)
|
||||
NET_W32_Pipe *next;
|
||||
NET_W32_Pipe *prev;
|
||||
|
||||
Atomic64 desired_port; // >64k means ephemeral
|
||||
Atomic64 desired_port; // ports >64k become ephemeral
|
||||
Atomic64 seen_desired_port;
|
||||
|
||||
Atomic64 total_bytes_sent;
|
||||
Atomic64 total_bytes_received;
|
||||
@ -184,6 +185,9 @@ Struct(NET_W32_Ctx)
|
||||
NET_W32_Packet *first_free_packet;
|
||||
|
||||
HANDLE iocp;
|
||||
|
||||
Atomic64 signal;
|
||||
Atomic64 seen_signal;
|
||||
};
|
||||
|
||||
extern NET_W32_Ctx NET_W32;
|
||||
|
||||
84
src/pp/pp.c
84
src/pp/pp.c
@ -1166,6 +1166,49 @@ void P_DebugDrawShape(P_Shape shape, Vec4 srgb)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void P_DebugDrawFrame(P_Frame *frame, Vec4 tint)
|
||||
{
|
||||
if (P_tl.debug_draw_enabled)
|
||||
{
|
||||
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
||||
{
|
||||
P_Shape world_shape = P_WorldShapeFromEnt(ent);
|
||||
|
||||
// Draw aabb
|
||||
{
|
||||
Vec4 color = VEC4(0.4, 0.2, 0.2, 1);
|
||||
Rng2 bb = P_BoundingBoxFromShape(world_shape);
|
||||
P_DebugDrawRect(bb, color);
|
||||
}
|
||||
|
||||
// Draw shape
|
||||
{
|
||||
// Vec4 color = Color_Cyan;
|
||||
// Vec4 color = VEC4(0.2, 0.4, 0.2, 1);
|
||||
Vec4 color = MulVec4Vec4(VEC4(1, 1, 1, 1), tint);
|
||||
P_DebugDrawShape(world_shape, color);
|
||||
}
|
||||
|
||||
// Draw rot
|
||||
{
|
||||
Vec4 color = VEC4(0.8, 0.8, 0.8, 1);
|
||||
Vec2 p0 = world_shape.centroid;
|
||||
Vec2 p1 = P_EdgePointFromShape(world_shape, UpFromXform(ent->xf));
|
||||
P_DebugDrawLine(p0, p1, color);
|
||||
}
|
||||
|
||||
// Draw look
|
||||
{
|
||||
Vec4 color = VEC4(0.4, 0.8, 0.4, 1);
|
||||
Vec2 p0 = world_shape.centroid;
|
||||
Vec2 p1 = P_EdgePointFromShape(world_shape, ent->control.look);
|
||||
P_DebugDrawLine(p0, p1, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//~ Msg
|
||||
|
||||
@ -2038,47 +2081,6 @@ void P_StepFrame(P_Frame *frame)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
//- Debug draw entities
|
||||
|
||||
if (P_tl.debug_draw_enabled)
|
||||
{
|
||||
for (P_Ent *ent = P_FirstEnt(frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
|
||||
{
|
||||
P_Shape world_shape = P_WorldShapeFromEnt(ent);
|
||||
|
||||
// Draw aabb
|
||||
{
|
||||
Vec4 color = VEC4(0.4, 0.2, 0.2, 1);
|
||||
Rng2 bb = P_BoundingBoxFromShape(world_shape);
|
||||
P_DebugDrawRect(bb, color);
|
||||
}
|
||||
|
||||
// Draw shape
|
||||
{
|
||||
// Vec4 color = Color_Cyan;
|
||||
Vec4 color = VEC4(0.2, 0.4, 0.2, 1);
|
||||
P_DebugDrawShape(world_shape, color);
|
||||
}
|
||||
|
||||
// Draw rot
|
||||
{
|
||||
Vec4 color = VEC4(0.8, 0.8, 0.8, 1);
|
||||
Vec2 p0 = world_shape.centroid;
|
||||
Vec2 p1 = P_EdgePointFromShape(world_shape, UpFromXform(ent->xf));
|
||||
P_DebugDrawLine(p0, p1, color);
|
||||
}
|
||||
|
||||
// Draw look
|
||||
{
|
||||
Vec4 color = VEC4(0.4, 0.8, 0.4, 1);
|
||||
Vec2 p0 = world_shape.centroid;
|
||||
Vec2 p1 = P_EdgePointFromShape(world_shape, ent->control.look);
|
||||
P_DebugDrawLine(p0, p1, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
//- End frame
|
||||
|
||||
|
||||
@ -69,6 +69,9 @@ Struct(P_DebugDrawNode)
|
||||
|
||||
// TODO: Pack efficiently, deduplicate redundant fields
|
||||
|
||||
#define P_MinPlayerNameLen 1
|
||||
#define P_MaxPlayerNameLen 24
|
||||
|
||||
Struct(P_Control)
|
||||
{
|
||||
Vec2 move;
|
||||
@ -128,7 +131,7 @@ Struct(P_Ent)
|
||||
NET_Key net;
|
||||
|
||||
u8 string_len;
|
||||
u8 string_text[32];
|
||||
u8 string_text[P_MaxPlayerNameLen + 8];
|
||||
|
||||
//- Solver
|
||||
|
||||
@ -308,6 +311,7 @@ Enum(P_MsgKind)
|
||||
|
||||
// Server <-> Client
|
||||
P_MsgKind_Chat,
|
||||
P_MsgKind_Connect,
|
||||
|
||||
// Client -> Server
|
||||
// P_MsgKind_UserSnapshot,
|
||||
@ -324,6 +328,7 @@ Enum(P_MsgKind)
|
||||
Struct(P_Msg)
|
||||
{
|
||||
P_MsgKind kind;
|
||||
NET_Key src;
|
||||
NET_Key dst;
|
||||
|
||||
// P_UserSnapshot user_snapshot;
|
||||
@ -525,6 +530,7 @@ void P_DebugDrawPoint(Vec2 p, Vec4 srgb);
|
||||
void P_DebugDrawLine(Vec2 p0, Vec2 p1, Vec4 srgb);
|
||||
void P_DebugDrawRect(Rng2 rect, Vec4 srgb);
|
||||
void P_DebugDrawShape(P_Shape shape, Vec4 srgb);
|
||||
void P_DebugDrawFrame(P_Frame *frame, Vec4 tint);
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
//~ Msg
|
||||
|
||||
@ -157,56 +157,85 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
//////////////////////////////
|
||||
//- Pop messages
|
||||
|
||||
|
||||
u64 port = 22121;
|
||||
NET_Bind(net_pipe, port);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
P_MsgList in_msgs = Zi;
|
||||
{
|
||||
NET_MsgList net_msgs = NET_Swap(frame_arena, net_pipe);
|
||||
for (NET_Msg *net_msg = net_msgs.first; net_msg; net_msg = net_msg->next)
|
||||
{
|
||||
NET_Key net_key = net_msg->sender;
|
||||
|
||||
String address_str = NET_StringFromKey(frame_arena, net_key);
|
||||
LogDebugF("Received message from client \"%F\"", FmtString(address_str));
|
||||
|
||||
|
||||
|
||||
// Create client
|
||||
S_Client *client = S_ClientFromNetKey(net_key);
|
||||
if (S_IsClientNil(client))
|
||||
{
|
||||
u64 hash = NET_HashFromKey(net_key);
|
||||
S_ClientBin *bin = &S.client_bins[hash % S.client_bins_count];
|
||||
|
||||
// FIXME: Freelist client
|
||||
client = PushStruct(perm, S_Client);
|
||||
client->hash = hash;
|
||||
client->net_key = net_key;
|
||||
DllQueuePushNPZ(&S_NilClient, S.first_client, S.last_client, client, next, prev);
|
||||
DllQueuePushNP(bin->first, bin->last, client, next_in_bin, prev_in_bin);
|
||||
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// String test_address_str = NET_StringFromKey(frame_arena, net_key);
|
||||
// LogDebugF("Sending test msg to client \"%F\"", FmtString(test_address_str));
|
||||
// NET_Send(net_pipe, net_key, Lit("HIII"), NET_SendFlag_None);
|
||||
|
||||
String packed = net_msg->data;
|
||||
P_MsgList client_msgs = P_UnpackMessages(frame_arena, packed, net_key);
|
||||
|
||||
P_MsgList client_msgs = P_UnpackMessages(frame_arena, packed);
|
||||
// if (client_msgs.count > 0)
|
||||
// {
|
||||
// S_Client *client = S_ClientFromNetKey(net_key);
|
||||
|
||||
// FIXME: Set src user here based on net key (or maybe just attach net key to msg instead of user)
|
||||
// // Create client
|
||||
// if (S_IsClientNil(client))
|
||||
// {
|
||||
// P_Msg *msg = &client_msgs.first->msg;
|
||||
// if (msg->kind == P_MsgKind_Connect)
|
||||
// {
|
||||
// u64 hash = NET_HashFromKey(net_key);
|
||||
// S_ClientBin *bin = &S.client_bins[hash % S.client_bins_count];
|
||||
|
||||
// String src_name = msg->data;
|
||||
// src_name = TrimWhitespace(src_name);
|
||||
// if (src_name.len < P_MinPlayerNameLen)
|
||||
// {
|
||||
// src_name = Lit("Player");
|
||||
// }
|
||||
// src_name.len = MinI64(src_name.len, P_MaxPlayerNameLen);
|
||||
|
||||
// // FIXME: Freelist client
|
||||
// client = PushStruct(perm, S_Client);
|
||||
// client->hash = hash;
|
||||
// client->net_key = net_key;
|
||||
// client->name_len = MinI64(countof(client->name_text), src_name.len);
|
||||
// CopyBytes(client->name_text, src_name.text, client->name_len);
|
||||
|
||||
// DllQueuePushNPZ(&S_NilClient, S.first_client, S.last_client, client, next, prev);
|
||||
// DllQueuePushNP(bin->first, bin->last, client, next_in_bin, prev_in_bin);
|
||||
|
||||
// {
|
||||
// P_Msg *out_msg = P_PushMsg(P_MsgKind_Connect, Zstr);
|
||||
// out_msg->dst = net_key;
|
||||
// }
|
||||
|
||||
// {
|
||||
// String name = STRING(client->name_len, client->name_text);
|
||||
|
||||
// if (MatchString(name, src_name))
|
||||
// {
|
||||
// LogDebugF("Client \"%F\" connecting with name \"%F\"", FmtString(address_str), FmtString(src_name));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LogDebugF("Client \"%F\" connecting with name \"%F\" (reassigned to \"%F\")", FmtString(address_str), FmtString(src_name), FmtString(name));
|
||||
// }
|
||||
|
||||
// P_Msg *out_msg = P_PushMsg(
|
||||
// P_MsgKind_Chat,
|
||||
// StringF(
|
||||
// frame_arena,
|
||||
// "Player %F is connecting",
|
||||
// FmtString(name)
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (client_msgs.first)
|
||||
{
|
||||
@ -225,49 +254,66 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
//- Connect clients
|
||||
|
||||
for (P_MsgNode *msg_node = in_msgs.first; msg_node; msg_node = msg_node->next)
|
||||
{
|
||||
P_Msg *msg = &msg_node->msg;
|
||||
NET_Key net_key = msg->src;
|
||||
S_Client *client = S_ClientFromNetKey(net_key);
|
||||
|
||||
if (S_IsClientNil(client))
|
||||
{
|
||||
String address_str = NET_StringFromKey(frame_arena, net_key);
|
||||
u64 hash = NET_HashFromKey(net_key);
|
||||
S_ClientBin *bin = &S.client_bins[hash % S.client_bins_count];
|
||||
|
||||
String src_name = msg->data;
|
||||
src_name = TrimWhitespace(src_name);
|
||||
if (src_name.len < P_MinPlayerNameLen)
|
||||
{
|
||||
src_name = Lit("Player");
|
||||
}
|
||||
src_name.len = MinI64(src_name.len, P_MaxPlayerNameLen);
|
||||
|
||||
// FIXME: Freelist client
|
||||
client = PushStruct(perm, S_Client);
|
||||
client->hash = hash;
|
||||
client->net_key = net_key;
|
||||
client->name_len = MinI64(countof(client->name_text), src_name.len);
|
||||
CopyBytes(client->name_text, src_name.text, client->name_len);
|
||||
|
||||
DllQueuePushNPZ(&S_NilClient, S.first_client, S.last_client, client, next, prev);
|
||||
DllQueuePushNP(bin->first, bin->last, client, next_in_bin, prev_in_bin);
|
||||
|
||||
// Acknowledge player connection
|
||||
{
|
||||
P_Msg *out_msg = P_PushMsg(P_MsgKind_Connect, Zstr);
|
||||
out_msg->dst = net_key;
|
||||
}
|
||||
|
||||
|
||||
// P_MsgList in_msgs = Zi;
|
||||
// {
|
||||
// NET_MsgList net_msgs = NET_Swap(frame_arena, net_pipe);
|
||||
// for (NET_Msg *net_msg = net_msgs.first; net_msg; net_msg = net_msg->next)
|
||||
// {
|
||||
// NET_Key net_key = net_msg->sender;
|
||||
// String address_str = NET_StringFromKey(frame_arena, net_key);
|
||||
// LogDebugF("Received message from client \"%F\"", FmtString(address_str));
|
||||
|
||||
// NET_Push(net_pipe, net_key, Lit("HIII"), 0);
|
||||
|
||||
// String packed = net_msg->data;
|
||||
// P_MsgNode *dst_msg_node = PushStruct(frame_arena, P_MsgNode);
|
||||
// P_Msg *dst_msg = &dst_msg_node->msg;
|
||||
// *dst_msg = P_UnpackMsg(frame_arena, packed);
|
||||
// // FIXME: Set src user here based on net key
|
||||
// // dst_msg->src_user =
|
||||
// DllQueuePush(in_msgs.first, in_msgs.last, dst_msg_node);
|
||||
// ++in_msgs.count;
|
||||
|
||||
|
||||
|
||||
// // {
|
||||
// // LogDebugF(
|
||||
// // "Server received msg (%F bytes): \"%F\"",
|
||||
// // FmtUint(packed.len),
|
||||
// // FmtString(packed)
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Broadcast player connection notification
|
||||
{
|
||||
String name = STRING(client->name_len, client->name_text);
|
||||
if (MatchString(name, src_name))
|
||||
{
|
||||
LogDebugF("Client \"%F\" connecting with name \"%F\"", FmtString(address_str), FmtString(src_name));
|
||||
}
|
||||
else
|
||||
{
|
||||
LogDebugF("Client \"%F\" connecting with name \"%F\" (reassigned to \"%F\")", FmtString(address_str), FmtString(src_name), FmtString(name));
|
||||
}
|
||||
P_Msg *out_msg = P_PushMsg(
|
||||
P_MsgKind_Chat,
|
||||
StringF(
|
||||
frame_arena,
|
||||
"\"%F\" connected",
|
||||
FmtString(name)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
//- Process connection messages
|
||||
@ -292,8 +338,8 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
|
||||
// // FIXME: Set net key here
|
||||
|
||||
// i32 min_name_len = 3;
|
||||
// i32 max_name_len = countof(user->string_text) - 8;
|
||||
// i32 min_name_len = P_MinPlayerNameLen;
|
||||
// i32 max_name_len = P_MaxPlayerNameLen;
|
||||
|
||||
// // De-duplicate user name
|
||||
// String user_name = msg->data;
|
||||
@ -526,9 +572,10 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
//////////////////////////////
|
||||
//- Step frame
|
||||
|
||||
// {
|
||||
// P_StepFrame(world_frame, user_msgs);
|
||||
// }
|
||||
{
|
||||
P_StepFrame(world_frame);
|
||||
P_DebugDrawFrame(world_frame, VEC4(0.2, 0.4, 0.2, 0.75));
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
//- Push tile messages
|
||||
@ -560,7 +607,7 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
// Arena *msgs_arena = P_tl.out_msgs_arena;
|
||||
// P_Msg *msg = P_PushMsg(P_MsgKind_SimSnapshot, Zstr);
|
||||
// msg->dst_user = user->key;
|
||||
// msg->burst = 1;
|
||||
// NET_SendFlag_None = 1;
|
||||
// P_SimSnapshot *snapshot = &msg->sim_snapshot;
|
||||
// {
|
||||
// snapshot->world_seed = world->seed;
|
||||
@ -642,7 +689,12 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
if (msgs.count > 0)
|
||||
{
|
||||
String packed = P_PackMessages(&packer_bbw, msgs);
|
||||
NET_Push(net_pipe, client->net_key, packed, 0);
|
||||
|
||||
|
||||
String address_str = NET_StringFromKey(frame_arena, client->net_key);
|
||||
LogDebugF("Sending message to client \"%F\"", FmtString(address_str));
|
||||
|
||||
NET_Send(net_pipe, client->net_key, packed, NET_SendFlag_None);
|
||||
ZeroStruct(&client->out_msgs);
|
||||
}
|
||||
}
|
||||
@ -699,7 +751,7 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
// {
|
||||
// if (user->is_user)
|
||||
// {
|
||||
// NET_Push(net_pipe, user->net, packed, msg->burst);
|
||||
// NET_Send(net_pipe, user->net, packed, NET_SendFlag_None);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@ -709,7 +761,7 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
// if (!NET_IsKeyNil(user->net))
|
||||
// {
|
||||
// String packed = P_PackMessage(frame_arena, msg);
|
||||
// NET_Push(net_pipe, user->net, packed, msg->burst);
|
||||
// NET_Send(net_pipe, user->net, packed, NET_SendFlag_None);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@ -732,7 +784,7 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
// {
|
||||
// if (user->is_user)
|
||||
// {
|
||||
// NET_Push(net_pipe, user->net, packed, msg->burst);
|
||||
// NET_Send(net_pipe, user->net, packed, NET_SendFlag_None);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@ -742,7 +794,7 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
// if (!NET_IsKeyNil(user->net))
|
||||
// {
|
||||
// String packed = P_PackMessage(frame_arena, msg);
|
||||
// NET_Push(net_pipe, user->net, packed, msg->burst);
|
||||
// NET_Send(net_pipe, user->net, packed, NET_SendFlag_None);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@ -862,9 +914,28 @@ void S_TickForever(WaveLaneCtx *lane)
|
||||
P.s2v.arena = old_arena;
|
||||
P.s2v.gen = old_gen;
|
||||
}
|
||||
ZeroStruct(P.s2v.arena);
|
||||
ResetArena(P.s2v.arena);
|
||||
P.s2v.gen += 1;
|
||||
P.s2v.pipe_stats = NET_StatsFromPipe(net_pipe);
|
||||
|
||||
{
|
||||
i64 dst_idx = 0;
|
||||
P.s2v.first_debug_draw_node = 0;
|
||||
P.s2v.last_debug_draw_node = 0;
|
||||
P.s2v.debug_draw_nodes_count = P_tl.debug_draw_nodes_count;
|
||||
P_DebugDrawNode *dst_nodes = PushStructsNoZero(P.s2v.arena, P_DebugDrawNode, P.s2v.debug_draw_nodes_count);
|
||||
for (P_DebugDrawNode *src = P_tl.first_debug_draw_node; src; src = src->next)
|
||||
{
|
||||
P_DebugDrawNode *dst = &dst_nodes[dst_idx];
|
||||
*dst = *src;
|
||||
dst_idx += 1;
|
||||
SllQueuePush(P.s2v.first_debug_draw_node, P.s2v.last_debug_draw_node, dst);
|
||||
}
|
||||
ResetArena(P_tl.debug_arena);
|
||||
P_tl.first_debug_draw_node = 0;
|
||||
P_tl.last_debug_draw_node = 0;
|
||||
P_tl.debug_draw_nodes_count = 0;
|
||||
}
|
||||
}
|
||||
UnlockTicketMutex(&P.s2v_tm);
|
||||
}
|
||||
|
||||
@ -12,6 +12,9 @@ Struct(S_Client)
|
||||
NET_Key net_key;
|
||||
P_MsgList out_msgs;
|
||||
|
||||
i32 name_len;
|
||||
u8 name_text[P_MaxPlayerNameLen];
|
||||
|
||||
u64 remote_tiles_hash;
|
||||
};
|
||||
|
||||
|
||||
@ -217,7 +217,7 @@ String P_PackMessages(BB_Writer *bbw, P_MsgList msgs)
|
||||
return result;
|
||||
}
|
||||
|
||||
P_MsgList P_UnpackMessages(Arena *arena, String packed)
|
||||
P_MsgList P_UnpackMessages(Arena *arena, String packed, NET_Key sender)
|
||||
{
|
||||
P_MsgList result = Zi;
|
||||
BB_Buff bb = BB_BuffFromString(packed);
|
||||
@ -235,6 +235,8 @@ P_MsgList P_UnpackMessages(Arena *arena, String packed)
|
||||
result.count += 1;
|
||||
// Read msg data string
|
||||
String data_str = BB_ReadString(arena, &bbr);
|
||||
dst_msg_node->msg.src = sender;
|
||||
dst_msg_node->msg.dst = NET_NilKey;
|
||||
dst_msg_node->msg.data = data_str;
|
||||
}
|
||||
else
|
||||
|
||||
@ -33,4 +33,4 @@ P_UnpackedWorld P_UnpackWorld(Arena *arena, String packed);
|
||||
//~ Message
|
||||
|
||||
String P_PackMessages(BB_Writer *bbw, P_MsgList msgs);
|
||||
P_MsgList P_UnpackMessages(Arena *arena, String packed);
|
||||
P_MsgList P_UnpackMessages(Arena *arena, String packed, NET_Key sender);
|
||||
|
||||
@ -543,6 +543,8 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
frame->equipped_tile = prev_frame->equipped_tile;
|
||||
frame->edit_camera_pos = prev_frame->edit_camera_pos;
|
||||
frame->edit_camera_zoom = prev_frame->edit_camera_zoom;
|
||||
frame->sim_key = prev_frame->sim_key;
|
||||
frame->desired_sim_key = prev_frame->desired_sim_key;
|
||||
|
||||
frame->tick = V.current_frame_tick;
|
||||
frame->time_ns = TimeNs();
|
||||
@ -659,6 +661,23 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
{
|
||||
s2v_gen = P.s2v.gen;
|
||||
sim_pipe_stats = P.s2v.pipe_stats;
|
||||
|
||||
{
|
||||
ResetArena(sim_debug_arena);
|
||||
first_sim_debug_draw_node = 0;
|
||||
last_sim_debug_draw_node = 0;
|
||||
{
|
||||
i64 dst_idx = 0;
|
||||
P_DebugDrawNode *dst_nodes = PushStructsNoZero(sim_debug_arena, P_DebugDrawNode, P.s2v.debug_draw_nodes_count);
|
||||
for (P_DebugDrawNode *src = P.s2v.first_debug_draw_node; src; src = src->next)
|
||||
{
|
||||
P_DebugDrawNode *dst = &dst_nodes[dst_idx];
|
||||
*dst = *src;
|
||||
dst_idx += 1;
|
||||
SllQueuePush(first_sim_debug_draw_node, last_sim_debug_draw_node, dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UnlockTicketMutex(&P.s2v_tm);
|
||||
@ -2657,6 +2676,47 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
//- Try to connect to server
|
||||
|
||||
frame->desired_sim_key = NET_KeyFromString(Lit("127.0.0.1"), Lit("22121"));
|
||||
if (!NET_MatchKey(frame->sim_key, frame->desired_sim_key))
|
||||
{
|
||||
i64 now_ns = TimeNs();
|
||||
// i64 retry_rate_ns = NsFromSeconds(0.100);
|
||||
i64 retry_rate_ns = NsFromSeconds(0);
|
||||
|
||||
b32 should_try = 0;
|
||||
if (!NET_MatchKey(prev_frame->desired_sim_key, frame->desired_sim_key))
|
||||
{
|
||||
should_try = 1;
|
||||
}
|
||||
if (V.connect_try_ns == 0 || now_ns - V.connect_try_ns > retry_rate_ns)
|
||||
{
|
||||
should_try = 1;
|
||||
}
|
||||
|
||||
// if (should_try && now_ns > NsFromSeconds(1.5))
|
||||
if (should_try)
|
||||
{
|
||||
P_MsgNode tmp_msg_node = Zi;
|
||||
tmp_msg_node.msg.kind = P_MsgKind_Connect;
|
||||
tmp_msg_node.msg.data = Lit("Guy");
|
||||
|
||||
P_MsgList tmp_msglist = Zi;
|
||||
tmp_msglist.count = 1;
|
||||
tmp_msglist.first = &tmp_msg_node;
|
||||
tmp_msglist.last = &tmp_msg_node;
|
||||
|
||||
BB_ResetWriter(&packer_bbw);
|
||||
String packed = P_PackMessages(&packer_bbw, tmp_msglist);
|
||||
|
||||
// FIXME: Real connection packet
|
||||
NET_Send(net_pipe, frame->desired_sim_key, packed, NET_SendFlag_None);
|
||||
V.connect_try_ns = now_ns;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
//- Pop messages from sim
|
||||
|
||||
@ -2667,21 +2727,26 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
// FIXME: Reject messages if not from currently connected sim
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
P_MsgList in_msgs = Zi;
|
||||
{
|
||||
NET_MsgList net_msgs = NET_Swap(frame->arena, net_pipe);
|
||||
for (NET_Msg *net_msg = net_msgs.first; net_msg; net_msg = net_msg->next)
|
||||
{
|
||||
NET_Key net_key = net_msg->sender;
|
||||
|
||||
String address_str = NET_StringFromKey(frame->arena, net_key);
|
||||
LogDebugF("Received message from server \"%F\"", FmtString(address_str));
|
||||
|
||||
if (NET_MatchKey(net_key, frame->desired_sim_key))
|
||||
{
|
||||
frame->sim_key = net_key;
|
||||
|
||||
// String address_str = NET_StringFromKey(frame->arena, net_key);
|
||||
// LogDebugF("Received message from server \"%F\"", FmtString(address_str));
|
||||
|
||||
String packed = net_msg->data;
|
||||
|
||||
P_MsgList server_msgs = P_UnpackMessages(frame->arena, packed);
|
||||
P_MsgList server_msgs = P_UnpackMessages(frame->arena, packed, net_key);
|
||||
if (server_msgs.first)
|
||||
{
|
||||
if (in_msgs.last)
|
||||
@ -2697,6 +2762,8 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
in_msgs.count += server_msgs.count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2979,6 +3046,7 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
|
||||
P_ClearFrames(predict_world, I64Min, I64Max);
|
||||
predict_frame = P_PushFrame(predict_world, sim_world->last_frame, sim_world->last_frame->tick);
|
||||
P_DebugDrawFrame(predict_frame, VEC4(0, 0, 1, 0.75));
|
||||
|
||||
|
||||
|
||||
@ -3458,66 +3526,66 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
//////////////////////////////
|
||||
//- Debug draw
|
||||
|
||||
// {
|
||||
// // Merge vis debug draws with sim debug draws
|
||||
// P_DebugDrawNode *first_debug_draw_node = first_sim_debug_draw_node;
|
||||
// P_DebugDrawNode *last_debug_draw_node = last_sim_debug_draw_node;
|
||||
// if (P_tl.first_debug_draw_node)
|
||||
// {
|
||||
// if (last_debug_draw_node)
|
||||
// {
|
||||
// last_debug_draw_node->next = P_tl.first_debug_draw_node;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// first_debug_draw_node = P_tl.first_debug_draw_node;
|
||||
// }
|
||||
// last_debug_draw_node = P_tl.last_debug_draw_node;
|
||||
// }
|
||||
{
|
||||
// Merge vis debug draws with sim debug draws
|
||||
P_DebugDrawNode *first_debug_draw_node = first_sim_debug_draw_node;
|
||||
P_DebugDrawNode *last_debug_draw_node = last_sim_debug_draw_node;
|
||||
if (P_tl.first_debug_draw_node)
|
||||
{
|
||||
if (last_debug_draw_node)
|
||||
{
|
||||
last_debug_draw_node->next = P_tl.first_debug_draw_node;
|
||||
}
|
||||
else
|
||||
{
|
||||
first_debug_draw_node = P_tl.first_debug_draw_node;
|
||||
}
|
||||
last_debug_draw_node = P_tl.last_debug_draw_node;
|
||||
}
|
||||
|
||||
// // Push draws
|
||||
// for (P_DebugDrawNode *n = first_debug_draw_node; n; n = n->next)
|
||||
// {
|
||||
// Vec4 color = Vec4FromU32(n->srgb32);
|
||||
// i32 detail = 24;
|
||||
// f32 radius = 5;
|
||||
// switch(n->kind)
|
||||
// {
|
||||
// case P_DebugDrawKind_Point:
|
||||
// {
|
||||
// Vec2 ui_p = MulXformV2(frame->xf.world_to_ui, n->point.p);
|
||||
// V_DrawPoint(ui_p, color);
|
||||
// } break;
|
||||
// Push draws
|
||||
for (P_DebugDrawNode *n = first_debug_draw_node; n; n = n->next)
|
||||
{
|
||||
Vec4 color = Vec4FromU32(n->srgb32);
|
||||
i32 detail = 24;
|
||||
f32 radius = 5;
|
||||
switch(n->kind)
|
||||
{
|
||||
case P_DebugDrawKind_Point:
|
||||
{
|
||||
Vec2 ui_p = MulXformV2(frame->xf.world_to_ui, n->point.p);
|
||||
V_DrawPoint(ui_p, color);
|
||||
} break;
|
||||
|
||||
// case P_DebugDrawKind_Line:
|
||||
// {
|
||||
// Vec2 ui_p0 = MulXformV2(frame->xf.world_to_ui, n->line.p0);
|
||||
// Vec2 ui_p1 = MulXformV2(frame->xf.world_to_ui, n->line.p1);
|
||||
// V_DrawLine(ui_p0, ui_p1, color);
|
||||
// } break;
|
||||
case P_DebugDrawKind_Line:
|
||||
{
|
||||
Vec2 ui_p0 = MulXformV2(frame->xf.world_to_ui, n->line.p0);
|
||||
Vec2 ui_p1 = MulXformV2(frame->xf.world_to_ui, n->line.p1);
|
||||
V_DrawLine(ui_p0, ui_p1, color);
|
||||
} break;
|
||||
|
||||
// case P_DebugDrawKind_Rect:
|
||||
// {
|
||||
// Rng2 ui_rect = Zi;
|
||||
// ui_rect.p0 = MulXformV2(frame->xf.world_to_ui, n->rect.p0);
|
||||
// ui_rect.p1 = MulXformV2(frame->xf.world_to_ui, n->rect.p1);
|
||||
// V_DrawRect(ui_rect, color, V_DrawFlag_Line);
|
||||
// } break;
|
||||
case P_DebugDrawKind_Rect:
|
||||
{
|
||||
Rng2 ui_rect = Zi;
|
||||
ui_rect.p0 = MulXformV2(frame->xf.world_to_ui, n->rect.p0);
|
||||
ui_rect.p1 = MulXformV2(frame->xf.world_to_ui, n->rect.p1);
|
||||
V_DrawRect(ui_rect, color, V_DrawFlag_Line);
|
||||
} break;
|
||||
|
||||
// case P_DebugDrawKind_Shape:
|
||||
// {
|
||||
// P_Shape ui_shape = P_MulXformShape(frame->xf.world_to_ui, n->shape);
|
||||
// V_DrawShape(ui_shape, color, detail, V_DrawFlag_Line);
|
||||
// } break;
|
||||
// }
|
||||
// }
|
||||
case P_DebugDrawKind_Shape:
|
||||
{
|
||||
P_Shape ui_shape = P_MulXformShape(frame->xf.world_to_ui, n->shape);
|
||||
V_DrawShape(ui_shape, color, detail, V_DrawFlag_Line);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// // Reset vis debug draws
|
||||
// ResetArena(P_tl.debug_arena);
|
||||
// P_tl.first_debug_draw_node = 0;
|
||||
// P_tl.last_debug_draw_node = 0;
|
||||
// P_tl.debug_draw_nodes_count = 0;
|
||||
// }
|
||||
// Reset vis debug draws
|
||||
ResetArena(P_tl.debug_arena);
|
||||
P_tl.first_debug_draw_node = 0;
|
||||
P_tl.last_debug_draw_node = 0;
|
||||
P_tl.debug_draw_nodes_count = 0;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////
|
||||
@ -3843,13 +3911,11 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
|
||||
// FIXME: Remove this (testing)
|
||||
|
||||
NET_Key server_key = NET_KeyFromString(Lit("127.0.0.1"), Lit("22121"));
|
||||
|
||||
if (frame->held_buttons[Button_R] && !prev_frame->held_buttons[Button_R])
|
||||
{
|
||||
LogDebugF("Sending test payload");
|
||||
// NET_Push(net_pipe, server_key, STRING(P_TilesCount, predict_world->tiles), 0);
|
||||
NET_Push(net_pipe, server_key, Lit("Hello there!"), 0);
|
||||
NET_Send(net_pipe, frame->sim_key, STRING(P_TilesCount, predict_world->tiles), NET_SendFlag_None);
|
||||
// NET_Send(net_pipe, frame->sim_key, Lit("Hello there!"), NET_SendFlag_None);
|
||||
}
|
||||
|
||||
|
||||
@ -3859,7 +3925,7 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
// {
|
||||
// P_Msg *msg = &msg_node->msg;
|
||||
// String packed = P_PackMessage(frame->arena, msg);
|
||||
// NET_Push(net_pipe, server_key, packed, msg->burst);
|
||||
// NET_Send(net_pipe, frame->sim_key, packed, NET_SendFlag_None);
|
||||
// }
|
||||
|
||||
{
|
||||
@ -3867,7 +3933,7 @@ void V_TickForever(WaveLaneCtx *lane)
|
||||
{
|
||||
BB_ResetWriter(&packer_bbw);
|
||||
String packed = P_PackMessages(&packer_bbw, P_tl.out_msgs);
|
||||
NET_Push(net_pipe, server_key, packed, 0);
|
||||
NET_Send(net_pipe, frame->sim_key, packed, NET_SendFlag_None);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,6 +235,9 @@ Struct(V_Frame)
|
||||
f64 dt;
|
||||
RandState rand;
|
||||
|
||||
NET_Key sim_key;
|
||||
NET_Key desired_sim_key;
|
||||
|
||||
Button held_buttons[Button_COUNT];
|
||||
V_Palette palette;
|
||||
|
||||
@ -305,6 +308,8 @@ Struct(V_Ctx)
|
||||
V_Panel *root_panel;
|
||||
V_Window *dragging_window;
|
||||
|
||||
i64 connect_try_ns;
|
||||
|
||||
// Notifications
|
||||
V_Notif *first_notif;
|
||||
V_Notif *last_notif;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user