Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 42 additions & 14 deletions GenOnlineService/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

using Amazon.S3.Model;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.EntityFrameworkCore;
using Org.BouncyCastle.Tls;
using System;
using System.Collections.Concurrent;
Expand Down Expand Up @@ -220,6 +221,13 @@ public static async Task<UserWebSocketInstance> CreateSession(AppDbContext _db,
// how many other sessions do they have online?
bool bIsFirstSessionForUser = WebSocketManager.GetAllDataFromUser(ownerID).Count == 0;

// kill any existing sessions for this user of same session type
if (m_dictWebsockets[sessionType].TryGetValue(ownerID, out UserWebSocketInstance? existingSession))
{
Console.WriteLine("Killing existing session for {0} ({1})", ownerID, strDisplayName);
await DeleteSession(ownerID, sessionType, existingSession, !bIsReconnect);
}

// get and cache social container
UserSocialContainer socialContainer = new();
socialContainer.Friends = await Database.Social.GetFriends(_db, ownerID);
Expand Down Expand Up @@ -262,27 +270,22 @@ public static async Task<UserWebSocketInstance> CreateSession(AppDbContext _db,
}
}

// kill any existing sessions for this user of same session type
if (m_dictWebsockets[sessionType].TryGetValue(ownerID, out UserWebSocketInstance? existingSession))
{
Console.WriteLine("Killing existing session for {0} ({1})", ownerID, strDisplayName);
await DeleteSession(ownerID, sessionType, existingSession, !bIsReconnect);
}

// now create a websocket, we always do this whether its reconnect or not, only data is persistent
UserWebSocketInstance newSess = new UserWebSocketInstance(sessionType, ownerID);
m_dictWebsockets[sessionType][ownerID] = newSess;

// update last login and last ip
await Database.Users.UpdateLastLoginData(_db, ownerID, ipAddr);

int numSessions = m_dictWebsockets.Count;
// TODO_EFCORE: Optimize this, dont iterate all the time
int numSessions = WebSocketManager.GetNumberOfUsersOnline();

if (numSessions > g_PeakConnectionCount)
{
g_PeakConnectionCount = numSessions;
}

Console.Title = String.Format("GenOnline - {0} players", m_dictWebsockets.Count);
Console.Title = String.Format("GenOnline - {0} players", numSessions);

SharedUserData? sharedUserData = WebSocketManager.GetSharedDataForUser(ownerID);

Expand Down Expand Up @@ -324,6 +327,17 @@ public static async Task Tick()
await Task.WhenAll(m_dictUserSessions.Values.SelectMany(inner => inner.Values).Select(sess => sess.TickWebsocket(cts.Token)));
}

public static int GetNumberOfUsersOnline()
{
int numSessions = 0;
foreach (var kvPair in m_dictUserSessions)
{
numSessions += kvPair.Value.Count;
}

return numSessions;
}

