// 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 #include #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); }