|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +This is the **CnCNet Ladder API**, a Laravel 11-based competitive ladder/ranking system for classic Command & Conquer games (Red Alert, Yuri's Revenge, Tiberian Sun, Dune 2000). It provides 1v1, 2v2, and clan match support with automated Quick Match (QM) matchmaking, ELO ratings, player statistics, and achievement tracking. |
| 8 | + |
| 9 | +**Tech Stack**: Laravel 11, PHP 8.3, FrankenPHP (Laravel Octane), MariaDB, Redis, Bootstrap 5, Vite |
| 10 | + |
| 11 | +## Development Commands |
| 12 | + |
| 13 | +### Initial Setup |
| 14 | +```bash |
| 15 | +# Build and start development containers |
| 16 | +docker compose -f docker-compose.dev.yml build |
| 17 | +docker compose -f docker-compose.dev.yml up -d |
| 18 | + |
| 19 | +# Generate Laravel app key |
| 20 | +docker exec dev_cncnet_ladder_app php artisan key:generate |
| 21 | + |
| 22 | +# Migrate database (or restore from backup) |
| 23 | +docker exec dev_cncnet_ladder_app php artisan migrate |
| 24 | +``` |
| 25 | + |
| 26 | +### Common Development Tasks |
| 27 | +```bash |
| 28 | +# Clear cache after .env changes |
| 29 | +docker exec dev_cncnet_ladder_app php artisan optimize:clear |
| 30 | + |
| 31 | +# Run queue workers (manual in dev) |
| 32 | +docker exec -it dev_cncnet_ladder_app php artisan queue:listen --queue=findmatch,saveladderresult |
| 33 | + |
| 34 | +# Run scheduler/cron manually |
| 35 | +docker exec dev_cncnet_ladder_app php artisan scheduler:run |
| 36 | + |
| 37 | +# Start Vite dev server (hot reload) |
| 38 | +docker exec -it dev_cncnet_ladder_app npm run dev |
| 39 | + |
| 40 | +# Watch SCSS changes |
| 41 | +npm run watch |
| 42 | + |
| 43 | +# Open shell in container |
| 44 | +docker exec -it dev_cncnet_ladder_app bash |
| 45 | + |
| 46 | +# Access database |
| 47 | +# Host: localhost, Port: 3307, User/Pass from .env |
| 48 | +``` |
| 49 | + |
| 50 | +### Testing |
| 51 | +```bash |
| 52 | +# Run PHPUnit tests |
| 53 | +docker exec dev_cncnet_ladder_app php artisan test |
| 54 | +``` |
| 55 | + |
| 56 | +### Production Commands (CI/CD automated) |
| 57 | +```bash |
| 58 | +# Build production containers |
| 59 | +docker compose build |
| 60 | + |
| 61 | +# Clear config cache after deployment |
| 62 | +docker exec cncnet_ladder_app php artisan config:cache |
| 63 | +``` |
| 64 | + |
| 65 | +## Architecture & Code Structure |
| 66 | + |
| 67 | +### Laravel Application Structure |
| 68 | + |
| 69 | +The main application lives in `cncnet-api/`. Key architectural patterns: |
| 70 | + |
| 71 | +**Controllers** (`app/Http/Controllers/`): |
| 72 | +- `ApiLadderController`: REST API for ladder data, game results, player stats |
| 73 | +- `ApiQuickMatchController`: QM matchmaking, map pools, rankings |
| 74 | +- `LadderController`: Web UI views |
| 75 | +- `AdminController`: Admin panel functionality |
| 76 | +- API versioning: v1 (main), v2 (new endpoints) |
| 77 | + |
| 78 | +**Services** (`app/Http/Services/`): |
| 79 | +Business logic layer: |
| 80 | +- `QuickMatchService`: Core matchmaking logic |
| 81 | +- `EloService`: Rating calculations using ELO algorithm |
| 82 | +- `GameService`: Game result processing and validation |
| 83 | +- `LadderService`: Ladder operations and caching |
| 84 | +- `PlayerService`: Player management and ratings |
| 85 | +- `AchievementService`: Achievement tracking |
| 86 | + |
| 87 | +**Queue System**: |
| 88 | +Two separate queues processed by dedicated workers: |
| 89 | +- `findmatch`: Handles opponent finding (`FindOpponentJob`) |
| 90 | +- `saveladderresult`: Processes game results (`SaveLadderResultJob`) |
| 91 | + |
| 92 | +Queue jobs live in `app/Jobs/Qm/` |
| 93 | + |
| 94 | +**Extensions** (`app/Extensions/`): |
| 95 | +Matchup handlers for different game modes: |
| 96 | +- `PlayerMatchupHandler`: 1v1 matchmaking |
| 97 | +- `TeamMatchupHandler`: 2v2+ matchmaking |
| 98 | +- `ClanMatchupHandler`: Clan match logic |
| 99 | + |
| 100 | +### Database Schema (70+ Models) |
| 101 | + |
| 102 | +**Core Models**: |
| 103 | +- `Ladder`: Game type definitions (RA2, YR, TS, etc.) |
| 104 | +- `LadderHistory`: Monthly ladder snapshots |
| 105 | +- `Player`: Player accounts (per-ladder) |
| 106 | +- `User`: Global user accounts |
| 107 | +- `Game`: Individual game records |
| 108 | +- `GameReport`: Player-submitted game results (can have multiple per game) |
| 109 | +- `PlayerGameReport`: Individual player performance in a game |
| 110 | +- `QmMatch`: Quick Match game records |
| 111 | +- `QmMatchPlayer`: Player participation in QM match |
| 112 | +- `QmQueueEntry`: Active matchmaking queue entries |
| 113 | +- `PlayerRating`/`UserRating`: ELO rating storage |
| 114 | +- `Clan`: Clan/team entities with ratings |
| 115 | +- `Ban`: Player ban records |
| 116 | + |
| 117 | +**Important Relationships**: |
| 118 | +- `Game` has many `GameReport`s (one is marked `best_report`) |
| 119 | +- `GameReport` has many `PlayerGameReport`s |
| 120 | +- `Player` belongs to `Ladder` and `User` |
| 121 | +- `QmMatch` has many `QmMatchPlayer`s |
| 122 | + |
| 123 | +### Quick Match Flow |
| 124 | + |
| 125 | +1. Client requests match via `POST /api/v1/qm/{ladder}/{player}` → `MatchUpController` |
| 126 | +2. Request validated through middleware: ClientUpToDate, ShadowBan, Ban, VerifiedEmail |
| 127 | +3. `QmQueueEntry` created for player |
| 128 | +4. `FindOpponentJob` dispatched to queue |
| 129 | +5. Job selects appropriate handler (Player/Team/Clan MatchupHandler) |
| 130 | +6. Matching algorithm considers: |
| 131 | + - ELO ratings and tier placement |
| 132 | + - Map preferences/vetoes |
| 133 | + - Faction policies (allowed pairings) |
| 134 | + - Connection quality/ping |
| 135 | +7. `QmMatch` created with spawn parameters sent to clients |
| 136 | +8. Game results submitted via `POST /api/v1/result/ladder/{ladderId}/game/{gameId}/player/{playerId}/pings/{sent}/{received}` |
| 137 | +9. `SaveLadderResultJob` processes stats dump file |
| 138 | +10. Points calculated (`awardPlayerPoints`, `awardTeamPoints`, or `awardClanPoints` methods) |
| 139 | +11. ELO ratings updated via `EloService` |
| 140 | +12. Player cache updated via `PlayerCache` model |
| 141 | + |
| 142 | +### Middleware & Caching |
| 143 | + |
| 144 | +**Custom Cache Middleware** (all public): |
| 145 | +- `CacheUltraShortPublic`: 10 seconds (IRC endpoints) |
| 146 | +- `CacheShortPublic`: 30 seconds |
| 147 | +- `CachePublicMiddleware`: 1 minute (player stats, short-lived data) |
| 148 | +- `CacheLongPublicMiddleware`: 60 minutes (ladder listings, static data) |
| 149 | + |
| 150 | +**Restriction Middleware**: |
| 151 | +- `Restrict`: Permission checking for admin actions |
| 152 | +- `BanMiddleware`: Checks active bans before QM |
| 153 | +- `VerifiedEmailMiddleware`: Requires verified email |
| 154 | +- `ClientUpToDateMiddleware`: Client version validation |
| 155 | + |
| 156 | +### Docker Architecture |
| 157 | + |
| 158 | +**Development** (`docker-compose.dev.yml`): |
| 159 | +- Single `app` container with volume mounts for hot reload |
| 160 | +- MySQL exposed on port 3307 |
| 161 | +- PHPMyAdmin on port 8080 |
| 162 | +- Vite dev server on port 5173 |
| 163 | +- Queue/scheduler NOT running (manual start required) |
| 164 | + |
| 165 | +**Production** (`docker-compose.yml`): |
| 166 | +- `app`: FrankenPHP web server (port 3000→8000) |
| 167 | +- `queue-findmatch`: Dedicated queue worker |
| 168 | +- `queue-saveladderresult`: Dedicated queue worker |
| 169 | +- `scheduler`: Laravel scheduler (cron tasks) |
| 170 | +- `mysql`: MariaDB database |
| 171 | +- `redis`: Cache and queue backend |
| 172 | +- `db-backup`: Automated backups using tiredofit/db-backup |
| 173 | +- `elogen`: ELO computation cron container |
| 174 | + |
| 175 | +**Multi-stage Build**: Dockerfiles in `docker/frankenphp/` and `docker/workers/` |
| 176 | + |
| 177 | +### Configuration Files |
| 178 | + |
| 179 | +**Environment Files**: |
| 180 | +- `.env`: Docker-specific (HOST_USER, HOST_UID, APP_TAG, ports) |
| 181 | +- `.app.env`: Laravel application config (production) |
| 182 | +- `.backup.env`: Backup container settings |
| 183 | +- `cncnet-api/.env`: Development Laravel config |
| 184 | + |
| 185 | +**Key Laravel Configs**: |
| 186 | +- `config/types.php`: Game type definitions (28KB of metadata) |
| 187 | +- `config/cameos.php`: Unit cameo mappings |
| 188 | +- `config/jwt.php`: JWT authentication |
| 189 | +- `config/octane.php`: FrankenPHP/Octane settings |
| 190 | + |
| 191 | +### Scheduled Tasks |
| 192 | + |
| 193 | +Defined in `bootstrap/app.php` (Laravel 11 structure): |
| 194 | +- **Daily**: Log pruning, stats cleanup |
| 195 | +- **Hourly**: Player cache updates (`UpdatePlayerCache` command) |
| 196 | +- **Monthly**: QM data pruning, player rating updates |
| 197 | +- **Every Minute**: Clear inactive queue entries |
| 198 | + |
| 199 | +### Routes Structure |
| 200 | + |
| 201 | +**Web Routes** (`routes/web.php`): 400+ lines |
| 202 | +- Ladder views, player profiles, admin panel |
| 203 | +- Grouped by authentication and permissions |
| 204 | + |
| 205 | +**API Routes** (`routes/api.php`): |
| 206 | +- `v1`: Main API (auth, ladder, QM, results) |
| 207 | +- `v2`: New endpoints (bans, events, user accounts) |
| 208 | +- Middleware groups for caching and authentication |
| 209 | + |
| 210 | +### Game Result Processing |
| 211 | + |
| 212 | +When game results are submitted: |
| 213 | + |
| 214 | +1. Stats dump file uploaded and moved to `config('filesystems.dmp')` directory |
| 215 | +2. `SaveLadderResultJob` queued |
| 216 | +3. Job calls `GameService::processStatsDmp()` to parse binary stats |
| 217 | +4. `GameReport` created or updated |
| 218 | +5. Dispute handling: Multiple reports compared, best report selected based on: |
| 219 | + - Finished status (prefer finished over disconnected) |
| 220 | + - Duration (prefer longer games) |
| 221 | + - Ping difference (prefer better connection) |
| 222 | +6. If both reports show disconnect/OOS: auto-wash game (create draw report) |
| 223 | +7. Points awarded via `awardPlayerPoints()`, `awardTeamPoints()`, or `awardClanPoints()` |
| 224 | +8. Achievement progress tracked (`updateAchievements()`) |
| 225 | +9. Player cache updated via `LadderService::updateCache()` |
| 226 | + |
| 227 | +### Achievement System |
| 228 | + |
| 229 | +Two types: |
| 230 | +- **CAREER**: Cumulative tracking (e.g., build 1000 tanks) |
| 231 | +- **IMMEDIATE**: Single-game threshold (e.g., build 50 tanks in one game) |
| 232 | + |
| 233 | +Tracked via `Achievement`, `AchievementProgress`, and `GameObjectCounts` models. |
| 234 | + |
| 235 | +### Anti-Cheat & Moderation |
| 236 | + |
| 237 | +- IP address tracking via `IpAddress` and `IpAddressHistory` |
| 238 | +- Admin actions logged via Spatie ActivityLog |
| 239 | +- Game "washing" (marking as draw) for: |
| 240 | + - Mutual disconnects |
| 241 | + - Mutual out-of-sync |
| 242 | + - Suspected abuse |
| 243 | +- Shadow bans (queue but never match) |
| 244 | +- Admin panel for manual intervention |
| 245 | + |
| 246 | +### Important Code Patterns |
| 247 | + |
| 248 | +**Finding Ladder by Abbreviation**: |
| 249 | +```php |
| 250 | +$ladder = Ladder::where('abbreviation', '=', $game)->first(); |
| 251 | +``` |
| 252 | + |
| 253 | +**Getting Current Ladder History** (monthly snapshot): |
| 254 | +```php |
| 255 | +$history = $ladder->currentHistory(); |
| 256 | +``` |
| 257 | + |
| 258 | +**Querying Player Games with Joins**: |
| 259 | +Always join through `game_reports` and `games` tables, filtering on `valid` and `best_report`: |
| 260 | +```php |
| 261 | +PlayerGameReport::where('player_game_reports.player_id', '=', $playerId) |
| 262 | + ->join('game_reports', 'game_reports.id', '=', 'player_game_reports.game_report_id') |
| 263 | + ->join('games', 'games.id', '=', 'game_reports.game_id') |
| 264 | + ->where('game_reports.valid', '=', true) |
| 265 | + ->where('game_reports.best_report', '=', true) |
| 266 | +``` |
| 267 | + |
| 268 | +**Table Prefixing**: Always prefix ambiguous columns (e.g., `player_game_reports.player_id` not `player_id`) when joining multiple tables. |
| 269 | + |
| 270 | +## CI/CD Pipeline |
| 271 | + |
| 272 | +**GitHub Actions** (`.github/workflows/build-and-deploy.yml`): |
| 273 | + |
| 274 | +1. **Build Job**: Builds 3 images in parallel (app, queue, scheduler) |
| 275 | + - Tagged with: `latest`, branch name, short SHA |
| 276 | + - Pushed to GitHub Container Registry (ghcr.io) |
| 277 | + |
| 278 | +2. **Deploy Job** (main branch only): |
| 279 | + - Uploads `docker-compose.yml` via SCP |
| 280 | + - SSH to server |
| 281 | + - Updates `.env` with new image tag |
| 282 | + - Pulls images and restarts containers |
| 283 | + - Runs `php artisan config:cache` |
| 284 | + |
| 285 | +**Deployment is automatic** on merge to main branch. |
| 286 | + |
| 287 | +## Development Notes |
| 288 | + |
| 289 | +### Laravel 11 Changes |
| 290 | +- No `app/Http/Kernel.php` - middleware in `bootstrap/app.php` |
| 291 | +- Scheduling in `bootstrap/app.php` instead of `app/Console/Kernel.php` |
| 292 | +- Slimmer directory structure |
| 293 | + |
| 294 | +### FrankenPHP (Octane) |
| 295 | +- High-performance PHP application server |
| 296 | +- Uses Caddy web server under the hood |
| 297 | +- Configured via `OCTANE_SERVER=frankenphp` in `.env` |
| 298 | +- Startup script: `docker/frankenphp/octane.sh` |
| 299 | + |
| 300 | +### Queue Workers |
| 301 | +- Run in separate containers for horizontal scaling |
| 302 | +- Each queue has dedicated worker (findmatch, saveladderresult) |
| 303 | +- Worker script: `docker/workers/queue.sh` |
| 304 | + |
| 305 | +### Database Access |
| 306 | +- **Dev**: localhost:3307 (exposed) |
| 307 | +- **Dev UI**: PHPMyAdmin at localhost:8080 |
| 308 | +- Migrations in `database/migrations/` (42 files) |
| 309 | +- Schema dumps in `database/schema/` |
| 310 | + |
| 311 | +### Assets |
| 312 | +- Vite build system (`vite.config.ts`) |
| 313 | +- SCSS in `resources/stylesheets/` |
| 314 | +- TypeScript in `resources/typescript/` |
| 315 | +- Build output to `public/build/` |
| 316 | +- Dev server: port 5173 (hot reload) |
| 317 | + |
| 318 | +### Elogen Container |
| 319 | +Separate cron-based ELO computation system: |
| 320 | +- Configured via `docker/elogen/crontab` |
| 321 | +- Reads/writes to `storage/app/rating/` |
| 322 | +- Independent calculation for validation |
| 323 | + |
| 324 | +## Access URLs |
| 325 | + |
| 326 | +- **Development Web UI**: http://localhost:3000 |
| 327 | +- **Development PHPMyAdmin**: http://localhost:8080 |
| 328 | +- **Development Vite**: http://localhost:5173 |
| 329 | +- **Production**: https://ladder.cncnet.org |
| 330 | + |
| 331 | +## Troubleshooting |
| 332 | + |
| 333 | +**"No supported encrypter found"**: |
| 334 | +- Generate key: `docker exec dev_cncnet_ladder_app php artisan key:generate` |
| 335 | +- Update .env then rebuild: `docker compose -f docker-compose.dev.yml up -d` |
| 336 | + |
| 337 | +**"Base table or view not found"**: |
| 338 | +- Restore from backup OR run migrations: `docker exec dev_cncnet_ladder_app php artisan migrate` |
| 339 | + |
| 340 | +**Queue jobs not processing**: |
| 341 | +- In dev, queue workers don't auto-start |
| 342 | +- Manual start: `docker exec -it dev_cncnet_ladder_app php artisan queue:listen --queue=findmatch,saveladderresult` |
| 343 | + |
| 344 | +**"Column 'X' is ambiguous" SQL errors**: |
| 345 | +- Always prefix columns with table name when joining (e.g., `player_game_reports.player_id`) |
| 346 | + |
| 347 | +**WSL2 Docker integration issues**: |
| 348 | +- Check Docker Desktop → Settings → Resources → WSL Integration |
| 349 | +- Ensure Ubuntu distro is enabled |
| 350 | +- Restart Docker Desktop or run `wsl --shutdown` and restart |
0 commit comments