# CLI API

The CLI entry point is `src/cli.php`. It is a thin dispatcher for long-running
or maintenance tasks. There is no command registry: a controller file and a
function are enough to expose a command.

## Usage

From the repository root:

```bash
php src/cli.php <controller> <command> [arguments]
```

From inside `src/`:

```bash
php cli.php <controller> <command> [arguments]
```

Examples:

```bash
php src/cli.php ranking generateAll
php src/cli.php ranking generatePlayers
```

The CLI changes its working directory to `src/` before loading configuration.
Relative paths such as `DATABASE_FOLDER` are therefore relative to `src/`.

Composer aliases:

```bash
composer updateRanking
cd src && composer updateRanking
```

## Dispatch

`src/cli.php` performs these steps:

1. Requires CLI SAPI and a loaded `php.ini`.
2. Loads `src/settings/config.php` and `src/settings/helpers.php`.
3. Loads `src/core/cli/<controller>.php`.
4. Calls `App\Cli\Controllers\<Controller>\<command>`.
5. Passes all arguments after the command as one zero-based array of strings.

The controller argument is also used as a filename, so file names are case
sensitive on Linux. The command function name is case-insensitive in PHP, but
matching the documented casing avoids confusion.

## Adding A Controller

Create `src/core/cli/<controller>.php` with a matching namespace:

```php
<?php

namespace App\Cli\Controllers\Maintenance;

function rebuild($arguments = [])
{
  // Read positional values from $arguments and perform the task.
}
```

Run it with:

```bash
php src/cli.php maintenance rebuild value1 value2
```

Implementation rules:

- Keep the controller procedural; do not create a controller class.
- Define one function per command in the controller file.
- Accept one `$arguments` array even when the command currently has no options.
- Load models with `loadModel('<modelName>')` before using them.
- Use `Model::$conn` for game data and `Model::$connCMS` for CMS data.
- Handle transactions and exceptions inside commands that write data.
- Update this document and `src/core/cli/Readme.md` when adding a command.
- Add a Composer script only for frequently used commands.

There is currently no automatic option parser, help command, or non-zero exit
status generated from a command return value. Parse arguments and report task
failures in the command implementation when automation depends on them.

## Database Transactions

The bootstrap creates both database connections. `Model` exposes these
transaction helpers:

```php
Model::beginTransaction(); // Start game and CMS transactions.
Model::commit();
Model::rollback();
```

For a CMS-only operation, use `Model::startTransaction()` and finish it with
`Model::finishTransaction($startedTransaction)` or
`Model::rollbackTransaction($startedTransaction)`. The ranking importer uses
the full transaction wrapper so `generateAll` commits all ranking datasets
together.

## Current Controllers

| Controller | File | Active commands | Purpose |
|---|---|---|---|
| `ranking` | `src/core/cli/ranking.php` | `generateAll`, `generatePlayers`, `generateGuilds`, `generateArena` | Import file-backed ranking data |
| `payment` | `src/core/cli/payment.php` | None | Reserved for payment maintenance tasks; the current experiment is commented out |

Ranking commands are intended for file-backed sources such as `stream`,
`stream-pod`, `stream-topco`, `stream-rdconquer`, and `trinity-stream`. They are
not the import path for emulator sources.

## Ranking Import Flow

The active source is selected by `SOURCE_TYPE`. The ranking controller loads
the active source's file readers through `MODELS_SOURCE` and writes to the
shared `CmsRankingModel`.

| Dataset | Input | Reader | Target | Key |
|---|---|---|---|---|
| Players | `DATABASE_FOLDER/Users/*.ini` | `src/model/<source>/filesReader/entities-file.iterable.php` | `cms_rank_entities` | `UID` |
| Guilds | `DATABASE_FOLDER/Guilds/*.txt` | `src/model/<source>/filesReader/guilds-file.iterable.php` | `cms_rank_guilds` | `ID` |
| Arena | `DATABASE_FOLDER/Arena.ini` | `src/model/<source>/filesReader/arena-file.iterable.php` | `cms_rank_arena` | `UID` |

Each generation uses batches of 500 rows. It upserts rows, records their keys
in a connection-local temporary `MEMORY` table, and deletes target rows whose
keys were not seen. All datasets in `generateAll` share one CMS transaction.
The ranking tables must be InnoDB; run `installation/cms-ranking-update.sql`
once on existing installations.

### Imported Fields

