Skip to content

feat: Implement Generals Online API integration with HTML scraping fallback#201

Draft
undead2146 wants to merge 7 commits intocommunity-outpost:mainfrom
undead2146:feat/generals-online-api
Draft

feat: Implement Generals Online API integration with HTML scraping fallback#201
undead2146 wants to merge 7 commits intocommunity-outpost:mainfrom
undead2146:feat/generals-online-api

Conversation

@undead2146
Copy link
Member

@undead2146 undead2146 commented Nov 29, 2025

Overview

This PR implements comprehensive Generals Online integration for GenHub, including:

  • OAuth authentication flow with gamecode-based browser login
  • Secure credentials storage using Windows DPAPI encryption
  • Token-based session management with refresh tokens
  • HTML scraping layer (temporary fallback until JSON APIs are available)
  • Active lobby/match display with real-time updates
  • Service statistics and player status monitoring

Architecture

Current Implementation (Hybrid Approach)

┌─────────────────────────────────────────────────────────────────┐
│                      GenHub Application                          │
├─────────────────────────────────────────────────────────────────┤
│  ViewModels (GeneralsOnlineViewModel, ActiveMatchesViewModel)   │
│      ↓                                                           │
│  Services (GeneralsOnlineApiClient, HtmlParsingService)          │
│      ↓                                                           │
│  ┌────────────────┐              ┌──────────────────┐          │
│  │  JSON API      │              │  HTML Scraping   │          │
│  │  (Auth, Stats) │              │  (Lobbies, etc)  │          │
│  └────────────────┘              └──────────────────┘          │
└─────────────────────────────────────────────────────────────────┘
           ↓                                  ↓
    [api.playgenerals.online ]      [playgenerals.online Web]

Target Architecture (When Full JSON API Available)

[GenHub] → [GeneralsOnlineApiClient] → [JSON API] → [playgenerals.online]

Core Layer (GenHub.Core)

New Interfaces

IGeneralsOnlineApiClient

Primary HTTP client for all Generals Online API interactions.

Key Methods:

  • Task<ServiceStats?> GetServiceStatsAsync() - Server statistics
  • Task<LoginResult?> CheckLoginAsync(string gameCode) - Poll browser login status
  • Task<LoginResult?> LoginWithTokenAsync(string refreshToken) - Silent re-authentication
  • Task<List<LobbyInfo>> GetLobbiesAsync() - Active lobbies (JSON)
  • Task<OperationResult<string>> GetActiveMatchesAsync() - Active matches (HTML fallback)
  • Task<List<LeaderboardEntry>> GetLeaderboardAsync(string period) - Rankings
  • Task<OperationResult<string>> GetMatchHistoryAsync() - Match history (HTML fallback)
  • void SetTokenProvider(Func<Task<string?>> tokenProvider) - Break circular dependency

Design Patterns:

  • Returns OperationResult<T> for error handling
  • Supports raw JSON methods (.JsonAsync()) for extensibility
  • Full CancellationToken support
  • Uses ConfigureAwait(false) throughout

IGeneralsOnlineAuthService

Manages authentication state, credentials, and login flow.

Key Features:

  • IObservable<bool> IsAuthenticated - Reactive auth state
  • string? CurrentSessionToken - Current API session token
  • string? CurrentDisplayName - Logged-in user's name
  • long? CurrentUserId - Logged-in user's ID

Authentication Methods:

  • Task InitializeAsync() - Start service, attempt silent login
  • Task<LoginResult?> TryLoginWithStoredCredentialsAsync() - Auto-login from saved credentials
  • Task ProcessLoginSuccessAsync(LoginResult) - Save tokens after successful login
  • Task LogoutAsync() - Clear credentials and state
  • string GenerateGameCode() - Create random gamecode for browser flow
  • string GetLoginUrl(string gameCode) - Build login URL with gamecode

Credentials Management:

  • Task<string?> GetAuthTokenAsync() - Get current token
  • Task SaveRefreshTokenAsync(string) - Store refresh token (DPAPI encrypted)

ICredentialsStorageService

Platform-specific credentials file management.

