Skip to content
Draft
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
1 change: 1 addition & 0 deletions GenHub/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<!-- Avalonia packages -->
<!-- Important: keep version in sync! -->
<PackageVersion Include="AngleSharp" Version="1.3.0" />
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.3.0" />
<PackageVersion Include="Avalonia" Version="11.2.7" />
<PackageVersion Include="Avalonia.Desktop" Version="11.2.7" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.2.7" />
Expand Down
93 changes: 92 additions & 1 deletion GenHub/GenHub.Core/Constants/GeneralsOnlineConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace GenHub.Core.Constants;
/// </summary>
public static class GeneralsOnlineConstants
{
// ===== API Endpoints =====
// ===== CDN API Endpoints =====

/// <summary>Base URL for Generals Online CDN.</summary>
public const string CdnBaseUrl = "https://cdn.playgenerals.online";
Expand All @@ -30,6 +30,35 @@ public static class GeneralsOnlineConstants
/// <summary>Support/discord URL.</summary>
public const string SupportUrl = "https://discord.playgenerals.online/";

// ===== Multiplayer API Endpoints =====

/// <summary>Base URL for Generals Online web API.</summary>
public const string ApiBaseUrl = "https://www.playgenerals.online";

/// <summary>API endpoint for service statistics.</summary>
public const string ServiceStatsEndpoint = "https://www.playgenerals.online/servicestats";

/// <summary>API endpoint for active matches and lobbies.</summary>
public const string MatchesEndpoint = "https://www.playgenerals.online/matches";

/// <summary>API endpoint for player profiles.</summary>
public const string PlayersEndpoint = "https://www.playgenerals.online/players";

/// <summary>API endpoint for leaderboards.</summary>
public const string LeaderboardsEndpoint = "https://www.playgenerals.online/leaderboards";

/// <summary>API endpoint for match history.</summary>
public const string MatchHistoryEndpoint = "https://www.playgenerals.online/matchhistory";

/// <summary>API endpoint for viewing a specific match.</summary>
public const string ViewMatchEndpoint = "https://www.playgenerals.online/viewmatch";

/// <summary>Service status monitoring URL.</summary>
public const string ServiceStatusUrl = "https://stats.uptimerobot.com/5OBCMJwv8P";

/// <summary>FAQ page URL.</summary>
public const string FaqUrl = ApiBaseUrl + "/faq";

// ===== Content Metadata =====

/// <summary>Publisher name for manifests.</summary>
Expand Down Expand Up @@ -108,6 +137,68 @@ public static class GeneralsOnlineConstants
/// <summary>Description for Generals Online deliverer.</summary>
public const string DelivererDescription = "Delivers Generals Online content via ZIP extraction and CAS storage";

// ===== Authentication & API =====

/// <summary>Base URL for Generals Online REST API (production environment, contract version 1).</summary>
public const string RestApiBaseUrl = "https://api.playgenerals.online/env/prod/contract/1";

/// <summary>API endpoint for checking browser login status by gamecode.</summary>
public const string CheckLoginEndpoint = RestApiBaseUrl + "/CheckLogin";

/// <summary>API endpoint for exchanging refresh token for session token.</summary>
public const string LoginWithTokenEndpoint = RestApiBaseUrl + "/LoginWithToken";

/// <summary>API endpoint for token verification.</summary>
public const string VerifyTokenEndpoint = "https://api.playgenerals.online/v1/user/verify";

/// <summary>API endpoint for user profile information.</summary>
public const string UserProfileEndpoint = "https://api.playgenerals.online/v1/user/profile";

/// <summary>API endpoint for lobby listings.</summary>
public const string LobbiesEndpoint = "https://api.playgenerals.online/v1/lobbies";

/// <summary>Client identifier for GenHub authentication.</summary>
public const string ClientId = "genhub";

/// <summary>Length of the gamecode for browser login.</summary>
public const int GameCodeLength = 32;

// ===== HTTP Headers =====

/// <summary>HTTP Accept header name.</summary>
public const string AcceptHeader = "Accept";

/// <summary>HTTP Accept header value for Generals Online API.</summary>
public const string AcceptHeaderValue = "text/html,application/json";

/// <summary>HTTP header name for authentication token.</summary>
public const string AuthTokenHeader = "X-Auth-Token";

// ===== OAuth and Authentication Flow =====

/// <summary>Base URL for the login page.</summary>
public const string LoginUrlBase = "https://www.playgenerals.online/login";

/// <summary>Custom URI scheme for OAuth callbacks.</summary>
public const string CallbackScheme = "genhub";

/// <summary>Path component for OAuth callback URI.</summary>
public const string CallbackPath = "auth/callback";

/// <summary>Full callback URI template: genhub://auth/callback.</summary>
public const string CallbackUriTemplate = CallbackScheme + "://" + CallbackPath;

// ===== Credentials File =====

/// <summary>Name of the GeneralsOnline data folder.</summary>
public const string DataFolderName = "GeneralsOnlineData";

/// <summary>Name of the credentials JSON file.</summary>
public const string CredentialsFileName = "credentials.json";

/// <summary>Relative path to GeneralsOnlineData folder from Generals Zero Hour Data.</summary>
public const string GeneralsOnlineDataPath = "Command and Conquer Generals Zero Hour Data" + "\\" + DataFolderName;

// ===== Content Tags =====

/// <summary>Content tags for search and categorization.</summary>
Expand Down
22 changes: 22 additions & 0 deletions GenHub/GenHub.Core/Constants/UiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,26 @@ public static class UiConstants
/// Display name for Content Bundle content type.
/// </summary>
public const string ContentBundleDisplayName = "Bundles";

// Error Messages

/// <summary>
/// Generic error message for failed service status loading.
/// </summary>
public const string FailedToLoadServiceStatus = "Failed to load service status. Please try again.";

/// <summary>
/// Generic error message for failed player data loading.
/// </summary>
public const string FailedToLoadPlayerData = "Failed to load player data. Please try again.";

/// <summary>
/// Generic error message for failed leaderboard loading.
/// </summary>
public const string FailedToLoadLeaderboard = "Failed to load leaderboard data. Please try again.";

/// <summary>
/// Generic error message for failed match history loading.
/// </summary>
public const string FailedToLoadMatchHistory = "Failed to load match history. Please try again.";
}
14 changes: 14 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Common/IExternalLinkService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace GenHub.Core.Interfaces.Common;