`CmsRankingModel` defines the target fields in its private column lists:

- Players: `Name`, `UID`, `ConquerPoints`, `Level`, `VIPLevel`, `HairStyle`,
  `Class`, `Money`, `Body`, `Face`, `Spouse`, `MoneySave`, `GuildID`,
  `GuildRank`, `RacePoints`, `Donation`, `PKPoints`, `Hitpoints`, `Vitality`,
  `Mana`, `Spirit`, `Strength`, `Agility`.
- Guilds: `ID`, `Name`, `LaderUID`, `LeaderName`, `SilverFund`,
  `ConquerPointFund`.
- Arena: `UID`, `Name`, `Level`, `Class`, `Mesh`, `ArenaPoints`,
  `CurrentHonor`, `HistoryHonor`, `TodayBattles`, `TodayWin`, `TotalLose`,
  `TotalWin`, `LastSeasonArenaPoints`, `LastSeasonWin`, `LastSeasonLose`,
  `LastSeasonRank`.

The database column is historically misspelled `LaderUID`; the reader/model
source key is `LeaderUID` and is resolved from `LeaderName` when needed.

The entity reader normalizes these common source aliases before import:

| Stored key | Fallback source key |
|---|---|
| `MoneySave` | `WHMoney` |
| `Donation` | `DonationNobility` |
| `VIPLevel` | `VipLevel` |
| `PKPoints` | `PkPoints` |
| `HairStyle` | `Haire` |

The stat keys `Hitpoints`, `Vitality`, `Mana`, `Spirit`, `Strength`, and
`Agility` are read from the entity `Character` section and stored unchanged.
The standard stream readers use `MinHitPoints`, `MinMana`, and `Vitaliti` as
fallbacks for `Hitpoints`, `Mana`, and `Vitality` respectively.

Entity and spouse names are converted from Windows-1252 to UTF-8. Guild files
skip their first line and read slash-separated values from the second line.
Arena files skip their first line and read slash-separated values in the same
order as the Arena field list above.

## Changing Imported Data

Use this order when adding or changing an imported field:

1. Update the reader in `src/model/<source>/filesReader/` so every emitted row
   contains one normalized key.
2. Add the key to the corresponding column list in
   `src/model/cms/cmsRanking.model.php`.
3. Add the database column to the ranking table in `installation/cms.sql`.
4. Add a separate `ALTER TABLE` statement to a migration file for existing
   installations; do not rerun the complete `cms.sql` on production.
5. If the website must display the field, add it to the relevant select mapper
   or ranking query in `CmsRankingModel` and verify the ranking component.
6. Update the field list in this document and run the applicable CLI command.

For source-specific input formats, update only the active source reader first.
If all stream variants share the same format, apply the equivalent mapping to
each source reader, because `MODELS_SOURCE` selects one directory at runtime.

To stop importing a field, remove it from the model column list and upsert
statement first, then plan a separate schema cleanup only if old stored values
must be removed. Do not use `TRUNCATE` for normal ranking refreshes.

## Changing Datasets Or Commands

To alter an existing ranking dataset, edit the matching `generate*` function in
`src/core/cli/ranking.php`:

- Keep `start<Dataset>Sync()` before reading the iterator.
- Accumulate rows and call the matching `upsert*()` method in batches.
- Call the matching `removeMissing*()` method only after the iterator finishes.
- Keep failures inside `runRankingGeneration()` so the transaction rolls back.

To add a new synchronized dataset, add a target table/key, a seen-table
definition, `upsert*()` and `removeMissing*()` methods to `CmsRankingModel`,
then add a generator function and command documentation. Use
`acquireRankingLock()` and `cleanupRankingSync()` through the existing wrapper;
do not use `LOCK TABLES`, because normal reads must remain available.

## Related Web APIs

The CLI importer and website ranking reader are separate:

- `CmsRankingModel` contains the shared CMS queries and CLI import helpers.
- `src/model/<source>/ranking.model.php` can extend `CmsRankingModel` to change
  website ranking behavior for a source.
- `src/pages/public/ranking/ranking.controller.php` validates ranking keys and
  determines available columns.
- `src/pages/public/ranking/rankings/*.php` selects the ranking endpoint used
  by each page.
- `AVAILABLE_RANKINGS` and `IGNORE_RANKING_COLUMNS` in the active config control
  visible ranking options and optional columns.

The public model methods are documented in [`models.md`](models.md).