public static async Task CheckForTimeouts()
{
List<UserWebSocketInstance> lstSessionsToDestroy = new();
Expand Down Expand Up @@ -388,6 +402,11 @@ public static async Task DeleteSession(Int64 user_id, EUserSessionType sessionTy
var item = m_dictWebsockets[sessionType].First(kvp => kvp.Value == oldWS); // safe to lookup by sessionType here since we only ever remove old WS of the same type
m_dictWebsockets[sessionType].Remove(item.Key, out UserWebSocketInstance? destroyedSess);

if (destroyedSess != null)
{
destroyedSess.CloseAsync(WebSocketCloseStatus.NormalClosure, "User signed in from another point of presence [A]");
}

// decrement ref count on shared data
if (m_dictSharedUserData.TryGetValue(user_id, out SharedUserData? sharedData))
{
Expand Down Expand Up @@ -442,13 +461,15 @@ public static async Task DeleteSession(Int64 user_id, EUserSessionType sessionTy
}
}

Console.Title = String.Format("GenOnline - {0} players", m_dictWebsockets.Count);
int numSessions = WebSocketManager.GetNumberOfUsersOnline(); ;
Console.Title = String.Format("GenOnline - {0} players", numSessions);

try
{
if (sourceData != null)
if (oldWS != null)
{
await sourceData.CloseWebsocket(WebSocketCloseStatus.NormalClosure, "Session being deleted");
// Close the WS directly, dont rely on session data as it may be linked to something else at this point
await oldWS.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session being deleted");
}
}
catch
Expand Down Expand Up @@ -503,7 +524,7 @@ public static async Task DeleteSession(Int64 user_id, EUserSessionType sessionTy

private static ConcurrentDictionary<Int64, SharedUserData> m_dictSharedUserData = new();

public static ConcurrentDictionary<EUserSessionType, ConcurrentDictionary<Int64, UserSession>> GetUserDataCache()
public static ConcurrentDictionary<EUserSessionType, ConcurrentDictionary<Int64, UserSession>> GetUserDataCache()
{
return m_dictUserSessions;
}
Expand Down Expand Up @@ -654,7 +675,14 @@ public static async Task TickRoomMemberList()
{
if (sessType == EUserSessionType.GameLauncher)
{
strDisplayName += " [LAUNCHER]";
if (sessionData.Value.m_client_id == KnownClients.EKnownClients.genhub)
{
strDisplayName += " [GENHUB]";
}
else
{
strDisplayName += " [LAUNCHER]";
}
}
else if (sessType == EUserSessionType.ChatClient)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,17 @@ public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr
}

user_id = highestIDFound + 1;

bool bTestSPOP = false;
if (bTestSPOP)
{
user_id = 0;
}
strDisplayName = String.Format("DEV_ACCOUNT_{0}", Math.Abs(user_id) - 1);


// make user
await Database.Users.CreateUserIfNotExists_DevAccount(db, user_id, result.display_name);
await Database.Users.CreateUserIfNotExists_DevAccount(db, user_id, strDisplayName);
}

bool bIsAdmin = await Database.Users.IsUserAdmin(db, user_id);
Expand Down
2 changes: 1 addition & 1 deletion GenOnlineService/Controllers/MOTD/MOTDController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public async Task<APIResult> Get()
if (System.IO.File.Exists(Path.Combine("data", "motd.txt")))
{
string strFileData = await System.IO.File.ReadAllTextAsync(Path.Combine("data", "motd.txt"));
int numPlayers = GenOnlineService.WebSocketManager.GetUserDataCache().Count;
int numPlayers = GenOnlineService.WebSocketManager.GetNumberOfUsersOnline();

result.MOTD = String.Format(strFileData, numPlayers);
}
Expand Down
22 changes: 16 additions & 6 deletions GenOnlineService/Controllers/WebSocket/WebSocketController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ public async Task Get([FromHeader(Name = "is-reconnect")] bool bIsReconnect)
return;
}

EUserSessionType sessType = TokenHelper.GetSessionType(this);

await using var db = await _dbFactory.CreateDbContextAsync();
UserWebSocketInstance wsSess = await WebSocketManager.CreateSession(
db,
EUserSessionType.GameClient,
sessType,
bIsReconnect,
user_id,
client_id,
Expand Down Expand Up @@ -170,11 +172,11 @@ public async Task Get([FromHeader(Name = "is-reconnect")] bool bIsReconnect)
receiveResult = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), cts.Token);
}
catch (OperationCanceledException)
{
// No message received in 30s — send a keep-alive pong and continue waiting
wsSess.SendPong();
continue;
catch (OperationCanceledException)
{
// No message received in 30s — send a keep-alive pong and continue waiting
wsSess.SendPong();
continue;
}
catch (Exception ex)
{
Expand All @@ -195,6 +197,14 @@ public async Task Get([FromHeader(Name = "is-reconnect")] bool bIsReconnect)
var segment = new ArraySegment<byte>(buffer, 0, receiveResult.Count);

UserSession? sourceUserData = WebSocketManager.GetSessionFromUser(wsSess.m_UserID, wsSess.m_SessionType);

// if we lost session data, close WS
if (sourceUserData == null)
{
wsSess.CloseAsync(WebSocketCloseStatus.NormalClosure, "User signed in from another point of presence [B]");
break;
}

await ProcessWSMessage(wsSess, sourceUserData, receiveResult, segment);
}

Expand Down
126 changes: 67 additions & 59 deletions GenOnlineService/Database/Database.ConnectionOutcomes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,65 +61,73 @@ namespace Database
{
public static class ConnectionOutcomes
{
public static async Task StoreConnectionOutcome(
AppDbContext db,
EIPVersion protocol,
EConnectionState outcome)
{
// Only track these states
if (outcome != EConnectionState.CONNECTED_DIRECT &&
outcome != EConnectionState.CONNECTED_RELAY &&
outcome != EConnectionState.CONNECTION_FAILED)
return;

int dayOfYear = DateTime.UtcNow.DayOfYear;

// Load existing row (if any)
var existing = await db.ConnectionOutcomes
.Where(c => c.DayOfYear == dayOfYear)
.FirstOrDefaultAsync();

// If no row exists → create one
if (existing == null)
{
existing = new ConnectionOutcome
{
DayOfYear = dayOfYear,
Ipv4Count = 0,
Ipv6Count = 0,
SuccessCount = 0,
FailedCount = 0
};

db.ConnectionOutcomes.Add(existing);
}

// Increment protocol counters
if (protocol == EIPVersion.IPV4)
existing.Ipv4Count = (existing.Ipv4Count ?? 0) + 1;
else if (protocol == EIPVersion.IPV6)
existing.Ipv6Count = (existing.Ipv6Count ?? 0) + 1;

// Increment outcome counters
if (outcome == EConnectionState.CONNECTED_DIRECT ||
outcome == EConnectionState.CONNECTED_RELAY)
{
existing.SuccessCount = (existing.SuccessCount ?? 0) + 1;
}
else if (outcome == EConnectionState.CONNECTION_FAILED)
{
existing.FailedCount = (existing.FailedCount ?? 0) + 1;
}

// Persist insert/update
await db.SaveChangesAsync();

// Cleanup: delete rows older than 30 days
int cutoff = dayOfYear - 30;

await db.ConnectionOutcomes
.Where(c => c.DayOfYear < cutoff)
.ExecuteDeleteAsync();
public static async Task StoreConnectionOutcome(
AppDbContext db,
EIPVersion protocol,
EConnectionState outcome)
{
// Only track these states
if (outcome != EConnectionState.CONNECTED_DIRECT &&
outcome != EConnectionState.CONNECTED_RELAY &&
outcome != EConnectionState.CONNECTION_FAILED)
return;

try
{
int dayOfYear = DateTime.UtcNow.DayOfYear;

// Load existing row (if any)
var existing = await db.ConnectionOutcomes
.Where(c => c.DayOfYear == dayOfYear)
.FirstOrDefaultAsync();

// If no row exists → create one
if (existing == null)
{
existing = new ConnectionOutcome
{
DayOfYear = dayOfYear,
Ipv4Count = 0,
Ipv6Count = 0,
SuccessCount = 0,
FailedCount = 0
};

db.ConnectionOutcomes.Add(existing);
}

// Increment protocol counters
if (protocol == EIPVersion.IPV4)
existing.Ipv4Count = (existing.Ipv4Count ?? 0) + 1;
else if (protocol == EIPVersion.IPV6)
existing.Ipv6Count = (existing.Ipv6Count ?? 0) + 1;

// Increment outcome counters
if (outcome == EConnectionState.CONNECTED_DIRECT ||
outcome == EConnectionState.CONNECTED_RELAY)
{
existing.SuccessCount = (existing.SuccessCount ?? 0) + 1;
}
else if (outcome == EConnectionState.CONNECTION_FAILED)
{
existing.FailedCount = (existing.FailedCount ?? 0) + 1;
}

// Persist insert/update
await db.SaveChangesAsync();

// Cleanup: delete rows older than 30 days
int cutoff = dayOfYear - 30;

await db.ConnectionOutcomes
.Where(c => c.DayOfYear < cutoff)
.ExecuteDeleteAsync();
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] StoreConnectionOutcome failed: {ex.Message}");
SentrySdk.CaptureException(ex);
}
}

}
Expand Down
Loading
Loading