# PLAN-LAZARUS-PORT — HC Launcher: Delphi → Lazarus/FPC One-Way Migration

## Context

HC Launcher is a single-form Delphi VCL application (1422 lines in `uMain1.pas`) that builds command-line arguments for the Harpoon Classic Game Engine. Its UI is built entirely on TMS Component Pack controls (89 instances, ~2,258 TMS-specific DFM properties) which are commercial and Delphi-only.

**Why:** Lazarus/FPC is the target toolchain for the Stratsims ecosystem going forward. Delphi will be dropped after this migration. The port also provides an opportunity to extract the tangled command-line building logic into a testable, UI-independent class.

**Outcome:** A Lazarus project using standard LCL controls, with the command-line builder extracted into its own interface+class, covered by FPCUnit tests.

---

## Decisions

- Replace all TMS controls with standard LCL equivalents (no TMS FNC)
- Drop ExeInfo — use existing `CICGetAppVersionStr` from `../shared/pascal/uCICVersionInformation.pas`
- Drop SE and Other tabs (empty stubs — will return later)
- One-way migration, no dual-codebase IFDEFs
- Extract `BuildCommandLineGE` into `uHCLCmdLineBuilder.pas` with interface + implementation

---

## Target File Layout

```
HCLauncher/
  Launcher.lpi                    ← new Lazarus project
  Launcher.lpr                    ← new program entry point
  Launcher.res                    ← reused
  uMain1.pas + uMain1.lfm        ← rewritten (LCL, no TMS)
  uHCLCmdLineBuilder.pas         ← NEW: extracted builder
  uHCLProcessUtils.pas           ← NEW: replaces FmxUtils.pas (license-incompatible)
  tests/
    HCLauncherTests.lpi + .lpr   ← FPCUnit test runner
    test_CmdLineBuilder.pas      ← unit tests for builder
    test_Settings.pas            ← INI round-trip tests
```

Legacy files (`Launcher.dpr`, `Launcher.dproj`, `uMain1.dfm`, `FmxUtils.pas`) stay in SVN history as reference. `FmxUtils.pas` is NOT carried forward due to licensing restrictions.

---

## Phase 1 — Infrastructure: Project Files + Utilities

### 1.1 Create `Launcher.lpr` and `Launcher.lpi`

Program entry point using LCL `Interfaces` unit. `.lpi` configured with:
- Unit search path: `../shared/pascal`
- Output dir: `lib/$(TargetCPU)-$(TargetOS)` (add to `.svnignore`)
- Build modes: Release + Debug
- `GraphicApplication = True`

### 1.2 Create `uHCLProcessUtils.pas` (replaces `FmxUtils.pas`)

**File:** `C:\Prog\Projects\StratsimsOSS\HCLauncher\uHCLProcessUtils.pas` (new)

`FmxUtils.pas` has a license incompatible with our proprietary requirements and is dropped entirely. The only function HC Launcher actually calls from it is `ExecuteFile` (a thin `ShellExecute` wrapper used in `btnLaunchGEClick`). We rewrite only what we need:

```pascal
unit uHCLProcessUtils;

{$mode objfpc}{$H+}

/// <summary>
/// Process launch utilities for HC Launcher.
/// Replaces the subset of FmxUtils.pas that HC Launcher used.
/// </summary>
```

**Functions to implement:**

- `ExecuteFile(const AFileName, AParams, ADefaultDir: String; AShowCmd: Integer): Boolean`
  - `{$IFDEF MSWINDOWS}`: Uses `ShellExecute` from `ShellAPI` (same logic as original, ~5 lines)
  - `{$ELSE}`: Uses `TProcess` from FCL `Process` unit — sets `Executable`, `Parameters`, `CurrentDirectory`, calls `Execute`
  - Returns `Boolean` (success/failure) instead of `THandle` — the original return value was never checked by the caller

No other FmxUtils functions (`CopyFile`, `MoveFile`, `GetFileSize`, `FileDateTime`, `HasAttr`, `ExecuteFileEx`, `GetShortName`, `IsWow64`) are used by HC Launcher and are not carried forward. If needed later, they can be implemented fresh or sourced from `uCICExecuteProgram` in the shared units.

### 1.3 Update `uCICVersionInformation.pas`

**File:** `C:\Prog\Projects\StratsimsOSS\shared\pascal\uCICVersionInformation.pas`

- Add `{$IFDEF FPC}{$MODE DELPHI}{$ENDIF}` before `unit` keyword
- Change `uses` from `System.Types, System.SysUtils` to plain `SysUtils` (Delphi accepts both)
- Guard implementation body with `{$IFDEF MSWINDOWS}` — return `''` on other platforms
- **Risk:** Shared with sibling Delphi projects. Verify a sibling build still compiles after this change.

### 1.4 Create stub `uMain1.pas` + `uMain1.lfm`

Minimal `TfrmMain1 = class(TForm)` that compiles and opens a blank window.