Methods:

  • Task<CredentialsModel?> LoadCredentialsAsync() - Read from encrypted file
  • Task SaveCredentialsAsync(CredentialsModel) - Write with DPAPI encryption
  • Task DeleteCredentialsAsync() - Remove credentials on logout
  • bool CredentialsFileExists() - Check if credentials are saved
  • string GetCredentialsPath() - Get full path to credentials.json

File Location:

%MyDocuments%\Command and Conquer Generals Zero Hour Data\GeneralsOnlineData\credentials.json

IExternalLinkService

Platform-agnostic URL opening for browser authentication.

Method:

  • bool OpenUrl(string url) - Open URL in default browser

New Models (GenHub.Core.Models.GeneralsOnline)

Authentication Models

CredentialsModel

{
    string RefreshToken;
    bool IsValid();
}

LoginResult

{
    PendingLoginState Result;    // Waiting | LoginSuccess | LoginFailed
    string SessionToken;          // For API calls
    string RefreshToken;          // For persistent auth
    long UserId;
    string DisplayName;
    string WebSocketUri;
    
    // Computed properties
    bool IsSuccess;
    bool IsPending;
    bool IsFailed;
}

PendingLoginState (enum)

  • None = -1 - No operation
  • Waiting = 0 - Browser auth pending
  • LoginSuccess = 1 - Auth complete
  • LoginFailed = 2 - Auth failed/cancelled

Lobby & Match Models

ActiveLobby
Represents a live game lobby with full match settings.

{
    string LobbyName;              // "[AS] Generals Online Lobby"
    string MapName;                // "Defcon 6"
    string MapThumbnailUrl;        // Thumbnail path
    int MapSlots;                  // Max players
    string Region;                 // "AS", "EU", "AF"
    IReadOnlyList<LobbySlot> Slots;
    LobbyStatus Status;            // InProgress | Waiting
    LobbySettings Settings;
    
    // Computed properties
    int PlayerCount;
    int OpenSlots;
    string PlayerCountDisplay;     // "2/6"
    string FullMapThumbnailUrl;
    bool CanJoin;
    bool IsInProgress;
    bool IsWaiting;
}

LobbySlot

{
    string? PlayerName;
    string TeamColor;              // Hex color
    string? FactionIcon;           // Faction icon URL
    SlotState SlotState;           // Occupied | Open | Closed
    
    string DisplayName;            // "PlayerName" | "Open" | "Closed"
    string SlotForeground;         // Color for UI
}

LobbySettings

{
    bool IsCustomMap;
    bool IsOriginalArmies;
    string StartingCash;           // "$10000"
    bool LimitSuperweapons;
    bool TrackStats;               // Ranked match
    bool AllowObservers;
    bool IsPassworded;
    string CameraHeight;
}

Existing Models

  • ServiceStats - Connection statistics, player counts
  • LeaderboardEntry - Rankings with scores
  • LobbyInfo - Basic lobby information (JSON endpoint)
  • ActivePlayer, PlayerProfile, MatchInfo, MatchDetails

New Constants (GenHub.Core.Constants.GeneralsOnlineConstants)

REST API Endpoints

RestApiBaseUrl = "https://api.playgenerals.online/env/prod/contract/1"
CheckLoginEndpoint = RestApiBaseUrl + "/CheckLogin"
LoginWithTokenEndpoint = RestApiBaseUrl + "/LoginWithToken"

OAuth & Authentication

LoginUrlBase = "https://www.playgenerals.online/login"
CallbackScheme = "genhub"
CallbackPath = "auth/callback"
CallbackUriTemplate = "genhub://auth/callback"
ClientId = "genhub"
GameCodeLength = 32

Credentials Storage

DataFolderName = "GeneralsOnlineData"
CredentialsFileName = "credentials.json"
GeneralsOnlineDataPath = "Command and Conquer Generals Zero Hour Data\\GeneralsOnlineData"

HTTP Headers

AcceptHeader = "Accept"
AcceptHeaderValue = "text/html,application/json"
AuthTokenHeader = "X-Auth-Token"

Multiplayer API Endpoints