/// <summary>
/// Interface for opening external links and URLs.
/// </summary>
public interface IExternalLinkService
{
/// <summary>
/// Opens the specified URL in the default system browser.
/// </summary>
/// <param name="url">The URL to open.</param>
/// <returns>True if the URL was opened successfully, otherwise false.</returns>
bool OpenUrl(string url);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using GenHub.Core.Models.GeneralsOnline;

namespace GenHub.Core.Interfaces.GeneralsOnline;

/// <summary>
/// Interface for managing GeneralsOnline credentials file storage.
/// </summary>
public interface ICredentialsStorageService
{
/// <summary>
/// Loads the credentials from the credentials.json file.
/// </summary>
/// <returns>The credentials model if the file exists and is valid, otherwise null.</returns>
Task<CredentialsModel?> LoadCredentialsAsync();

/// <summary>
/// Saves the credentials to the credentials.json file.
/// </summary>
/// <param name="credentials">The credentials to save.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task SaveCredentialsAsync(CredentialsModel credentials);

/// <summary>
/// Deletes the credentials.json file if it exists.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
Task DeleteCredentialsAsync();

/// <summary>
/// Gets the full path to the credentials.json file.
/// </summary>
/// <returns>The absolute path to the credentials file.</returns>
string GetCredentialsPath();

/// <summary>
/// Checks if the credentials file exists.
/// </summary>
/// <returns>True if the file exists, otherwise false.</returns>
bool CredentialsFileExists();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using GenHub.Core.Models.GeneralsOnline;
using GenHub.Core.Models.Results;

namespace GenHub.Core.Interfaces.GeneralsOnline;

/// <summary>
/// Interface for Generals Online API interactions with strongly-typed responses.
/// Extension-friendly design: Add new methods here when new API endpoints become available.
/// </summary>
public interface IGeneralsOnlineApiClient
{
/// <summary>
/// Sets the token provider function for authenticated API calls.
/// This breaks circular dependency between API client and auth service.
/// </summary>
/// <param name="tokenProvider">Function that returns the current auth token.</param>
void SetTokenProvider(Func<Task<string?>> tokenProvider);

// ===== Service & Stats =====

/// <summary>
/// Gets service statistics including player counts and connection data.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>Service statistics or null if unavailable.</returns>
Task<ServiceStats?> GetServiceStatsAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets service statistics as raw JSON (for custom parsing or future extensibility).
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the JSON string or error information.</returns>
Task<OperationResult<string>> GetServiceStatsJsonAsync(CancellationToken cancellationToken = default);

// ===== Authentication =====

/// <summary>
/// Verifies the provided authentication token with the server.
/// </summary>
/// <param name="token">The token to verify.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>True if the token is valid, otherwise false.</returns>
Task<bool> VerifyTokenAsync(string token, CancellationToken cancellationToken = default);

// ===== Matches =====

/// <summary>
/// Gets active matches and lobbies as HTML.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the HTML string or error information.</returns>
Task<OperationResult<string>> GetActiveMatchesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets details for a specific match as HTML.
/// </summary>
/// <param name="matchId">The match ID.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the HTML string or error information.</returns>
Task<OperationResult<string>> GetMatchDetailsAsync(int matchId, CancellationToken cancellationToken = default);

/// <summary>
/// Gets the match history as HTML.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the HTML string or error information.</returns>
Task<OperationResult<string>> GetMatchHistoryAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets the match history for a specific player as HTML.
/// </summary>
/// <param name="playerName">The player name to look up.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the HTML string or error information.</returns>
Task<OperationResult<string>> GetPlayerMatchHistoryAsync(string playerName, CancellationToken cancellationToken = default);

// ===== Lobbies =====

/// <summary>
/// Gets available lobbies.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A list of available lobbies.</returns>
Task<List<LobbyInfo>> GetLobbiesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets lobbies as raw JSON (for custom parsing or future extensibility).
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the JSON string or error information.</returns>
Task<OperationResult<string>> GetLobbiesJsonAsync(CancellationToken cancellationToken = default);

// ===== Players =====

/// <summary>
/// Gets a list of currently active players as HTML.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the HTML string or error information.</returns>
Task<OperationResult<string>> GetActivePlayersAsync(CancellationToken cancellationToken = default);

// ===== Leaderboards =====

/// <summary>
/// Gets leaderboard data for the specified period.
/// </summary>
/// <param name="period">The leaderboard period (daily, monthly, annual).</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A list of leaderboard entries.</returns>
Task<List<LeaderboardEntry>> GetLeaderboardAsync(string period = "daily", CancellationToken cancellationToken = default);

/// <summary>
/// Gets leaderboard as raw JSON (for custom parsing or future extensibility).
/// </summary>
/// <param name="period">The leaderboard period.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An operation result containing the JSON string or error information.</returns>
Task<OperationResult<string>> GetLeaderboardJsonAsync(string period = "daily", CancellationToken cancellationToken = default);

// ===== Authentication - Gamecode Flow =====

/// <summary>
/// Checks if a user has completed login for the given gamecode.
/// Polls the server to see if browser authentication is complete.
/// </summary>
/// <param name="gameCode">The gamecode to check.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The login result containing authentication state and tokens.</returns>
Task<LoginResult?> CheckLoginAsync(string gameCode, CancellationToken cancellationToken = default);

/// <summary>
/// Validates a refresh token and exchanges it for a session token.
/// Used for silent re-authentication with stored credentials.
/// </summary>
/// <param name="refreshToken">The refresh token to validate.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The login result containing session token and user info, or null on failure.</returns>
Task<LoginResult?> LoginWithTokenAsync(string refreshToken, CancellationToken cancellationToken = default);
}
Loading