**Acceptance:** `lazbuild Launcher.lpi` exits 0. App opens blank window and closes cleanly. No TMS units anywhere.

---

## Phase 2 — Extract `uHCLCmdLineBuilder`

**File:** `C:\Prog\Projects\StratsimsOSS\HCLauncher\uHCLCmdLineBuilder.pas` (new)

This is the most important architectural change. Done before the form rewrite so tests serve as the specification.

### 2.1 `THCGECmdLineInputs` record

Flat record capturing every value that `BuildCommandLineGE` currently reads from controls. ~50 fields organized by flag group:

| Group | Fields | Flag |
|-------|--------|------|
| Log file | `LogFN: Boolean`, `LogFNPath: String` | `-f` |
| Logging | `LoggingEnabled`, `LogAll`, 18× individual log booleans | `-l` |
| Debug | `DebugEnabled`, `DebugI32501` | `-I` |
| Window hide | `WindowHideEnabled`, 5× sub-booleans | `-h` |
| Switches | `IgnoreReg`, `AutoPlay`, `SubAIDisable`, `Eff115Old` | `-i`, `-a`, `-x`, `-e` |
| Save interval | `SavIntEnabled`, `SavIntText` | `-t` |
| Iterative save | `IterSave` | `-s` |
| Iterative save GT | `IterSaveGT`, `Interval`, `Saves`, `FNEnabled`, `FNPath` | `-S`, `-k` |
| Message log | `LogMLFNEnabled`, `LogMLFNPath` | `-m` |
| Battle set | `BSByNum`, `BSNumIndex`, `BSByFile`, `BSFilePath` | `-b` |
| Scenario | `ScenByNum`, `ScenNumIndex`, `ScenByFile`, `ScenFilePath` | `-c` |
| Save file | `SavFNEnabled`, `SavFNPath` | `-v` |
| CD databases | `CDBresFNEnabled`/`Path`, `CDBrsrFNEnabled`/`Path` | `-d`, `-r` |
| Game options | `GameOptsEnabled`, `GOUseSideChecklist`, `GOSideIndex`, `GOSidesBitmask`, 8× option indices | `-g` |
| CPU affinity | `AffinityEnabled`, `AffinityIndex` | `-p` |
| Priority | `PriorityEnabled`, `PriorityIndex` | `-u` |

### 2.2 `IHCLCmdLineBuilder` interface + `THCLCmdLineBuilder` class

```pascal
IHCLCmdLineBuilder = interface
  function BuildCommandLine(const AInputs: THCGECmdLineInputs): String;
end;
```

Implementation is a direct extraction of the ~230-line `BuildCommandLineGE`, with these improvements:

**Constant arrays replace case statements:**
- `BSIndexToID: array[0..31]` — maps combo index to battle-set ID (skips ID 5; index 31→35)
- `AffinityMask: array[0..3]` = (1, 2, 4, 8)
- Priority = `Index - 2`
- Scenario ID = `Index + 1`

**Locale bug fix:** Replace `IsFloat(S)` (uses locale-dependent `StrToFloat`) with `TryStrToFloat(S, Val, InvariantFS)` where `InvariantFS.DecimalSeparator = '.'`.

**No UI dependency:** Only `uses SysUtils, Math`.

### 2.3 Tests: `tests/test_CmdLineBuilder.pas`

FPCUnit test suite (~35 test methods). Key coverage:

- Empty inputs → empty string
- Each flag independently (log file, individual `-l` flags, `-l all` short-circuit, debug, window hide, switches, intervals, battle sets, scenarios, game options, affinity, priority)
- Battle-set index mapping edge cases (index 4→ID 6 skip, index 31→ID 35)
- `-S` format verification: `'-S' + Format('%0.5d', [100]) + Format('%0.4d ', [5])` = `'-S001000005 '`
- `-g` 9-digit string with both old-GE checklist bitmask and new-GE combobox modes
- Locale fix: `'1,5'` rejected, `'1.5'` accepted for `-t`
- Flag ordering matches original (f, l, I, h, i, a, t, s, S, m, x, e, b, c, v, d, r, g, p, u)
- Full realistic input → verified complete string

### 2.4 Test runner: `tests/HCLauncherTests.lpi` + `.lpr`

Console FPCUnit runner (pattern: `../shared/pascal/tests/`).

**Acceptance:** `lazbuild tests/HCLauncherTests.lpi && tests/HCLauncherTests` — all builder tests pass. `uHCLCmdLineBuilder` has zero LCL imports.

---

## Phase 3 — Rewrite Form (`uMain1.pas` + `uMain1.lfm`)

### 3.1 Build `.lfm` from scratch in Lazarus IDE

Do NOT convert the `.dfm` — it's ~8700 lines of TMS properties. Use the original as a layout reference only.

**TMS → LCL control map:**

