Phase 0: scaffold modern RIOJoy solution + plan
Modernization of the legacy vJoy-based RIO cockpit interface for Win10/11, removing the vJoy dependency in favor of a custom VHF/UMDF HID driver, rewritten in C#/.NET 8 as a background tray app with per-game profiles. - Reorganize: legacy C++ -> legacy/, cockpit art -> docs/reference/ - RioJoy.sln: src/RioJoy.Core (lib) + src/RioJoy.Tray (tray app), net8.0-windows x64 - driver/ placeholder for the RioGamepad WDK driver - docs/PLAN.md (7-phase plan; profiles + serial-yield model) - docs/PROTOCOL.md (RIO wire format + iRIO input-map reference) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
# RIOJoy — modernization plan
|
||||
|
||||
Modernize the cockpit RIO interface app for Windows 10/11, **removing the vJoy
|
||||
dependency** and replacing it with a custom virtual HID device, rewritten in
|
||||
C#/.NET as a background tray app with **per-game profiles**.
|
||||
|
||||
The legacy app is preserved under [`legacy/`](../legacy/) as the behavioral
|
||||
reference. The RIO wire format and input map are documented in
|
||||
[PROTOCOL.md](PROTOCOL.md).
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
The cockpits run two native games (**Firestorm**, **Red Planet**) that talk to
|
||||
the RIO hardware **directly** and never use this app. RIOJoy exists to **broaden
|
||||
which other games can run in the cockpits**: arbitrary games don't know about the
|
||||
cockpit's extra hardware (5 analog axes, 96 lighted buttons, the plasma/VFD
|
||||
display, the labeled wallpaper), so RIOJoy bridges the RIO to whatever input
|
||||
those games *do* understand — joystick, keyboard, and mouse — and drives the
|
||||
cockpit's outputs on their behalf.
|
||||
|
||||
---
|
||||
|
||||
## Target architecture
|
||||
|
||||
```
|
||||
┌─────────────────────── C# / .NET 8 tray app (x64) ───────────────────────┐
|
||||
│ Serial (RIO protocol) → Input mapper (profile) → Output router │
|
||||
│ COM port, 9600 8N1 72 inputs + keypads ├─ Keyboard/Mouse: SendInput (P/Invoke)
|
||||
│ packet parse + ACK/NAK decode iRIO bitfield ├─ Joystick: HID report → DeviceIoControl ↓
|
||||
│ analog poll + recovery axis calibration └─ Lamps: LampRequest back over serial
|
||||
│ Plasma/VFD on 2nd COM · Profiles + auto-switch + tray UI + logging │
|
||||
└───────────────────────────────────────────────┬───────────────────────────┘
|
||||
│ IOCTL (input report bytes)
|
||||
┌─────────────────────────────────────────────────▼──────────────────────────┐
|
||||
│ RioGamepad.sys — KMDF + VHF (vhf.sys) virtual HID device │
|
||||
│ Report descriptor: X,Y,Z,Rx,Ry,Rz (16-bit) · 1 hat · 96 buttons │
|
||||
│ Control device + custom IOCTL → VhfReadReportSubmit() → Windows sees │
|
||||
│ a HID gamepad │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Decisions (confirmed)
|
||||
|
||||
- **Virtual joystick:** custom **VHF/UMDF HID driver** (full fidelity: 6 axes,
|
||||
96 buttons, 1 hat — exactly the legacy vJoy layout). Not ViGEm (can't hold 96
|
||||
buttons).
|
||||
- **Stack:** C# / **.NET 8** (LTS), x64. Driver is C (WDK), separate toolchain.
|
||||
- **Form:** background **tray app** (NotifyIcon), auto-start with Windows.
|
||||
- **Targets:** Windows 10/11 **x64 only**. The legacy x86 / WinXP targets are
|
||||
dropped.
|
||||
|
||||
### What ports over vs. what's new
|
||||
|
||||
| Concern | Legacy | Modern |
|
||||
|---|---|---|
|
||||
| Serial + RIO protocol | overlapped I/O + watch thread | `SerialPort` + async loop; faithful port of the packet state machine |
|
||||
| Input decode | `iRIO[]` bitfield + `Press_V2` | same semantics, ported to C# |
|
||||
| Keyboard/mouse | `SendInput` scancode | `SendInput` via P/Invoke (≈verbatim) |
|
||||
| Joystick | vJoy `SetAxis/SetBtn/SetDiscPov` | **HID report → IOCTL → RioGamepad.sys** |
|
||||
| Lamps | `LampRequest` over serial | same |
|
||||
| Axis calibration | `UpdateJoystick/Throttle/Padal` | ported math |
|
||||
| Plasma display | `CPlasma` (COM2) | ported; content per-profile |
|
||||
| Config | SimpleIni, single file, hard-coded COM1 | profile library (JSON), configurable ports; importer for legacy `RIO.ini` |
|
||||
|
||||
---
|
||||
|
||||
## Profiles (the core abstraction)
|
||||
|
||||
A **profile** fully describes how the cockpit behaves for **one non-native
|
||||
game**. Everything is per-profile, not global:
|
||||
|
||||
- Button/keypad mapping (the decoded `iRIO` table for this game)
|
||||
- Axis calibration, curves, and invert flags
|
||||
- Lamp behavior
|
||||
- Plasma/VFD content (or "off")
|
||||
- Cockpit overlay labels + the generated wallpaper (Phase 7)
|
||||
- Display/resolution targets
|
||||
|
||||
Profiles **never** describe the native games — those are the "hands-off" case.
|
||||
|
||||
### Serial-port yield & the auto-switch state machine
|
||||
|
||||
Because the native games own the RIO's COM port directly, RIOJoy must yield it.
|
||||
A process/window watcher drives three states:
|
||||
|
||||
| Detected running app | RIOJoy behavior |
|
||||
|---|---|
|
||||
| **Native game** (Firestorm, Red Planet) | **Release COM port, go fully dormant** — no serial, no HID, no overlay |
|
||||
| **Supported non-native game** | Acquire port, load that game's profile, drive HID / keyboard / mouse / lamps / plasma / wallpaper |
|
||||
| **Nothing / desktop** | Idle — port released or a configurable neutral default |
|
||||
|
||||
Config therefore holds: the per-game profile library, a **list of native games
|
||||
to yield to**, and the executable→profile match rules. Manual override from the
|
||||
tray menu is always available.
|
||||
|
||||
---
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0 — Repo & scaffold ✅ (this commit)
|
||||
- `git init`, remote `https://gitea.mysticmachines.com/VWE/riovjoy2.git`.
|
||||
- Legacy C++ moved to `legacy/`; cockpit art to `docs/reference/`.
|
||||
- Solution `RioJoy.sln` with `src/RioJoy.Core` (lib) + `src/RioJoy.Tray`
|
||||
(tray app); `driver/` placeholder for the WDK project.
|
||||
- This plan + [PROTOCOL.md](PROTOCOL.md).
|
||||
|
||||
### Phase 1 — Virtual HID driver (highest risk; do first)
|
||||
- KMDF + VHF virtual HID gamepad: report descriptor = 6×16-bit axes, 1 hat,
|
||||
96 buttons.
|
||||
- Control device + custom IOCTL → `VhfReadReportSubmit`.
|
||||
- Test harness (throwaway C#) wiggles axes/buttons; verify in `joy.cpl`.
|
||||
- Test-signing setup for the cabinets.
|
||||
|
||||
### Phase 2 — Serial + RIO protocol core (`RioJoy.Core`)
|
||||
- `SerialPort` wrapper; packet parser/builder (length table, 7-bit checksum,
|
||||
ACK/NAK, framing resync).
|
||||
- Analog poll timer + >5 s reset-recovery.
|
||||
- **Clean COM-port acquire/release** (foundation for serial yield).
|
||||
- Verify against hardware: version reply, check reply, analog stream.
|
||||
|
||||
### Phase 3 — Input mapping + output routing
|
||||
- Port `iRIO` decode and routing precedence (keyboard/mouse/joy/hat/RIO-command).
|
||||
- Keyboard/mouse via `SendInput` P/Invoke; lamp feedback over serial.
|
||||
- HID-report feeder → the Phase 1 driver via `DeviceIoControl`.
|
||||
|
||||
### Phase 4 — Axis calibration + plasma display
|
||||
- Port `UpdateJoystick/Throttle/Padal` math (deadzones, ratchet, rudder).
|
||||
- Port the `CPlasma` ESC command set on the secondary COM port.
|
||||
|
||||
### Phase 5 — Tray app + profiles
|
||||
- NotifyIcon + menu mirroring the legacy console menu (reset/recalibrate axes,
|
||||
version/status, toggle raw-axis & poll-rate readouts, quit) + status/log window.
|
||||
- **Profile library**, manual selection, and the **three-state auto-switch
|
||||
watcher** (incl. native-game yield).
|
||||
- Config persistence; auto-start.
|
||||
|
||||
### Phase 6 — Packaging / signing / deploy
|
||||
- Driver install via `pnputil`; app installer; test-signing script.
|
||||
- Cabinet deployment doc. (Production option: attestation signing.)
|
||||
|
||||
### Phase 7 — Profile/mapping editor + cockpit overlay generator
|
||||
Replaces the legacy Google-Sheet → `.data` → GIMP → Script-Fu pipeline
|
||||
(see [`docs/reference/customBackground/`](reference/customBackground/)).
|
||||
- **Mapping editor:** per-button UI to set action + label + lamp, no hex
|
||||
bit-twiddling; clone-from-existing profile.
|
||||
- **Overlay generator:** render labels into named regions over a base cockpit
|
||||
image using **SkiaSharp**, porting the auto-fit/justification logic from
|
||||
`sg-goobie-MFD.scm`; export the per-profile wallpaper and re-apply via
|
||||
`SystemParametersInfo`.
|
||||
- **Region authoring:** extract the label rectangles from `riojoy.xcf` into a
|
||||
`regions.json`; in-app box editor for future tweaks.
|
||||
- The unified profile JSON supersedes both `RIO.ini` and the Google Sheet, with
|
||||
importers for each.
|
||||
- To confirm at Phase 7: target wallpaper resolution(s); static wallpaper vs.
|
||||
live overlay (e.g. lit-button highlighting mirroring lamp state).
|
||||
|
||||
---
|
||||
|
||||
## Open items / risks
|
||||
|
||||
- **Driver signing** is the main friction point. Test-signing is fine for owned
|
||||
cabinets; redistribution needs attestation signing (EV cert + Partner Center).
|
||||
- **Input injection vs. session:** `SendInput` targets the interactive session —
|
||||
fine for a tray app, which is why a Windows service was rejected.
|
||||
- **Legacy quirks to decide on** (see PROTOCOL.md ⚠️ notes): disabled inbound
|
||||
checksum verification; odd mouse-move deltas.
|
||||
@@ -0,0 +1,230 @@
|
||||
# RIO serial protocol & `iRIO` input map
|
||||
|
||||
Reverse-engineered from the legacy implementation in
|
||||
[`legacy/riovjoy2.cpp`](../legacy/riovjoy2.cpp). This is the authoritative
|
||||
reference for the Phase 2 C# port. Line references point into the legacy file.
|
||||
|
||||
> ⚠️ Where the legacy code looks buggy, this document describes **what it
|
||||
> actually does** and flags the suspect behavior. Port the documented behavior,
|
||||
> then decide intentionally whether to fix the flagged items.
|
||||
|
||||
---
|
||||
|
||||
## 1. Physical link
|
||||
|
||||
- **RS-232, 9600 baud, 8 data bits, no parity, 1 stop bit (8N1).**
|
||||
- The RIO is on one COM port (legacy hard-codes **COM1**, [`OpenConnection`](../legacy/riovjoy2.cpp#L747)).
|
||||
- An optional **plasma / VFD text display** is on a second COM port (legacy
|
||||
hard-codes **COM2**, [`CPlasma`](../legacy/riovjoy2.cpp#L41)).
|
||||
- DTR is pulsed on open (SETDTR, 50 ms, CLRDTR) as a board reset/handshake.
|
||||
- Both ports must be **configurable** in the modern app (no hard-coded COM
|
||||
numbers).
|
||||
|
||||
### Serial-port ownership (critical)
|
||||
|
||||
The native games (Firestorm, Red Planet) talk to the RIO **directly over this
|
||||
same port**. Only one process can own it at a time, so the modern app must
|
||||
**release the COM port and go dormant whenever a native game is running**, and
|
||||
re-acquire it when a supported non-native game is active. See PLAN.md §Profiles.
|
||||
|
||||
---
|
||||
|
||||
## 2. Framing
|
||||
|
||||
Every message is a packet:
|
||||
|
||||
```
|
||||
[ command byte ] [ payload byte ... ] [ checksum byte ]
|
||||
0x80-0x8C high bit clear 7-bit checksum
|
||||
```
|
||||
|
||||
- **Command byte:** high bit set (`0x80`+). Value identifies the message; see
|
||||
the command table. The valid range is `0x80 .. 0x80 + N` where `N` is the
|
||||
command count.
|
||||
- **Payload length** is fixed per command (the length table below), excluding
|
||||
the command byte and checksum byte.
|
||||
- **Payload bytes** always have the high bit **clear** (7-bit data). Receiving a
|
||||
byte with the high bit set mid-packet means a framing error → **abort the
|
||||
current packet and resync** ([`ReadCommBlock`](../legacy/riovjoy2.cpp#L871)).
|
||||
- **Checksum byte:** `(sum of (b & 0x7F) for each byte in command+payload) & 0x7F`
|
||||
([build: `SendCommand`](../legacy/riovjoy2.cpp#L1217),
|
||||
[verify: `ReadCommBlock`](../legacy/riovjoy2.cpp#L884)).
|
||||
|
||||
### Control characters (single bytes, outside packet framing)
|
||||
|
||||
| Name | Byte | Meaning |
|
||||
|-------------|------|---------|
|
||||
| `ACK` | 0xFC | Packet accepted |
|
||||
| `NAK` | 0xFD | Packet rejected (resend) |
|
||||
| `RESTART` | 0xFE | Restart / also used as an in-payload "invalid" sentinel |
|
||||
| `IDLE` | 0xFF | Idle |
|
||||
|
||||
After a valid received packet, the PC replies `ACK` (when its output queue is
|
||||
empty). For **button** packets with a bad checksum it replies `NAK`; for other
|
||||
packet types with a bad checksum it still `ACK`s.
|
||||
|
||||
> ⚠️ The legacy receive path force-accepts every packet regardless of checksum
|
||||
> (`static bool s_bool = true;` at [riovjoy2.cpp#L887](../legacy/riovjoy2.cpp#L887)),
|
||||
> so checksum verification is effectively disabled inbound. Decide whether to
|
||||
> enable real verification in the port.
|
||||
|
||||
---
|
||||
|
||||
## 3. Command table
|
||||
|
||||
Enum base `0x80` ([`RIOCommand`](../legacy/riovjoy2.cpp#L189)); payload lengths
|
||||
from [`g_baRIOLengthsA`](../legacy/riovjoy2.cpp#L171).
|
||||
|
||||
| Code | Name | Dir | Payload len | Payload |
|
||||
|------|-------------------|---------|-------------|---------|
|
||||
| 0x80 | CheckRequest | PC→RIO | 0 | — |
|
||||
| 0x81 | VersionRequest | PC→RIO | 0 | — |
|
||||
| 0x82 | AnalogRequest | PC→RIO | 0 | — |
|
||||
| 0x83 | ResetRequest | PC→RIO | 1 | `target` (see §Reset) |
|
||||
| 0x84 | LampRequest | PC→RIO | 2 | `lamp#`, `state` |
|
||||
| 0x85 | CheckReply | RIO→PC | 2 | `statusType`, `number` |
|
||||
| 0x86 | VersionReply | RIO→PC | 2 | `major`, `minor` |
|
||||
| 0x87 | AnalogReply | RIO→PC | 10 | 5 axes × (low, high) |
|
||||
| 0x88 | ButtonPressed | RIO→PC | 1 | `index` (0x00–0x47) |
|
||||
| 0x89 | ButtonReleased | RIO→PC | 1 | `index` (0x00–0x47) |
|
||||
| 0x8A | KeyPressed | RIO→PC | 2 | `pad`, `index` |
|
||||
| 0x8B | KeyReleased | RIO→PC | 2 | `pad`, `index` |
|
||||
| 0x8C | TestModeChange | RIO→PC | 1 | `mode` (0 = exit) |
|
||||
|
||||
### Reset targets (ResetRequest payload)
|
||||
|
||||
`0` = general/all, `1` = throttle, `2` = left pedal, `3` = right pedal,
|
||||
`4` = vertical joystick (Y), `5` = horizontal joystick (X)
|
||||
([`ResetThrottle`](../legacy/riovjoy2.cpp#L1273) etc.).
|
||||
|
||||
### Lamp state byte (LampRequest)
|
||||
|
||||
Composed of flash + two brightness fields
|
||||
([`LampState`](../legacy/riovjoy2.cpp#L213)):
|
||||
|
||||
- Flash: `solid=0, flashSlow=1, flashMed=2, flashFast=3`
|
||||
- Field 1: `Off=0x00, Dim=0x04, Bright=0x0C`
|
||||
- Field 2: `Off=0x00, Dim=0x10, Bright=0x30`
|
||||
- Common combos: `SolidOff=0x00`, `SolidDim=0x14`, `SolidBright=0x3C`
|
||||
|
||||
---
|
||||
|
||||
## 4. Analog values
|
||||
|
||||
`AnalogReply` carries 5 axes, each 2 bytes (low then high), in this order:
|
||||
|
||||
1. **Throttle**, 2. **LeftPedal**, 3. **RightPedal**, 4. **JoystickY**, 5. **JoystickX**
|
||||
([`AnalogEvent`](../legacy/riovjoy2.cpp#L1127)).
|
||||
|
||||
Each axis is a **14-bit signed** value packed as two 7-bit bytes
|
||||
([`CombinePair`](../legacy/riovjoy2.cpp#L1116)):
|
||||
|
||||
```
|
||||
raw = (low & 0x7F) | (high << 7); // 14 bits
|
||||
if (raw & 0x2000) raw |= ~0x3FFF; // sign-extend bit 13
|
||||
```
|
||||
|
||||
If any payload byte equals `0xFE`, the reply is treated as invalid and ignored.
|
||||
|
||||
### Polling & recovery
|
||||
|
||||
The legacy watch thread requests an analog update on a ~**55 ms** timeout
|
||||
([`CommWatchProc`](../legacy/riovjoy2.cpp#L1069)). If **>5 s** elapse with no
|
||||
`AnalogReply`, it issues a general reset to recover
|
||||
([riovjoy2.cpp#L1096](../legacy/riovjoy2.cpp#L1096)).
|
||||
|
||||
### Axis → virtual-device mapping (range 0..32766, center 16383)
|
||||
|
||||
Calibration/deadzone math lives in
|
||||
[`UpdateJoystick`](../legacy/riovjoy2.cpp#L1722),
|
||||
[`UpdateThrottle`](../legacy/riovjoy2.cpp#L1637),
|
||||
[`UpdatePadal`](../legacy/riovjoy2.cpp#L1504):
|
||||
|
||||
| RIO axis | Virtual axis | Notes |
|
||||
|---------------|--------------|-------|
|
||||
| JoystickX | X | auto-ranging rate from observed min/max, ±5 deadzone |
|
||||
| JoystickY | Y | same |
|
||||
| Throttle | Z | range ±800, deadzone 50, ratchet via `g_ThrottleResult` |
|
||||
| LeftPedal | Rx | range ±500, deadzone 10 (only when `enableZR` off) |
|
||||
| RightPedal | Ry | same |
|
||||
| (computed) | Rz | rudder = `16383 - leftPedal/2 + rightPedal/2` (when `enableZR` on) |
|
||||
|
||||
Per-axis invert flags (`invertX/Y/Z/XR/YR/ZR`) and `enableZR` come from config.
|
||||
|
||||
---
|
||||
|
||||
## 5. Digital inputs → the `iRIO` map
|
||||
|
||||
The RIO reports button/keypad events by **address**; the app translates each
|
||||
address to a Windows action via a 112-entry table `iRIO[]`
|
||||
([load](../legacy/riovjoy2.cpp#L352), [decode `Press_V2`](../legacy/riovjoy2.cpp#L1944)).
|
||||
|
||||
### Address space (index into `iRIO`)
|
||||
|
||||
| Range | Source |
|
||||
|------------------|--------|
|
||||
| `0x00`–`0x47` (0–71) | the 72 digital button inputs (`ButtonPressed/Released` index) |
|
||||
| `0x50`–`0x5F` (80–95) | keypad **pad 0** (`KeyPressed` with `pad=0`, app adds 0x50) |
|
||||
| `0x60`–`0x6F` (96–111) | keypad **pad 1** (`KeyPressed` with `pad=1`, app adds 0x60) |
|
||||
|
||||
(`0x48`–`0x4F` unused.) Keypad offset logic:
|
||||
[`KeypadEvent`](../legacy/riovjoy2.cpp#L1185).
|
||||
|
||||
### Per-entry 16-bit encoding
|
||||
|
||||
Each `iRIO[addr]` is a 16-bit word. High byte = routing flags, low byte =
|
||||
payload:
|
||||
|
||||
| Bit | Name | Meaning |
|
||||
|--------|-----------|---------|
|
||||
| 0x8000 | hasLamp | This input drives a lighted button (lamp feedback on press/release) |
|
||||
| 0x4000 | mouse | Route payload to **mouse** |
|
||||
| 0x2000 | hat | Route payload to **POV hat** |
|
||||
| 0x1000 | joy | Route payload to **joystick button** |
|
||||
| 0x0800 | extended | Keyboard: set `KEYEVENTF_EXTENDEDKEY` |
|
||||
| 0x0400 | alt | Keyboard: hold ALT around the key |
|
||||
| 0x0200 | ctrl | Keyboard: hold CTRL around the key |
|
||||
| 0x0100 | shift | Keyboard: hold SHIFT around the key |
|
||||
| 0x00FF | value | VK code / button# / hat direction / mouse action |
|
||||
|
||||
### Routing precedence (`Press_V2` / `Release_V2`)
|
||||
|
||||
1. If **none** of joy/hat/mouse set → **keyboard**: press modifiers, then send
|
||||
the key by **scancode** (`MapVirtualKey(VK, MAPVK_VK_TO_VSC)` +
|
||||
`KEYEVENTF_SCANCODE`, plus extended flag). VK list:
|
||||
[`legacy/hbb_vkey.cpp`](../legacy/hbb_vkey.cpp).
|
||||
2. If **joy && hat && mouse all set** (`0x7000`) → it's a **RIO command**, not an
|
||||
output: calls `RIOcmd(value)` (axis resets, recalibrate, status, etc. — see
|
||||
[`RIOcmd`](../legacy/riovjoy2.cpp#L1852)).
|
||||
3. Else if **joy** → set joystick button `value`.
|
||||
4. Else if **hat** → set POV to direction `value` (release → center).
|
||||
5. Else if **mouse** → mouse action `value`.
|
||||
|
||||
Lamp feedback: on press `SolidBright`, on release `SolidDim`; lamps initialized
|
||||
to `SolidDim` at startup for entries with `hasLamp`.
|
||||
|
||||
### Mouse action codes (low byte when `mouse` set)
|
||||
|
||||
`0`=move up, `1`=move right, `2`=move down, `3`=move left, `4`=left click,
|
||||
`5`=right click ([`Mouse`](../legacy/riovjoy2.cpp#L2075)).
|
||||
|
||||
> ⚠️ The legacy move directions mix `dx`/`dy` oddly (e.g. "up" sets `dx=-50`).
|
||||
> Treat the codes as the contract; fix the actual movement deltas in the port.
|
||||
|
||||
---
|
||||
|
||||
## 6. Lighted-button & board name tables
|
||||
|
||||
Human-readable lamp and board names (useful for the config UI and diagnostics)
|
||||
are in [`GetLampName`](../legacy/riovjoy2.cpp#L1332) and
|
||||
[`GetBoardName`](../legacy/riovjoy2.cpp#L1413).
|
||||
|
||||
---
|
||||
|
||||
## 7. Plasma / VFD display (secondary COM port)
|
||||
|
||||
ESC-based command set ([`CPlasma`](../legacy/riovjoy2.cpp#L2146)): clear (`ESC @`),
|
||||
cursor X/Y (`ESC R`/`ESC Q`), font (`ESC K`), attribute (`ESC H`), box draw/fill
|
||||
(`ESC X`/`ESC x`), plus text. Fonts and sizes:
|
||||
[`GetFontSize`](../legacy/riovjoy2.cpp#L2198). Port for Phase 4; content becomes
|
||||
per-profile.
|
||||
@@ -0,0 +1,3 @@
|
||||
; This is the Data to be used with gimp RIO template, copy the first three lines into a data file, install GIMP run gimp script sg-goobie-MFD.scm
|
||||
( a-00 b-00 b-01 b-02 b-03 b-04 b-05 b-06 b-07 b-08 b-09 b-0A b-0B b-0C b-0D b-0E b-0F b-10 b-11 b-12 b-13 b-14 b-15 b-16 b-17 b-18 b-19 b-1A b-1B b-1C b-1D b-1E b-1F b-20 b-21 b-22 b-23 b-24 b-25 b-26 b-27 b-28 b-29 b-2A b-2B b-2C b-2D b-2E b-2F b-30 b-31 b-32 b-33 b-34 b-35 b-36 b-37 b-38 b-39 b-3A b-3B b-3C b-3D b-3E b-3F b-40 b-41 b-42 b-43 b-44 b-45 b-46 b-47 b-50 b-51 b-52 b-53 b-54 b-55 b-56 b-57 b-58 b-59 b-5A b-5B b-5C b-5D b-5E b-5F b-60 b-61 b-62 b-63 b-64 b-65 b-66 b-67 b-68 b-69 b-6A b-6B b-6C b-6D b-6E b-6F )
|
||||
( "defalt" "LSDI" "ZOOM" "UNDOCK" "LDS" "BALANCE" "DRIVE" "DEFENCE" "OFFENCE" "LAST AGGESSOR" "SUB TARGET" "NEXT ENEMY" "NEAR ENEMY" "LAST CONTACT" "DOWN CON" "UP CONTACT" "TOP CONTACT" "M U" "M R" "M D" "M L" "MLC" "TAB" "" "" "ENTER" "ALT" "CTRL" "SHIFT" "VL U" "VL D" "" "" ">" "v" "^" "<" "N" "M" "L" "K" "V" "U" "T" "S" "J" "I" "H" "G" "Z" "Y" "X" "W" "R" "Q" "P" "O" "M ^" "M >" "M v" "M <" "ML" "ESC" "MR" "REVERSE" "FIRE/ACCEPT" "H v" "H ^" "H >" "H <" "NEXT PRIMEARY" "NEXT SECOND" "TARGET/BACK" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F" "RIO_0" "RIO_1" "RIO_2" "RIO_3" "RIO_4" "RIO_5" "RIO_6" "RIO_7" "RIO_8" "RIO_9" "RIO_A" "RIO_B" "RIO_C" "RIO_D" "RIO_E" "RIO_F" )
|
||||
@@ -0,0 +1 @@
|
||||
https://docs.google.com/spreadsheets/d/1TkWsk0xXA4rPArQvldmQiWhGd77eB3NUm0Q3XpW5cEU/edit?usp=sharing
|
||||
Binary file not shown.
@@ -0,0 +1,212 @@
|
||||
; This program is free software; you can redistribute it and/or modify
|
||||
; it under the terms of the GNU General Public License as published by
|
||||
; the Free Software Foundation; either version 2 of the License, or
|
||||
; (at your option) any later version.
|
||||
;
|
||||
; This program is distributed in the hope that it will be useful,
|
||||
; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
; GNU General Public License for more details.
|
||||
|
||||
; This script reads in a data file containing a list of
|
||||
; RIO button information.
|
||||
|
||||
; The data file uses Lisp/Scheme style lists and commenting (comments
|
||||
; start with a semi-colon and extend to the end of the line).
|
||||
|
||||
; The first non-comment line of the data file should be a list containing
|
||||
; the field names of the RIO button addresses wrapped in parentheses.
|
||||
; For example:
|
||||
;
|
||||
; ; THIS IS A COMMENT AND IGNORED
|
||||
; ( 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F )
|
||||
;
|
||||
; This "header" list should be followed by the LINE containing the
|
||||
; actual data to be substituted into the template file. The data should
|
||||
; be text strings and appear in the same order as the fields specified in
|
||||
; the header list.
|
||||
;
|
||||
; ("A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "CTRL" "ALT")
|
||||
;
|
||||
;
|
||||
; The currently open image is used as a template with the upper layers having
|
||||
; names that match the field names. These field layers are used to determine
|
||||
; the location and size of the text (this layer is otherwise ignored).
|
||||
; The name text is centered within the bounds of this field template layer.
|
||||
;
|
||||
;
|
||||
(define (script-fu-sg-goobie image
|
||||
datafilename
|
||||
save-xcf
|
||||
save-jpeg
|
||||
save-png
|
||||
save-dir
|
||||
vert-justification
|
||||
size-handling)
|
||||
|
||||
;; Perform a search for the largest font that will fit within
|
||||
;; the cell.
|
||||
;
|
||||
(define (calc-fontsize text font width height)
|
||||
(let loop ((fontsize 6) ;; minimum possible fontsize
|
||||
(last-extents nil)
|
||||
(last-fontsize 3)
|
||||
(adjust 2)
|
||||
)
|
||||
(let ((extents (gimp-text-get-extents-fontname text fontsize PIXELS font)))
|
||||
(if (or (= last-fontsize fontsize) (equal? extents last-extents))
|
||||
(max fontsize 6)
|
||||
(if (or (> (car extents) width) (> (cadr extents) height))
|
||||
(loop (truncate (* last-fontsize (+ (* (- adjust 1) 0.5) 1)))
|
||||
last-extents
|
||||
last-fontsize
|
||||
(+ (* (- adjust 1) 0.5) 1) )
|
||||
(loop (truncate (* fontsize adjust))
|
||||
extents
|
||||
fontsize
|
||||
adjust ))))))
|
||||
|
||||
(define (create-data-layer image field-name field-data)
|
||||
(let ((frame-layer
|
||||
(let loop ((layers (vector->list (cadr (gimp-image-get-layers image)))))
|
||||
(if (null? layers)
|
||||
#f
|
||||
(if (string=? field-name (car (gimp-drawable-get-name (car layers))))
|
||||
(car layers)
|
||||
(loop (cdr layers)) )))))
|
||||
(if frame-layer
|
||||
(if (= (car (gimp-drawable-is-text-layer frame-layer)) 1)
|
||||
(let ((x (car (gimp-drawable-offsets frame-layer)))
|
||||
(y (cadr (gimp-drawable-offsets frame-layer)))
|
||||
(w (car (gimp-drawable-width frame-layer)))
|
||||
(h (car (gimp-drawable-height frame-layer)))
|
||||
(font (car (gimp-text-layer-get-font frame-layer)))
|
||||
(size (car (gimp-text-layer-get-font-size frame-layer)))
|
||||
)
|
||||
(gimp-image-set-active-layer image frame-layer)
|
||||
(gimp-text-layer-set-text frame-layer field-data)
|
||||
(let ((extents (gimp-text-get-extents-fontname field-data
|
||||
size
|
||||
PIXELS
|
||||
font )))
|
||||
(when (and (zero? size-handling)
|
||||
(or (> (car extents) w)
|
||||
(> (cadr extents) h) ))
|
||||
(gimp-text-layer-set-font-size frame-layer (calc-fontsize field-data font w h) PIXELS) )
|
||||
(case vert-justification
|
||||
((0) ; center
|
||||
(gimp-layer-set-offsets frame-layer
|
||||
x
|
||||
(+ y (/ (- h (cadr extents)) 2)) ))
|
||||
((1) ; top
|
||||
(gimp-layer-set-offsets frame-layer
|
||||
x
|
||||
y ))
|
||||
((2) ; bottom
|
||||
(gimp-layer-set-offsets frame-layer
|
||||
x
|
||||
(- (+ y h) (cadr extents)) )))))
|
||||
(gimp-message "Field layer is not a text layer")
|
||||
)
|
||||
(begin
|
||||
(gimp-message (string-append "Field layer not found: " field-name)) ))
|
||||
frame-layer ))
|
||||
|
||||
;; ----------------------------------------------------------------------
|
||||
;; Main processing start here
|
||||
;
|
||||
(let* ((inport (open-input-file datafilename))
|
||||
(field-names (map symbol->string (read inport)))
|
||||
(filetag (car field-names)) )
|
||||
(gimp-image-undo-freeze image)
|
||||
(gimp-context-push)
|
||||
(let entry-loop ((fields (read inport)))
|
||||
(unless (eof-object? fields)
|
||||
(let ((temp-image (car (gimp-image-duplicate image)))
|
||||
(filename #f) )
|
||||
(let field-loop ((field-names field-names)
|
||||
(field-values fields) )
|
||||
(unless (null? field-values)
|
||||
(when (string=? (car field-names) filetag)
|
||||
(set! filename (car field-values)) )
|
||||
(create-data-layer temp-image (car field-names) (car field-values))
|
||||
(field-loop (cdr field-names) (cdr field-values)) ))
|
||||
(if filename
|
||||
(let ((fullname (string-append save-dir
|
||||
DIR-SEPARATOR
|
||||
filename )))
|
||||
(unless (zero? save-xcf)
|
||||
(let ((filename (string-append fullname ".xcf")))
|
||||
(gimp-xcf-save TRUE
|
||||
temp-image
|
||||
(car (gimp-image-get-active-layer image))
|
||||
filename
|
||||
filename )))
|
||||
(unless (zero? save-png)
|
||||
(let ((layer (car (gimp-image-merge-visible-layers temp-image
|
||||
CLIP-TO-IMAGE )))
|
||||
(filename (string-append fullname ".png")) )
|
||||
(file-png-save2 RUN-NONINTERACTIVE
|
||||
temp-image
|
||||
layer
|
||||
filename
|
||||
filename
|
||||
FALSE ; interlace
|
||||
9
|
||||
FALSE ; bkgd
|
||||
(car (gimp-drawable-has-alpha layer))
|
||||
FALSE ; offs
|
||||
FALSE ; phys
|
||||
FALSE ; time
|
||||
TRUE ; comment
|
||||
FALSE ; svtrans
|
||||
)))
|
||||
(unless (zero? save-jpeg)
|
||||
(let ((layer (car (gimp-image-flatten temp-image)))
|
||||
(filename (string-append fullname ".jpg")) )
|
||||
(file-jpeg-save RUN-NONINTERACTIVE
|
||||
temp-image
|
||||
layer
|
||||
filename
|
||||
filename
|
||||
0.93
|
||||
0 ; smoothing
|
||||
1 ; optimize
|
||||
1 ; progressive
|
||||
"" ; comment
|
||||
0 ; subsmp (0-4)
|
||||
1 ; baseline
|
||||
0 ; restart
|
||||
0 ;dct
|
||||
))))
|
||||
(gimp-message "Error encountered") )
|
||||
; (gimp-image-delete temp-image)
|
||||
)
|
||||
(entry-loop (read inport)) ))
|
||||
(close-input-port inport)
|
||||
(gimp-context-pop)
|
||||
(gimp-image-undo-thaw image)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
(script-fu-register "script-fu-sg-goobie"
|
||||
"Goobie (MFD)..."
|
||||
"Create MFD image based on template image"
|
||||
"Frank Galatis"
|
||||
"Saul Goode"
|
||||
"June 2012"
|
||||
"*"
|
||||
SF-IMAGE "Image" 0
|
||||
SF-FILENAME "Data file" "example.data"
|
||||
SF-TOGGLE "Save as XCF" FALSE
|
||||
SF-TOGGLE "Save as JPEG" TRUE
|
||||
SF-TOGGLE "Save as PNG" FALSE
|
||||
SF-DIRNAME "Save directory" ""
|
||||
SF-OPTION "Vertical justification" '("Center" "Top" "Bottom")
|
||||
SF-OPTION "Font sizing (if too large)" '("Fit" "Crop" "Overflow")
|
||||
)
|
||||
(script-fu-menu-register "script-fu-sg-goobie"
|
||||
"<Image>/File"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user