210 lines
10 KiB
C
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);
|
|
}
|