| TMS (count) | LCL Replacement | API change |
|-------------|----------------|------------|
| TAdvSmoothTabPager + TabPage (1+3→1+1) | TPageControl + TTabSheet | Only GE tab |
| TAdvSmoothComboBox (13) | TComboBox | `.SelectedItemIndex` → `.ItemIndex`; `OnItemSelected` → `OnChange` |
| THTMLCheckBox (46) | TCheckBox | Same `.Checked` API |
| THTMLCheckList (1) | TCheckListBox | Same `.Checked[i]` API |
| TAdvFileNameEdit (8) | TEdit + TButton + TOpenDialog | `.FileName` → `.Text`; browse via dialog |
| TAdvDirectoryEdit (1) | TEdit + TButton + TSelectDirectoryDialog | Same pattern |
| TAdvEdit (2) | TEdit | Direct |
| TAdvSpinEdit (2) | TSpinEdit | Same `.Value` API |
| TAdvSmoothPanel (5) | TGroupBox | Visual container |
| TAdvSmoothButton (1) | TButton | Direct |
| THTMLabel (4) | TLabel | Strip HTML from captions |
| TAdvLUEdit (1) | TEdit | Direct |
| TAdvOfficeStatusBar (1) | TStatusBar | Direct |
| TExeInfo (1) | Dropped | Use `CICGetAppVersionStr` |

### 3.2 Rewrite `uMain1.pas`

```pascal
uses
  LCLIntf, LCLType, Classes, SysUtils, Forms, Controls, StdCtrls, ComCtrls,
  Dialogs, ExtCtrls, Spin, CheckLst, FileUtil, Registry, IniFiles, Math,
  uHCLProcessUtils, uCICVersionInformation, uHCLCmdLineBuilder;
```

**Key changes:**
- `FBuilder: IHCLCmdLineBuilder` created in `FormCreate`, freed automatically (interface ref-counted)
- New `SnapshotInputs: THCGECmdLineInputs` method reads all LCL controls into the record
- `BuildCommandLineGE` calls `FBuilder.BuildCommandLine(SnapshotInputs)` and sets `edtCmdLineGE.Text`
- `FormCreate`: version from `CICGetAppVersionStr(Application.ExeName)`, no ExeInfo
- `FormDestroy`: drop OLE `RevokeDragDrop` calls (no OLE in LCL)
- `SettingsLoad`/`SettingsSave`: same INI keys, `.SelectedItemIndex` → `.ItemIndex`, `.FileName` → `.Text`
- Version-gating in `edtGENameChange`: unchanged logic, same `CICGetAppVersionStr` + string comparisons
- Mutual-exclusion logic (BS num/file, Scen num/file, debug/WH cascading) stays on the form

### 3.3 Settings extraction for testability

Extract `SettingsLoad`/`SettingsSave` to accept a record + `TIniFile`, making them testable without a form instance. Form methods become thin wrappers. Complete `tests/test_Settings.pas` with INI round-trip tests.

**Acceptance:**
- `lazbuild Launcher.lpi` in Release and Debug
- App loads existing `Launcher.ini` correctly
- All controls functional, command-line preview updates live
- Version-gated panels show/hide correctly
- Launch button executes GE with correct arguments
- All tests pass including settings round-trip

---

## Phase 4 — Test Completion + Housekeeping

- Verify locale bug fix on comma-decimal system
- Ensure 100% branch coverage of `THCLCmdLineBuilder.BuildCommandLine`
- Tag last Delphi-compilable SVN revision
- Add `lib/`, `*.exe` to `.svnignore`
- Update `CLAUDE.md` build instructions (Lazarus replaces RAD Studio)

**Acceptance:** All tests pass. `CLAUDE.md` is accurate.

---

## Phase 5 — Future Work (out of scope)

- Restore SE and Other tabs
- Linux: non-Windows `uCICVersionInformation`, TProcess paths in FmxUtils
- LCL drag-drop for file/directory edits
- High-DPI testing at 150%/200%
- SE command-line builder (second `IHCLCmdLineBuilder` implementation)

---

## Risk Register

| Risk | Mitigation |
|------|-----------|
| `uCICVersionInformation` change breaks sibling Delphi builds | `{$IFDEF FPC}{$MODE DELPHI}` + plain `SysUtils` are Delphi-transparent. Test a sibling build. |
| Form layout is significant manual effort | Accepted cost. Use `.dfm` as pixel reference. |
| Locale bug fix changes behavior for `.` decimal users | `TryStrToFloat` with `.` separator accepts everything that was previously valid. Only `,` values (which silently misbehaved) are now correctly rejected. |
| `GetFileVersionInfo` not in FPC's Windows unit | Available since FPC 3.2. Confirm version. |

---

## Dependency Order

```
Phase 1 (project files + FmxUtils + uCICVersionInformation)
    ↓
Phase 2 (uHCLCmdLineBuilder + tests)  ← builder is the spec for Phase 3
    ↓
Phase 3 (uMain1.pas rewrite + settings tests)
    ↓
Phase 4 (test completion + housekeeping)
```