ApiBaseUrl = "https://www.playgenerals.online"
ServiceStatsEndpoint = "/servicestats"
MatchesEndpoint = "/matches"
PlayersEndpoint = "/players"
LeaderboardsEndpoint = "/leaderboards"
MatchHistoryEndpoint = "/matchhistory"
ViewMatchEndpoint = "/viewmatch"
ServiceStatusUrl = "https://stats.uptimerobot.com/5OBCMJwv8P"
FaqUrl = ApiBaseUrl + "/faq"

Feature Layer (GenHub.Features.GeneralsOnline)

Services

GeneralsOnlineApiClient

Implementation Highlights:

  • Uses HttpClient with proper disposal
  • Implements SetTokenProvider() to break circular dependency with auth service
  • Returns OperationResult<T> with success/failure information
  • Full async/await with cancellation support
  • Proper error logging with structured logging

Authentication Flow Methods:

public async Task<LoginResult?> CheckLoginAsync(string gameCode, CancellationToken cancellationToken)
{
    var request = new { gamecode = gameCode, client_id = ClientId };
    var response = await PostJsonAsync(CheckLoginEndpoint, request, cancellationToken);
    return JsonSerializer.Deserialize<LoginResult>(response);
}

public async Task<LoginResult?> LoginWithTokenAsync(string refreshToken, CancellationToken cancellationToken)
{
    var request = new { refresh_token = refreshToken, client_id = ClientId };
    var response = await PostJsonAsync(LoginWithTokenEndpoint, request, cancellationToken);
    return JsonSerializer.Deserialize<LoginResult>(response);
}

GeneralsOnlineAuthService

Responsibilities:

  1. Manage authentication state (Observable pattern)
  2. Handle login flow (gamecode + browser)
  3. Store/retrieve credentials (DPAPI encryption)
  4. Silent re-authentication on startup
  5. Monitor credentials file changes (FileSystemWatcher)

Key Implementation:

private BehaviorSubject<bool> _isAuthenticatedSubject = new(false);
public IObservable<bool> IsAuthenticated => _isAuthenticatedSubject.AsObservable();

public async Task InitializeAsync()
{
    // Try silent login with stored credentials
    var loginResult = await TryLoginWithStoredCredentialsAsync();
    if (loginResult?.IsSuccess == true)
    {
        await ProcessLoginSuccessAsync(loginResult);
    }
    
    // Start watching credentials file for changes
    StartFileWatcher();
}

public async Task ProcessLoginSuccessAsync(LoginResult loginResult)
{
    // Save refresh token (encrypted)
    await SaveRefreshTokenAsync(loginResult.RefreshToken);
    
    // Set API token provider
    _apiClient.SetTokenProvider(() => Task.FromResult(loginResult.SessionToken));
    
    // Update state
    _isAuthenticatedSubject.OnNext(true);
}

HtmlParsingService ⚠️ TEMPORARY

Purpose: Parse HTML responses until JSON APIs are available.

Uses Source-Generated Regex:

[GeneratedRegex(@"<div class=""lobby-name"">(.*?)</div>", RegexOptions.Compiled)]
private static partial Regex LobbyNameRegex();

Current Parsing Methods:

  • List<ActiveLobby> ParseActiveLobbies(string html) - Extract lobby cards
  • LobbySettings ParseLobbySettings(IElement element) - Parse settings icons
  • List<LobbySlot> ParseLobbySlots(IElement element) - Extract player slots
  • ServiceStats ParseServiceStats(string html) - Parse stats from HTML

Dependencies:

  • AngleSharp - HTML DOM parsing

CredentialsStorageService (Platform-Specific)

Windows Implementation:

  • Uses System.Security.Cryptography.ProtectedData (DPAPI)
  • Encrypts JSON with DataProtectionScope.CurrentUser
  • Stores in %MyDocuments%\Command and Conquer Generals Zero Hour Data\GeneralsOnlineData\

ViewModels

GeneralsOnlineViewModel (Main Hub)

