Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
673e180
Fixed a edge case crash when multiple threads accessed a user session…
x64-dev Jan 27, 2026
9a7e13d
- Removed duplicate maps from QM map list
x64-dev Jan 31, 2026
07a0f75
- Moved monitor settings to appsettings.json
x64-dev Jan 31, 2026
76d753b
- Removed a duplicate map from QM playlist
x64-dev Jan 31, 2026
dc2ceaf
Discord settings are now in appsettings.json
x64-dev Feb 1, 2026
1a9e95c
- Begin support for middleware auth
x64-dev Feb 3, 2026
86bb315
- Store the MWID of the owning session on the lobby member
x64-dev Feb 3, 2026
bece958
- Move to a delayed tick model for room member list
x64-dev Feb 6, 2026
e235c96
- Pass on safety, optimization and error handling
x64-dev Feb 13, 2026
fd7e733
Performance optimizations and IPv4/IPv6 normalization
x64-dev Feb 13, 2026
a54aa71
Fix bug where TimeMemberLeft was using the slot index and userID as i…
x64-dev Feb 14, 2026
ecb7ca5
Further improvement to win detection
x64-dev Feb 14, 2026
738da65
- Increased friends limit to 200 from 100
x64-dev Feb 14, 2026
65c2521
- Stability improvements
x64-dev Mar 3, 2026
1e4e98e
Fix a Discord unhandled exception
x64-dev Mar 3, 2026
4bdcbda
NETWORK_SIGNAL now validates same lobby
x64-dev Mar 3, 2026
fd14b27
- Improved timer exception handling
x64-dev Mar 3, 2026
366caa6
- Extra guards around lobby join
x64-dev Mar 3, 2026
03f516f
- Cleanup of lobby update permissions system
x64-dev Mar 3, 2026
ec9c74d
- Clean up exception handler for ws timeout
x64-dev Mar 3, 2026
142fa0e
- WS exception: capture event to sentry instead of immediate flush
x64-dev Mar 3, 2026
ff6fc7f
- Tweaks to external HTTP requests + sql semaphore
x64-dev Mar 3, 2026
fd47965
Use ConcurrentDictionary for RegisterInitialPlayerExeCRC
x64-dev Mar 3, 2026
73d798a
Use ConcurrentDictionary for RegisterInitialPlayerExeCRC
x64-dev Mar 3, 2026
2ceb17d
- Improved locking and multi-thread mysql impl
x64-dev Mar 4, 2026
af943e8
- Added "send_room_chat_to_discord" flag to Discord settings
x64-dev Mar 4, 2026
cf2f1c8
- Added a 20ms timeout to websocket drain
x64-dev Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions GenOnlineService/BackgroundS3Uploader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ static class BackgroundS3Uploader
private static Int64 g_LastUpload = -1;

private static Thread g_BackgroundThread = null;
private static bool g_bShutdownRequested = false;
private static volatile bool g_bShutdownRequested = false;

public static void Initialize()
{
Expand All @@ -76,7 +76,7 @@ public static void TickThreaded() // This is called on a thread, and uploads one
// queue the next thing
if (m_queueUploads.TryDequeue(out S3QueuedUploadEntry entry))
{
DoUpload(entry);
DoUpload(entry).GetAwaiter().GetResult();
g_LastUpload = Environment.TickCount64;
}
}
Expand Down
158 changes: 91 additions & 67 deletions GenOnlineService/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ public static class Constants

public const UInt16 g_DefaultCameraMaxHeight = 310;
}
public class RoomMember
{
public RoomMember(Int64 a_UserID, string strName, bool admin)
{
UserID = a_UserID;
Name = strName;
IsAdmin = admin;
}

public Int64 UserID { get; set; } = -1;
public String Name { get; set; } = String.Empty;
public bool IsAdmin { get; set; } = false;
}

public enum EPendingLoginState
{
Expand Down Expand Up @@ -225,12 +238,15 @@ public static async Task<UserWebSocketInstance> CreateSession(bool bIsReconnect,
return newSess;
}

public static async void Tick()
public static async Task Tick()
{
foreach (var kvPair in m_dictUserSessions)
{
kvPair.Value.TickWebsocket();
}
// Give the entire tick a 20 ms deadline. All users drain concurrently via
// Task.WhenAll, so a slow/stuck client cannot delay others. If the deadline
// fires, the CancellationToken propagates into each in-flight SendAsync and
// into the dequeue loop guard, so the stuck user is skipped and their unsent
// messages stay in the queue for the next tick.
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(20));
await Task.WhenAll(m_dictUserSessions.Values.Select(sess => sess.TickWebsocket(cts.Token)));
}

public static async Task CheckForTimeouts()
Expand Down Expand Up @@ -444,59 +460,60 @@ public static async Task SendNewOrDeletedLobbyToAllNetworkRoomMembers(int networ
}
}

public static async Task SendRoomMemberListToAllInRoom(int roomID)
private static ConcurrentList<int> g_lstDirtyNetworkRooms = new();
public static async Task TickRoomMemberList()
{
// need a member list update
WebSocketMessage_NetworkRoomMemberListUpdate memberListUpdate = new WebSocketMessage_NetworkRoomMemberListUpdate();
memberListUpdate.msg_id = (int)EWebSocketMessageID.NETWORK_ROOM_MEMBER_LIST_UPDATE;
memberListUpdate.names = new List<string>();
memberListUpdate.ids = new List<Int64>();
foreach (int roomID in g_lstDirtyNetworkRooms)
{

// need a member list update
WebSocketMessage_NetworkRoomMemberListUpdate memberListUpdate = new WebSocketMessage_NetworkRoomMemberListUpdate();
memberListUpdate.msg_id = (int)EWebSocketMessageID.NETWORK_ROOM_MEMBER_LIST_UPDATE;
memberListUpdate.members = new();

SortedDictionary<Int64, bool> usersAlreadyProcessed = new();
SortedDictionary<Int64, bool> usersAlreadyProcessed = new();

List<UserSession> lstUsersToSend = new();
List<UserSession> lstUsersToSend = new();

// populate list of everyone in the room
foreach (KeyValuePair<Int64, UserSession> sessionData in m_dictUserSessions)
{
UserSession sess = sessionData.Value;
if (sess.networkRoomID == roomID)
// populate list of everyone in the room
foreach (KeyValuePair<Int64, UserSession> sessionData in m_dictUserSessions)
{
if (!usersAlreadyProcessed.ContainsKey(sess.m_UserID))
UserSession sess = sessionData.Value;
if (sess.networkRoomID == roomID)
{
usersAlreadyProcessed[sess.m_UserID] = true;

// add to member list

// flag staff accounts
if (sess.IsAdmin())
{
memberListUpdate.names.Add(String.Format("[\u2605\u2605GO STAFF\u2605\u2605] {0}", sess.m_strDisplayName));
}
else
if (!usersAlreadyProcessed.ContainsKey(sess.m_UserID))
{
memberListUpdate.names.Add(sess.m_strDisplayName);
}
usersAlreadyProcessed[sess.m_UserID] = true;

memberListUpdate.ids.Add(sess.m_UserID);
// add to member list
string strDisplayName = sess.IsAdmin() ? String.Format("[\u2605\u2605GO STAFF\u2605\u2605] {0}", sess.m_strDisplayName) : sess.m_strDisplayName;
memberListUpdate.members.Add(new RoomMember(sess.m_UserID, strDisplayName, sess.IsAdmin()));

// also add to list of users who need this update, since they were in there
UserSession? targetWS = WebSocketManager.GetDataFromUser(sess.m_UserID);
if (targetWS != null)
{
lstUsersToSend.Add(targetWS);
// also add to list of users who need this update, since they were in there
UserSession? targetWS = WebSocketManager.GetDataFromUser(sess.m_UserID);
if (targetWS != null)
{
lstUsersToSend.Add(targetWS);
}
}
}
}
}

byte[] bytesJSON = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(memberListUpdate));
byte[] bytesJSON = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(memberListUpdate));

// now send to everyone in the room
foreach (UserSession sess in lstUsersToSend)
{
sess.QueueWebsocketSend(bytesJSON);
// now send to everyone in the room
foreach (UserSession sess in lstUsersToSend)
{
sess.QueueWebsocketSend(bytesJSON);
}
}

g_lstDirtyNetworkRooms.Clear();
}

public static async Task MarkRoomMemberListAsDirty(int roomID)
{
g_lstDirtyNetworkRooms.Add(roomID);
}
}

Expand Down Expand Up @@ -525,13 +542,25 @@ public class UserSession

private Int64 m_timeAbandoned = -1;

private string m_strMiddlewareUserID = String.Empty;

public string m_client_id = String.Empty;
DateTime m_CreateTime = DateTime.Now;
public DateTime GetCreationTime()
{
return m_CreateTime;
}

public void SetMiddlewareID(string strMiddlewareUserID)
{
m_strMiddlewareUserID = strMiddlewareUserID;
}

public string GetMiddlewareID()
{
return m_strMiddlewareUserID;
}

public UInt64 GetLatestMatchID()
{
UInt64 mostRecentMatchID = 0;
Expand Down Expand Up @@ -564,7 +593,7 @@ public UserSession(Int64 ownerID, UserSocialContainer socialContainer, string cl
if (Helpers.g_dictInitialExeCRCs.ContainsKey(ownerID))
{
ACExeCRC = Helpers.g_dictInitialExeCRCs[ownerID].ToUpper();
Helpers.g_dictInitialExeCRCs.Remove(ownerID);
Helpers.g_dictInitialExeCRCs.Remove(ownerID, out string removedCRC);
}

m_socialContainer = socialContainer;
Expand Down Expand Up @@ -593,16 +622,9 @@ public void QueueWebsocketSend(byte[] bytesJSON)
return;
}

// If we have a websocket active, just send immediately, otherwise, queue it
UserWebSocketInstance websocketForUser = WebSocketManager.GetWebSocketForSession(this);
if (websocketForUser != null)
{
websocketForUser.SendAsync(bytesJSON, WebSocketMessageType.Text);
}
else
{
m_lstPendingWebsocketSends.Enqueue(bytesJSON);
}
// Always enqueue; the TickWebsocket drain loop is the sole sender,
// ensuring WebSocket.SendAsync is never called concurrently.
m_lstPendingWebsocketSends.Enqueue(bytesJSON);
}

public async Task<UserWebSocketInstance> CloseWebsocket(WebSocketCloseStatus reason, string strReason)
Expand All @@ -616,7 +638,7 @@ public async Task<UserWebSocketInstance> CloseWebsocket(WebSocketCloseStatus rea
return websocketForUser;
}

public async void TickWebsocket()
public async Task TickWebsocket(CancellationToken tickToken = default)
{
// Do we have a connection to send on?
UserWebSocketInstance websocketForUser = WebSocketManager.GetWebSocketForSession(this);
Expand All @@ -625,16 +647,16 @@ public async void TickWebsocket()
const int maxMessagesSendPerFrame = 50;
int messagesSent = 0;
// start dequeing and sending
while (messagesSent < maxMessagesSendPerFrame && m_lstPendingWebsocketSends.TryDequeue(out byte[] packetData))
while (!tickToken.IsCancellationRequested && messagesSent < maxMessagesSendPerFrame && m_lstPendingWebsocketSends.TryDequeue(out byte[] packetData))
{
websocketForUser.SendAsync(packetData, WebSocketMessageType.Text);
await websocketForUser.SendAsync(packetData, WebSocketMessageType.Text, tickToken);
++messagesSent;
}
}
}

// TODO_CACHE: Size limit this?
Queue<byte[]> m_lstPendingWebsocketSends = new Queue<byte[]>();
ConcurrentQueue<byte[]> m_lstPendingWebsocketSends = new ConcurrentQueue<byte[]>();

public void NotifyFriendslistDirty()
{
Expand Down Expand Up @@ -722,21 +744,21 @@ public bool WasPlayerInMatch(UInt64 matchID, out int slotIndexInLobby, out int a
return bWasInMatch;
}

public async void UpdateSessionNetworkRoom(Int16 newRoomID)
public async Task UpdateSessionNetworkRoom(Int16 newRoomID)
{
Int16 oldRoom = networkRoomID;
networkRoomID = newRoomID;

// update the room roster they left
if (oldRoom >= 0) // only if they werent in the dummy room before
{
await WebSocketManager.SendRoomMemberListToAllInRoom(oldRoom);
await WebSocketManager.MarkRoomMemberListAsDirty(oldRoom);
}

// send update to joiner + everyone in new room already
if (newRoomID >= 0) // only if they actually joined a room and weren't going to the dummy room
{
await WebSocketManager.SendRoomMemberListToAllInRoom(newRoomID);
await WebSocketManager.MarkRoomMemberListAsDirty(newRoomID);
}

// make the client force refresh list too
Expand Down Expand Up @@ -827,7 +849,7 @@ public Int64 GetTimeSinceLastPing()
return Environment.TickCount64 - m_lastPingTime;
}

public async Task SendAsync(byte[] buffer, WebSocketMessageType messageType)
public async Task SendAsync(byte[] buffer, WebSocketMessageType messageType, CancellationToken externalToken = default)
{
if (m_SockInternal != null)
{
Expand Down Expand Up @@ -863,7 +885,8 @@ public async Task SendAsync(byte[] buffer, WebSocketMessageType messageType)
}
*/

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var cts = CancellationTokenSource.CreateLinkedTokenSource(externalToken);
cts.CancelAfter(TimeSpan.FromMilliseconds(500));
await m_SockInternal.SendAsync(buffer, messageType, true, cts.Token);
}
catch
Expand Down Expand Up @@ -1641,7 +1664,7 @@ private static void GetTURNConfig(out int TTL, out string token, out string key,
// we should only have 1 turn credential at a time... clean it up
if (g_DictTURNUsernames.ContainsKey(userID))
{
DeleteCredentialsForUser(userID);
await DeleteCredentialsForUser(userID);
}

// create new credential
Expand Down Expand Up @@ -1688,6 +1711,7 @@ private static void GetTURNConfig(out int TTL, out string token, out string key,
}
}))
{
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Add("Authorization", String.Format("Bearer {0}", TurnToken));
client.DefaultRequestHeaders.Add("Accept", "application/json");
//client.DefaultRequestHeaders.Add("Content-Type", "application/json");
Expand Down Expand Up @@ -1737,7 +1761,7 @@ private static void GetTURNConfig(out int TTL, out string token, out string key,
return null;
}

public static async void DeleteCredentialsForUser(Int64 userID)
public static async Task DeleteCredentialsForUser(Int64 userID)
{
#if DEBUG
await Task.Delay(1);
Expand Down Expand Up @@ -1799,6 +1823,7 @@ public static async void DeleteCredentialsForUser(Int64 userID)
}
}))
{
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Add("Authorization", String.Format("Bearer {0}", TurnToken));
client.DefaultRequestHeaders.Add("Accept", "application/json");
try
Expand Down Expand Up @@ -2059,8 +2084,7 @@ public class WebSocketMessage_RelayUpgradeInbound : WebSocketMessage

public class WebSocketMessage_NetworkRoomMemberListUpdate : WebSocketMessage
{
public List<string>? names { get; set; }
public List<Int64>? ids { get; set; }
public List<RoomMember> members { get; set; } = new();
}

public class WebSocketMessage_CurrentLobbyUpdate : WebSocketMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public async Task<APIResult> Post()
//bSecureWS = false;
}

POST_CheckLogin_Result result = (POST_CheckLogin_Result)await Post_InternalHandler(jsonData, HttpContext.Connection.RemoteIpAddress?.ToString(), bSecureWS);
POST_CheckLogin_Result result = (POST_CheckLogin_Result)await Post_InternalHandler(jsonData, IPHelpers.NormalizeIP(HttpContext.Connection.RemoteIpAddress?.ToString()), bSecureWS);
return result;
}
}
Expand Down Expand Up @@ -216,7 +216,7 @@ public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr
result.ws_uri = null;
}

Database.Functions.Auth.CleanupPendingLogin(GlobalDatabaseInstance.g_Database, gameCode);
await Database.Functions.Auth.CleanupPendingLogin(GlobalDatabaseInstance.g_Database, gameCode);

return result;
}
Expand All @@ -231,7 +231,7 @@ public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr
{
result.result = EPendingLoginState.LoginFailed;
Response.StatusCode = (int)HttpStatusCode.Forbidden;
Database.Functions.Auth.CleanupPendingLogin(GlobalDatabaseInstance.g_Database, gameCode);
await Database.Functions.Auth.CleanupPendingLogin(GlobalDatabaseInstance.g_Database, gameCode);
}
#if !DEBUG
}
Expand Down
2 changes: 1 addition & 1 deletion GenOnlineService/Controllers/Friends/SocialController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ public async Task AddFriend(Int64 target_user_id)
}

// too many friends?
const int friendsLimit = 100;
const int friendsLimit = 200;
UserSession? userData = WebSocketManager.GetDataFromUser(requester_user_id);
if (userData.GetSocialContainer().Friends.Count >= friendsLimit)
{
Expand Down
Loading