fs2port/port/src/panelDigits.c
2026-05-14 10:03:23 -05:00

210 lines
10 KiB
C

// Live digital readouts overlaid on the FS2 instrument panel.
//
// Coordinates and placeholder strings come straight from the FS2
// disassembly (chunk5 message definitions). The original FS2 panel
// bitmap has placeholder digits ("2485" / "1000" / "1135" / "1200" /
// "2370" / "000") baked into the artwork; before drawing live values
// we paint a black rectangle over those bytes so they don't bleed
// through.
//
// FS2's font cell is 6 colour-pixels (12 hires pixels) wide with a
// 4-colour-pixel (8 hires pixel) advance per character, so an N-char
// placeholder spans `N * 8 + 4` hires pixels. Our port uses a narrower
// font, so the erasure rectangle has to be sized to the FS2 placeholder
// width and our drawn text gets centred within it for visual balance.
#include <stdio.h>
#include <string.h>
#include "camera.h"
#include "font.h"
#include "panelDigits.h"
// Hires pixels covered by an N-char FS2 placeholder text.
#define FS2_CHAR_ADVANCE_HIRES 8
#define FS2_CHAR_WIDTH_HIRES 12
typedef struct ReadoutT {
int16_t x; // screen pixel column (FS2 col * 2)
int16_t y; // screen pixel row
uint8_t cellsWide; // how many character cells to clear
} ReadoutT;
// FS2 chunk5 has e.g. `msg_com1: MESSAGE $6C, $69, "2485"`. Row $6C =
// 108, col $69 = 105 colour-pixels = 210 hires-pixels. Same for the
// rest.
static const ReadoutT readoutCom1 = { 210, 108, 4 };
static const ReadoutT readoutNav1 = { 210, 122, 4 };
static const ReadoutT readoutNav2 = { 210, 136, 4 };
// chunk3 msg_adf_frequency at row $88=136, col $67=103 colour = 206
// hires. Lives in the same row as msg_nav2 but starts 4 hires pixels
// left; FS2 paints whichever message ADFMode selects.
static const ReadoutT readoutAdfFreq = { 206, 136, 4 };
static const ReadoutT readoutXpndr = { 246, 136, 4 };
static const ReadoutT readoutDme = { 250, 122, 3 }; // msg_dme: $7A, $7D
static const ReadoutT readoutClockHH = { 228, 145, 2 }; // msg_clock_hh: $91, $72
static const ReadoutT readoutClockMM = { 246, 145, 2 }; // msg_clock_mm: $91, $7B
static const ReadoutT readoutClockSS = { 264, 145, 2 }; // msg_clock_ss: $91, $84
static const ReadoutT readoutRpm = { 246, 179, 4 };
static const ReadoutT readoutHeading = { 64, 161, 3 };
static const ReadoutT readoutRecip = { 64, 177, 3 };
static const ReadoutT readoutVor1Course = { 168, 108, 3 };
static const ReadoutT readoutVor1Recip = { 168, 140, 3 };
static const ReadoutT readoutVor2Course = { 168, 151, 3 };
static const ReadoutT readoutVor2Recip = { 168, 183, 3 };
// Lights toggle (FS2 msg_lights_on/off at $9A=154, col $87=135 colour
// = 270 hires). Carb heat toggle (FS2 msg_carbheat_on/off at $BB=187,
// col $6E=110 colour = 220 hires) shows "HEAT" or "C.H.".
static const ReadoutT readoutLights = { 270, 154, 1 };
static const ReadoutT readoutCarbHeat = { 220, 187, 4 };
static void drawReadout(FramebufferT *fb, const ReadoutT *r, const char *text);
static void drawReadout(FramebufferT *fb, const ReadoutT *r, const char *text) {
// Erase the FS2 placeholder behind the readout. FS2 chars
// occupy 12 hires pixels in width with an 8-hires advance, so
// an N-char placeholder needs `N * 8 + (12 - 8)` = `N*8+4`
// hires pixels.
int16_t clearW = (int16_t)(r->cellsWide * FS2_CHAR_ADVANCE_HIRES + (FS2_CHAR_WIDTH_HIRES - FS2_CHAR_ADVANCE_HIRES));
int16_t clearH = FONT_HEIGHT + 1;
framebufferFillRect(fb, (int16_t)(r->x - 2), (int16_t)(r->y - 1), (int16_t)(clearW + 2), clearH, COLOR_BLACK);
fontDrawStringCentered(fb, (int16_t)(r->x + clearW / 2), r->y, text, COLOR_WHITE);
}
void panelDigitsDraw(FramebufferT *fb, const AircraftT *ac, const RadiosT *radios, const TimeOfDayT *tod) {
char buf[16];
// COM1 / NAV1 / NAV2: tuned frequencies from the radios state.
// Format is "XXXX" -> XXX.X MHz (FS2 BCD layout).
char freqBuf[8];
radiosFormatFreq(radios->com1Freq, RADIO_COM1, freqBuf);
drawReadout(fb, &readoutCom1, freqBuf);
// ATIS chunked text (FS2 chunk5 UpdateCOMMessageChunks). When
// tuned to an active COM station, scroll a synthesized info
// string ("KMDW WIND 270/12 ALT 30.05 RWY 18L") through a
// dedicated cell to the right of the COM frequency. Rolls one
// char left per frame; wraps with a 4-space gap. We synthesize
// the text from the station name + time + airframe state since
// FS2's actual ATIS payload isn't in our station db.
static char atisText[96];
static int atisIdx;
static int atisLen;
static uint16_t lastComFreq;
if (radios->com1Station != NULL) {
if (radios->com1Freq != lastComFreq) {
const char *nm = radios->com1Station->name[0]
? radios->com1Station->name : "ATIS";
int hh = tod ? tod->hours : 12;
snprintf(atisText, sizeof(atisText),
"%s WIND 270/12 ALT 30.05 RWY 18L TIME %02d:00 ",
nm, hh);
atisLen = (int)strlen(atisText);
atisIdx = 0;
lastComFreq = radios->com1Freq;
}
if (atisLen > 0) {
// Display 8 chars from atisIdx, wrapping.
char window[9];
for (int j = 0; j < 8; j++) {
window[j] = atisText[(atisIdx + j) % atisLen];
}
window[8] = '\0';
// Print at a free spot to the right of COM
// freq (col 250+, row 108).
framebufferFillRect(fb, 244, 107, 36, 8, COLOR_BLACK);
fontDrawString(fb, 246, 108, window, COLOR_WHITE);
// Roll every other frame to keep readable.
static uint8_t rollAccum;
rollAccum++;
if ((rollAccum & 1) == 0) {
atisIdx = (atisIdx + 1) % atisLen;
}
}
}
radiosFormatFreq(radios->nav1Freq, RADIO_NAV1, freqBuf);
drawReadout(fb, &readoutNav1, freqBuf);
// NAV2 vs ADF frequency share the same row; FS2 picks one per
// chunk4 ADFMode (DrawNav2 / UpdateADFIndicator each gate on
// the OTHER mode). Match that.
if (ac->adfMode) {
radiosFormatFreq(radios->adfFreq, RADIO_ADF, freqBuf);
drawReadout(fb, &readoutAdfFreq, freqBuf);
} else {
radiosFormatFreq(radios->nav2Freq, RADIO_NAV2, freqBuf);
drawReadout(fb, &readoutNav2, freqBuf);
}
drawReadout(fb, &readoutXpndr, "1200");
// RPM derived from forward speed; idle 600, max ~2300.
// forwardSpeed is Q8.8: rpm = 600 + (speed_q88 * 1100) >> 8.
int rpm = 600 + ((int)ac->forwardSpeed * 1100 >> 8);
if (ac->stalled) {
rpm = 600;
}
if (rpm > 2700) {
rpm = 2700;
}
snprintf(buf, sizeof(buf), "%4d", rpm);
drawReadout(fb, &readoutRpm, buf);
// Heading and reciprocal as 3-digit readouts under the
// airspeed dial. Byte angle 0..255 -> degrees 0..360. Reality
// mode replaces the digits with a dashed placeholder when the
// gyrocompass bit is cleared.
if (ac->failedInstruments & AC_FAIL_HEADING) {
drawReadout(fb, &readoutHeading, "---");
drawReadout(fb, &readoutRecip, "---");
} else {
int heading = byteAngleToDegrees(ac->yaw);
heading %= 360;
snprintf(buf, sizeof(buf), "%03d", heading);
drawReadout(fb, &readoutHeading, buf);
int recip = (heading + 180) % 360;
snprintf(buf, sizeof(buf), "%03d", recip);
drawReadout(fb, &readoutRecip, buf);
}
// VOR1 / VOR2 OBS course + reciprocal. obs is byte angle;
// convert to degrees 0..359.
int obs1Deg = byteAngleToDegrees(radios->nav1Obs);
int obs2Deg = byteAngleToDegrees(radios->nav2Obs);
snprintf(buf, sizeof(buf), "%03d", obs1Deg);
drawReadout(fb, &readoutVor1Course, buf);
snprintf(buf, sizeof(buf), "%03d", (obs1Deg + 180) % 360);
drawReadout(fb, &readoutVor1Recip, buf);
snprintf(buf, sizeof(buf), "%03d", obs2Deg);
drawReadout(fb, &readoutVor2Course, buf);
snprintf(buf, sizeof(buf), "%03d", (obs2Deg + 180) % 360);
drawReadout(fb, &readoutVor2Recip, buf);
// DME from active NAV1 (FS2 only displayed one DME readout).
if (radios->nav1Valid) {
int dme = radios->nav1Dme > 999 ? 999 : (int)radios->nav1Dme;
snprintf(buf, sizeof(buf), "%03d", dme);
drawReadout(fb, &readoutDme, buf);
} else {
drawReadout(fb, &readoutDme, "---");
}
// Cockpit toggles. Lights "1" (on) / "O" (off), carb heat
// "HEAT" (on) / "C.H." (off) -- text matches FS2 chunk5
// msg_lights_on/off and msg_carbheat_on/off.
drawReadout(fb, &readoutLights, ac->lightsOn ? "1" : "O");
drawReadout(fb, &readoutCarbHeat, ac->carbHeatOn ? "HEAT" : "C.H.");
uint8_t clockHH = tod != NULL ? tod->hours : 0;
uint8_t clockMM = tod != NULL ? tod->minutes : 0;
// Sub-minute frame counter doubles as a coarse seconds proxy
// (TIME_FRAMES_PER_MINUTE in timeOfDay.c).
uint8_t clockSS = tod != NULL ? (uint8_t)((tod->frameSubMinute * 60) / 4 % 60) : 0;
snprintf(buf, sizeof(buf), "%02u", clockHH);
drawReadout(fb, &readoutClockHH, buf);
snprintf(buf, sizeof(buf), "%02u", clockMM);
drawReadout(fb, &readoutClockMM, buf);
snprintf(buf, sizeof(buf), "%02u", clockSS);
drawReadout(fb, &readoutClockSS, buf);
}