Child ViewModels:

  • LoginViewModel - Authentication flow
  • ServiceStatusViewModel - Server statistics
  • LobbiesViewModel - Browse lobbies (JSON API)
  • ActiveMatchesViewModel - Active matches (HTML scraping)
  • LeaderboardViewModel - Rankings
  • MatchHistoryViewModel - Recent matches

Navigation:

  • Tab-based navigation
  • Auth-gated content (shows login if not authenticated)

LoginViewModel

Features:

  • Generate gamecode → Open browser → Poll for completion
  • Display gamecode with copy button
  • Real-time status updates ("Waiting...", "Success!", "Failed")
  • Logout functionality

Flow:

[RelayCommand]
private async Task StartLoginAsync()
{
    var gameCode = _authService.GenerateGameCode();
    var loginUrl = _authService.GetLoginUrl(gameCode);
    
    _externalLinkService.OpenUrl(loginUrl);
    
    // Poll for completion
    while (!cancellationToken.IsCancellationRequested)
    {
        var result = await _apiClient.CheckLoginAsync(gameCode, cancellationToken);
        
        if (result?.IsSuccess == true)
        {
            await _authService.ProcessLoginSuccessAsync(result);
            break;
        }
        await Task.Delay(2000, cancellationToken);
    }
}

ActiveMatchesViewModel

Features:

  • Display active lobbies with rich UI (map thumbnails, player slots, settings icons)
  • Auto-refresh every 60 seconds
  • Filter by status (Waiting, In Progress)
  • Shows lobby count, waiting count, in-progress count

Data Binding:

ObservableCollection<ActiveLobby> Lobbies;
int LobbyCount;
int WaitingLobbiesCount;
int InProgressLobbiesCount;

ServiceStatusViewModel

Displays:

  • Players online (24h, 30d)
  • Peak concurrent players
  • Lifetime total players
  • Connection statistics (IPv4/IPv6, success rate)

Views

Visual Design

  • Modern dark theme with subtle gradients
  • AsyncImageLoader for map thumbnails (lazy loading)
  • Icon system for factions, settings (custom maps, ranked, etc.)
  • Color-coded slots (green for open, white for occupied, gray for closed)
  • Responsive grid layout for lobby cards

New UI Components

  • LoginView.axaml - Authentication interface
  • ActiveMatchesView.axaml - Lobby grid display
  • ServiceStatusView.axaml - Statistics dashboard
  • GeneralsOnlineView.axaml - Main navigation hub

Screenshots

Active Matches View

Displays lobbies with map thumbnails, player slots, and settings

Login Flow

Gamecode display with browser authentication

Service Statistics

Player counts, connection stats, uptime


@undead2146 undead2146 force-pushed the feat/generals-online-api branch 3 times, most recently from 5f42591 to dbb69d2 Compare December 14, 2025 19:07
…tch History, and Service Status views

- Implemented LeaderboardView and its code-behind.
- Created LobbiesView and its code-behind with UI elements for displaying active players and matches.
- Developed MatchHistoryView and its code-behind to show recent match details.
- Added ServiceStatusView and its code-behind for displaying service statistics.
- Introduced various converters for player status, rank, region, and tab names to enhance UI data binding.
- Registered new services and ViewModels in the dependency injection container for Generals Online features.
- Removed unnecessary await statements in SettingsViewModel.
…ation services, credential storage, and UI components.
@undead2146 undead2146 force-pushed the feat/generals-online-api branch from dbb69d2 to 8c40523 Compare December 15, 2025 00:16
undead2146 and others added 2 commits December 28, 2025 19:38
…trols

- Added `NullableDecimalToIntConverter` to handle data conversion between View and ViewModel.
- Defined a new "phantom" style for `NumericUpDown` to match the existing minimalist UI.
- Replaced `TextBlock` value displays with `NumericUpDown` controls for Gamma, Audio volumes, Font sizes, and Camera settings.
- Adjusted Grid column widths to accommodate interactive input fields.
@undead2146 undead2146 force-pushed the feat/generals-online-api branch from 854a824 to ffce239 Compare January 3, 2026 17:21
@undead2146 undead2146 force-pushed the feat/generals-online-api branch from ffce239 to f4859ed Compare January 3, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant