diff --git a/port/include/aircraft.h b/port/include/aircraft.h index 46aa8d9..4ca84a2 100644 --- a/port/include/aircraft.h +++ b/port/include/aircraft.h @@ -59,6 +59,11 @@ typedef enum ViewDirectionE { // Compose a uint8 throttle/flaps/mixture value from a percent 0..100. #define AC_BYTE_PCT(p) ((uint8_t)(((int)(p) * 255) / 100)) +// Top-of-envelope forward speed in Q8.8 world-units/frame +// (~1.6 wu/frame). audio.c references this to compute its wind hiss +// amplitude. +#define AC_MAX_FORWARD_SPEED_Q88 410 + // Reality-mode instrument failure bits. Mirrors chunk3 // `InstrumentOperationalFlags` (init $FF = all good). The // chunk3 `FailureProcTable` clears one of these bits or sets one of diff --git a/port/include/camera.h b/port/include/camera.h index 58d394c..6abc08d 100644 --- a/port/include/camera.h +++ b/port/include/camera.h @@ -25,6 +25,22 @@ #define CAM_ROT_FRACT_BITS 15 #define CAM_ROT_ONE (1 << CAM_ROT_FRACT_BITS) + +static inline int32_t metresFromQ1616(int32_t v_q1616) { + return v_q1616 >> CAM_POS_FRACT_BITS; +} + + +static inline int32_t q1616FromMetres(int32_t m) { + return m * CAM_POS_FRACT_ONE; +} + + +// Convert FS2 byte angle (0..255 == 0..359 degrees) to degrees. +static inline int16_t byteAngleToDegrees(uint8_t angle) { + return (int16_t)(((int32_t)angle * 360) / 256); +} + typedef struct CameraT { int32_t worldX; // Q16.16 world units int32_t worldY; diff --git a/port/include/font.h b/port/include/font.h index e5c8b21..6d1bba6 100644 --- a/port/include/font.h +++ b/port/include/font.h @@ -18,4 +18,8 @@ int16_t fontDrawChar(FramebufferT *fb, int16_t x, int16_t y, char ch, ColorE col // Draw a NUL-terminated string. Returns the X advance. int16_t fontDrawString(FramebufferT *fb, int16_t x, int16_t y, const char *s, ColorE color); +// Draw `s` horizontally centred around screen column `cx`, with the +// top-left of the glyph row at `y`. Returns the X advance. +int16_t fontDrawStringCentered(FramebufferT *fb, int16_t cx, int16_t y, const char *s, ColorE color); + #endif diff --git a/port/include/fs2math.h b/port/include/fs2math.h index dc2f13a..31c185f 100644 --- a/port/include/fs2math.h +++ b/port/include/fs2math.h @@ -61,4 +61,22 @@ uint8_t fs2VsiNeedlePos(int16_t value16); // `UpdateSlipSkidIndicator` (chunk4 L2497) consumes. uint8_t fs2SlipSkidIndex(int8_t slipValue); + +// Clamp `v` to [lo, hi]. +static inline int fs2ClampInt(int v, int lo, int hi) { + if (v < lo) { + return lo; + } + if (v > hi) { + return hi; + } + return v; +} + + +// Add `step` to `v`, then clamp to [lo, hi]. +static inline int fs2StepClamp(int v, int step, int lo, int hi) { + return fs2ClampInt(v + step, lo, hi); +} + #endif diff --git a/port/include/renderer.h b/port/include/renderer.h index b716486..8255c9f 100644 --- a/port/include/renderer.h +++ b/port/include/renderer.h @@ -29,7 +29,6 @@ void rendererBegin(RenderStateT *state, FramebufferT *fb); void rendererSetDrawColor(RenderStateT *state, ColorE color); void rendererSetHiresColor(RenderStateT *state, uint8_t hiresCode); void rendererSetFillColors(RenderStateT *state, ColorE fill, ColorE altFill); -void rendererSwapFillColors(RenderStateT *state); void rendererDrawLine(RenderStateT *state, int16_t x1, int16_t y1, int16_t x2, int16_t y2); diff --git a/port/include/sceneryData.h b/port/include/sceneryData.h index 3df4e9c..8f4c041 100644 --- a/port/include/sceneryData.h +++ b/port/include/sceneryData.h @@ -65,4 +65,10 @@ void sceneryDataFree(SceneryDataT *out); // Human-readable name for a region. Always returns a non-NULL string. const char *sceneryDataRegionName(SceneryRegionE region); +// Reverse-lookup: map the basename string used by SCENERY_REGION +// (e.g. "FS2.1", "FS2.1_chicago", "SD3", "SD14B") back to its enum. +// Accepts the bare SD form ("SD3") as well as the "A2.SD3" full form. +// Returns SCENERY_NONE if unrecognised. +SceneryRegionE sceneryDataRegionFromName(const char *name); + #endif diff --git a/port/include/types.h b/port/include/types.h index 5600351..448ad7d 100644 --- a/port/include/types.h +++ b/port/include/types.h @@ -22,16 +22,6 @@ // Default scale factor (window pixels per native pixel). #define WINDOW_SCALE 4 -// 16-bit signed scenery coordinate. -typedef int16_t Coord16T; - -// One vertex in scenery / camera space. -typedef struct VertexT { - Coord16T x; - Coord16T y; - Coord16T z; -} VertexT; - // Cohen-Sutherland-style outcode bits for frustum clipping. Mirrors // the layout used by the original disassembly so the clipper logic // can be ported one-for-one. diff --git a/port/include/ww1ace.h b/port/include/ww1ace.h index 04d55e2..bfab319 100644 --- a/port/include/ww1ace.h +++ b/port/include/ww1ace.h @@ -97,11 +97,6 @@ void ww1aceInit(WW1AceStateT *s); // score/bomb counters. Player coords are Q16.16 world-units. void ww1aceToggle(WW1AceStateT *s, int32_t playerX, int32_t playerZ); -// Legacy bomb-drop entry that just decrements the bomb count. Prefer -// `ww1aceDropBombAt` so the bomb is added to the in-flight pool with -// the player's actual position + velocity. -void ww1aceDropBomb(WW1AceStateT *s); - // Drop a bomb at the player's current world position with the player's // horizontal velocity (Q8.8 metres/frame). The bomb then falls under // gravity in `ww1aceUpdate` and tries to score a hit on a ground diff --git a/port/src/aircraft.c b/port/src/aircraft.c index a4377f8..a4df570 100644 --- a/port/src/aircraft.c +++ b/port/src/aircraft.c @@ -26,8 +26,8 @@ #define DEFAULT_DECAY_K 218 // ~0.85 self-centring #define RUDDER_DECAY_K 179 // ~0.70 -// Speed envelope. -#define MAX_FORWARD_SPEED_Q88 410 // 1.6 world-units / frame +// Speed envelope. AC_MAX_FORWARD_SPEED_Q88 lives in aircraft.h so +// audio.c can read it without duplicating the literal. #define DRAG_K 1 // 1/256 ~ 0.4% // Lift / gravity. @@ -217,26 +217,23 @@ void aircraftInit(AircraftT *ac) { static AircraftT recoveryBuf; -static bool recoveryValid; void aircraftArmRecovery(AircraftT *ac) { - recoveryBuf = *ac; - recoveryValid = true; + // Set the snapshot flag before capturing so the restored state + // also reports armed -- successive crashes can reuse the same + // snapshot without needing another arm pass. ac->hasRecoverySnapshot = true; + recoveryBuf = *ac; } void aircraftRestoreRecovery(AircraftT *ac) { - if (!recoveryValid) { + if (!ac->hasRecoverySnapshot) { return; } - // Preserve the recovery-armed flag so further crashes can use - // the same snapshot. - bool hasSnap = ac->hasRecoverySnapshot; *ac = recoveryBuf; - ac->hasRecoverySnapshot = hasSnap; - ac->crashed = false; + ac->crashed = false; ac->crashType = CRASH_NONE; } @@ -559,9 +556,7 @@ static void stepFlight(AircraftT *ac, WindStateT *wind) { // (using the >> 7 form keeps int8 input valued at full scale). // Trim biases yokeVert -- chunk5 RefreshElevatorIndicator // accumulates trim into the elevator command. - int yokePlusTrim = (int)(int8_t)ac->yokeVert + (int)(int8_t)ac->trim; - if (yokePlusTrim > 127) yokePlusTrim = 127; - if (yokePlusTrim < -127) yokePlusTrim = -127; + int yokePlusTrim = fs2ClampInt((int)(int8_t)ac->yokeVert + (int)(int8_t)ac->trim, -127, 127); int16_t pitchTarget = (int16_t)(((int32_t)yokePlusTrim * MAX_PITCH_RATE_Q88) / 127); int16_t bankTarget = (int16_t)(((int32_t)(int8_t)ac->yokeHoriz * MAX_BANK_RATE_Q88) / 127); ac->pitchRate = q88Lerp(ac->pitchRate, pitchTarget, INPUT_SMOOTH_K); @@ -597,7 +592,7 @@ static void stepFlight(AircraftT *ac, WindStateT *wind) { if (ac->fuelLeft == 0 && ac->fuelRight == 0) { effectiveThrottle = 0; } - int16_t targetSpeed = (int16_t)(((int32_t)effectiveThrottle * MAX_FORWARD_SPEED_Q88) / 255); + int16_t targetSpeed = (int16_t)(((int32_t)effectiveThrottle * AC_MAX_FORWARD_SPEED_Q88) / 255); int16_t engineDelta = (int16_t)((((int32_t)targetSpeed - ac->forwardSpeed) * ENGINE_SMOOTH_K) >> 8); int16_t dragDelta = (int16_t)(((int32_t)ac->forwardSpeed * DRAG_K) >> 8); ac->forwardSpeed = q88Add(ac->forwardSpeed, q88Add(engineDelta, (int16_t)-dragDelta)); diff --git a/port/src/audio.c b/port/src/audio.c index 1969c8f..b386fdf 100644 --- a/port/src/audio.c +++ b/port/src/audio.c @@ -193,11 +193,6 @@ void audioTriggerGun(void) { } -// AC_RATE_FRACT_ONE * 1.6 = 410, the FS2 max forward speed in Q8.8. -// See MAX_FORWARD_SPEED_Q88 in aircraft.c. -#define AUDIO_SPEED_FULL_Q88 410 - - void audioUpdate(const AircraftT *ac) { if (audio.device == 0) { return; @@ -207,9 +202,13 @@ void audioUpdate(const AircraftT *ac) { // single SDL_LockAudioDevice store; the audio synth itself // still runs in float because SDL streams float samples. // speedFraction : Q8.8 from 0..256 (= 0.0..1.0) - int speedT = (int)ac->forwardSpeed * 256 / AUDIO_SPEED_FULL_Q88; - if (speedT < 0) speedT = 0; - if (speedT > 256) speedT = 256; + int speedT = (int)ac->forwardSpeed * 256 / AC_MAX_FORWARD_SPEED_Q88; + if (speedT < 0) { + speedT = 0; + } + if (speedT > 256) { + speedT = 256; + } // freqQ88 = base + speedT * (max - base) / 256 int32_t freqRange = ENGINE_MAX_HZ - ENGINE_BASE_HZ; int32_t freqQ88 = ((int32_t)ENGINE_BASE_HZ << 8) @@ -271,7 +270,9 @@ void audioUpdate(const AircraftT *ac) { if (!ac->onGround) { float speedFrac = (float)speedT / 256.0f; windAmp = sqrtf(speedFrac); - if (windAmp > 1.0f) windAmp = 1.0f; + if (windAmp > 1.0f) { + windAmp = 1.0f; + } } SDL_LockAudioDevice(audio.device); diff --git a/port/src/camera.c b/port/src/camera.c index 799f910..e87d2bc 100644 --- a/port/src/camera.c +++ b/port/src/camera.c @@ -4,6 +4,7 @@ #include "camera.h" #include "chunk5Setup.h" +#include "fs2math.h" #include "math6502.h" @@ -71,12 +72,8 @@ void cameraTransform(const CameraT *cam, int32_t wx_q1616, int32_t wy_q1616, int // matRow2 and the int16 RAM mirror. Verified against MAME 2026-05-07. void cameraGet2x3Matrix(const CameraT *cam, int8_t outRowX[3], int8_t outRowZ[3]) { for (int i = 0; i < 3; i++) { - int x = cam->rot[i][0] >> 8; - int z = cam->rot[i][2] >> 8; - if (x > 127) x = 127; - if (x < -128) x = -128; - if (z > 127) z = 127; - if (z < -128) z = -128; + int x = fs2ClampInt(cam->rot[i][0] >> 8, -128, 127); + int z = fs2ClampInt(cam->rot[i][2] >> 8, -128, 127); outRowX[i] = (int8_t)x; outRowZ[i] = (int8_t)z; } diff --git a/port/src/editor.c b/port/src/editor.c index 2eac060..c1de15f 100644 --- a/port/src/editor.c +++ b/port/src/editor.c @@ -8,8 +8,10 @@ #include #include "editor.h" #include "aircraft.h" +#include "camera.h" #include "font.h" #include "framebuffer.h" +#include "fs2math.h" #include "radios.h" #include "timeOfDay.h" #include "wind.h" @@ -117,7 +119,6 @@ static const FieldT fields[F_COUNT] = { }; -static int applyStep(int v, int step, int lo, int hi); static void formatValue(char *buf, size_t n, FieldIndexE fi, const AircraftT *ac, const RadiosT *radios, const TimeOfDayT *tod, const WindStateT *wind); @@ -126,28 +127,6 @@ static void modifyField(FieldIndexE fi, int step, TimeOfDayT *tod, WindStateT *wind); -static int applyStep(int v, int step, int lo, int hi) { - int r = v + step; - if (r < lo) { - r = lo; - } - if (r > hi) { - r = hi; - } - return r; -} - - -static int16_t metresFromQ1616(int32_t v_q1616) { - return (int16_t)(v_q1616 >> CAM_POS_FRACT_BITS); -} - - -static int32_t q1616FromMetres(int16_t m) { - return (int32_t)m * CAM_POS_FRACT_ONE; -} - - static void formatValue(char *buf, size_t n, FieldIndexE fi, const AircraftT *ac, const RadiosT *radios, const TimeOfDayT *tod, const WindStateT *wind) { @@ -155,9 +134,9 @@ static void formatValue(char *buf, size_t n, FieldIndexE fi, case F_WORLD_X: snprintf(buf, n, "%6d", metresFromQ1616(ac->worldX)); break; case F_WORLD_Y: snprintf(buf, n, "%6d", metresFromQ1616(ac->worldY)); break; case F_WORLD_Z: snprintf(buf, n, "%6d", metresFromQ1616(ac->worldZ)); break; - case F_YAW: snprintf(buf, n, "%6d", (ac->yaw * 360) / 256); break; - case F_PITCH: snprintf(buf, n, "%6d", (ac->pitch * 360) / 256); break; - case F_BANK: snprintf(buf, n, "%6d", (ac->bank * 360) / 256); break; + case F_YAW: snprintf(buf, n, "%6d", byteAngleToDegrees(ac->yaw)); break; + case F_PITCH: snprintf(buf, n, "%6d", byteAngleToDegrees(ac->pitch)); break; + case F_BANK: snprintf(buf, n, "%6d", byteAngleToDegrees(ac->bank)); break; case F_THROTTLE: snprintf(buf, n, "%6d", (ac->throttle * 100) / 256); break; case F_MIXTURE: snprintf(buf, n, "%6d", (ac->mixture * 100) / 256); break; case F_MAGNETOS: snprintf(buf, n, "%6d", ac->magnetos); break; @@ -174,15 +153,15 @@ static void formatValue(char *buf, size_t n, FieldIndexE fi, snprintf(buf, n, "%6s", names[tod->season & 3]); break; } - case F_WIND_DIR: snprintf(buf, n, "%6d", (wind->surface.direction * 360) / 256); break; + case F_WIND_DIR: snprintf(buf, n, "%6d", byteAngleToDegrees(wind->surface.direction)); break; case F_WIND_SPD: snprintf(buf, n, "%6d", wind->surface.magnitude); break; case F_REALITY: snprintf(buf, n, "%6s", ac->realityMode ? "ON" : "OFF"); break; case F_NAV1_FREQ: snprintf(buf, n, "$%04X", radios->nav1Freq); break; case F_NAV2_FREQ: snprintf(buf, n, "$%04X", radios->nav2Freq); break; case F_COM1_FREQ: snprintf(buf, n, "$%04X", radios->com1Freq); break; case F_ADF_FREQ: snprintf(buf, n, "$%04X", radios->adfFreq); break; - case F_NAV1_OBS: snprintf(buf, n, "%6d", (radios->nav1Obs * 360) / 256); break; - case F_NAV2_OBS: snprintf(buf, n, "%6d", (radios->nav2Obs * 360) / 256); break; + case F_NAV1_OBS: snprintf(buf, n, "%6d", byteAngleToDegrees(radios->nav1Obs)); break; + case F_NAV2_OBS: snprintf(buf, n, "%6d", byteAngleToDegrees(radios->nav2Obs)); break; default: buf[0] = '\0'; break; } } @@ -194,19 +173,19 @@ static void modifyField(FieldIndexE fi, int step, switch (fi) { case F_WORLD_X: { int16_t m = metresFromQ1616(ac->worldX); - m = (int16_t)applyStep(m, step, -32768, 32767); + m = (int16_t)fs2StepClamp(m, step, -32768, 32767); ac->worldX = q1616FromMetres(m); break; } case F_WORLD_Y: { int16_t m = metresFromQ1616(ac->worldY); - m = (int16_t)applyStep(m, step, 0, 30000); + m = (int16_t)fs2StepClamp(m, step, 0, 30000); ac->worldY = q1616FromMetres(m); break; } case F_WORLD_Z: { int16_t m = metresFromQ1616(ac->worldZ); - m = (int16_t)applyStep(m, step, -32768, 32767); + m = (int16_t)fs2StepClamp(m, step, -32768, 32767); ac->worldZ = q1616FromMetres(m); break; } @@ -214,29 +193,29 @@ static void modifyField(FieldIndexE fi, int step, case F_PITCH: ac->pitch = (uint8_t)(ac->pitch + step); break; case F_BANK: ac->bank = (uint8_t)(ac->bank + step); break; case F_THROTTLE: { - int v = applyStep(ac->throttle, step, 0, 255); + int v = fs2StepClamp(ac->throttle, step, 0, 255); ac->throttle = (uint8_t)v; break; } - case F_MIXTURE: ac->mixture = (uint8_t)applyStep(ac->mixture, step, 0, 255); break; - case F_MAGNETOS: ac->magnetos = (uint8_t)applyStep(ac->magnetos, step, 0, 4); break; + case F_MIXTURE: ac->mixture = (uint8_t)fs2StepClamp(ac->mixture, step, 0, 255); break; + case F_MAGNETOS: ac->magnetos = (uint8_t)fs2StepClamp(ac->magnetos, step, 0, 4); break; case F_CARB_HEAT: ac->carbHeatOn = !ac->carbHeatOn; break; - case F_FUEL_L: ac->fuelLeft = (uint8_t)applyStep(ac->fuelLeft, step, 0, 255); break; - case F_FUEL_R: ac->fuelRight = (uint8_t)applyStep(ac->fuelRight, step, 0, 255); break; - case F_FLAPS: ac->flaps = (uint8_t)applyStep(ac->flaps, step, 0, 255); break; + case F_FUEL_L: ac->fuelLeft = (uint8_t)fs2StepClamp(ac->fuelLeft, step, 0, 255); break; + case F_FUEL_R: ac->fuelRight = (uint8_t)fs2StepClamp(ac->fuelRight, step, 0, 255); break; + case F_FLAPS: ac->flaps = (uint8_t)fs2StepClamp(ac->flaps, step, 0, 255); break; case F_TRIM: { - int v = applyStep((int8_t)ac->trim, step, -84, 84); + int v = fs2StepClamp((int8_t)ac->trim, step, -84, 84); ac->trim = (uint8_t)v; break; } case F_LIGHTS: ac->lightsOn = !ac->lightsOn; break; case F_TOD_HOURS: { - int hr = applyStep(tod->hours, step, 0, 23); + int hr = fs2StepClamp(tod->hours, step, 0, 23); timeOfDaySet(tod, (uint8_t)hr, tod->minutes); break; } case F_TOD_MIN: { - int mn = applyStep(tod->minutes, step, 0, 59); + int mn = fs2StepClamp(tod->minutes, step, 0, 59); timeOfDaySet(tod, tod->hours, (uint8_t)mn); break; } @@ -247,7 +226,7 @@ static void modifyField(FieldIndexE fi, int step, break; } case F_WIND_DIR: wind->surface.direction = (uint8_t)(wind->surface.direction + step); break; - case F_WIND_SPD: wind->surface.magnitude = (uint8_t)applyStep(wind->surface.magnitude, step, 0, 255); break; + case F_WIND_SPD: wind->surface.magnitude = (uint8_t)fs2StepClamp(wind->surface.magnitude, step, 0, 255); break; case F_REALITY: ac->realityMode = !ac->realityMode; break; case F_NAV1_FREQ: radios->nav1Freq = (uint16_t)(radios->nav1Freq + step); break; case F_NAV2_FREQ: radios->nav2Freq = (uint16_t)(radios->nav2Freq + step); break; diff --git a/port/src/font.c b/port/src/font.c index d166622..b03c40a 100644 --- a/port/src/font.c +++ b/port/src/font.c @@ -106,3 +106,13 @@ int16_t fontDrawString(FramebufferT *fb, int16_t x, int16_t y, const char *s, Co } return cursor; } + + +int16_t fontDrawStringCentered(FramebufferT *fb, int16_t cx, int16_t y, const char *s, ColorE color) { + int len = 0; + while (s[len] != '\0') { + len++; + } + int16_t startX = (int16_t)(cx - (len * (FONT_WIDTH + 1)) / 2); + return fontDrawString(fb, startX, y, s, color); +} diff --git a/port/src/hud.c b/port/src/hud.c index faf070d..5b6362d 100644 --- a/port/src/hud.c +++ b/port/src/hud.c @@ -118,7 +118,7 @@ void hudDraw(FramebufferT *fb, const CameraT *cam) { fontDrawString(fb, 6, (int16_t)(panelTop + 12), buf, COLOR_ORANGE); // Heading: 0..359 deg derived from yaw byte angle. - int heading = (int)(cam->yaw * 360 / 256); + int heading = byteAngleToDegrees(cam->yaw); snprintf(buf, sizeof(buf), "HDG %03d", heading); fontDrawString(fb, 6, (int16_t)(panelTop + 24), buf, COLOR_ORANGE); diff --git a/port/src/instruments.c b/port/src/instruments.c index eee9466..9cc5108 100644 --- a/port/src/instruments.c +++ b/port/src/instruments.c @@ -59,8 +59,6 @@ static const GaugeT adfGauge = { 180, 175, 12 }; #define VOR2_FLAG_X 168 #define VOR2_FLAG_Y 173 -#define DEG2RAD 0.01745329251994f - typedef struct PixelListT { uint8_t count; @@ -68,7 +66,6 @@ typedef struct PixelListT { } PixelListT; -static void drawCenteredString(FramebufferT *fb, int16_t cx, int16_t cy, const char *s, ColorE color); static void drawControlIndicators(FramebufferT *fb, const AircraftT *ac); static void drawFailX(FramebufferT *fb, const GaugeT *g); static void drawHorizonDisc(FramebufferT *fb, const GaugeT *g, const AircraftT *ac); @@ -97,16 +94,6 @@ static const PixelListT plBall = { 12, plBallData }; static const PixelListT plFlapsTrimMixture = { 4, plFlapsTrimMixtureData }; -static void drawCenteredString(FramebufferT *fb, int16_t cx, int16_t cy, const char *s, ColorE color) { - int len = 0; - while (s[len] != '\0') { - len++; - } - int16_t startX = (int16_t)(cx - (len * (FONT_WIDTH + 1)) / 2); - fontDrawString(fb, startX, cy, s, color); -} - - // Draw the four FS2 control-position markers (aileron, rudder, // throttle, elevator). Each is a small pixel-list sprite that slides // along a fixed track on the panel. FS2 anchors: @@ -240,9 +227,7 @@ static void drawSlipSkidBall(FramebufferT *fb, const AircraftT *ac) { // bank pushes it sideways. Port's sideslip_q88 is computed in // aircraft.c as (yawRate - coordinated-turn yawRate); divide // down to the same -127..+127 range fs2SlipSkidIndex expects. - int32_t ssClamped = ac->sideslip_q88 / 8; - if (ssClamped > 127) ssClamped = 127; - if (ssClamped < -127) ssClamped = -127; + int32_t ssClamped = fs2ClampInt(ac->sideslip_q88 / 8, -127, 127); uint8_t idx = fs2SlipSkidIndex((int8_t)ssClamped); int16_t anchorX = (int16_t)((int)idx + 0x0E); drawPixelList(fb, anchorX, 0xB4, &plBall, COLOR_WHITE); @@ -403,6 +388,11 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra drawSlipSkidBall(fb, ac); } + // FS2 chunk5 DrawMagCompass paints `msg_magcompass` (a 3-digit + // heading readout), not a graphical compass card. Port renders + // the same digit readout via `readoutHeading` over in + // panelDigits.c -- no separate rose drawing needed. + // Aileron / rudder / throttle / elevator position markers. drawControlIndicators(fb, ac); @@ -415,10 +405,10 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra } if (ac->stalled) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 4, "STALL", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 4, "STALL", COLOR_WHITE); } if (ac->envelopeWarning && !ac->stalled) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 4, "VNE", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 4, "VNE", COLOR_WHITE); } if (ac->crashed) { // Mirrors FS2 chunk3 `HandleCrashOrSplash` / @@ -434,25 +424,25 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra case CRASH_NONE: default: msg = "CRASH"; break; } - drawCenteredString(fb, NATIVE_WIDTH / 2, 14, msg, COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 14, msg, COLOR_WHITE); } if (ac->demoMode && !ac->slewMode) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 24, "DEMO", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 24, "DEMO", COLOR_WHITE); } if (ac->editMode) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 24, "EDIT", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 24, "EDIT", COLOR_WHITE); } if (ac->realityMode) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 44, "REALITY", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 44, "REALITY", COLOR_WHITE); } // Engine-fault flags: chunk3 SetEngineFault01/23 OR-in the // bits; we surface them as a status line so the pilot has // something visible to react to. if (ac->engineFaults & AC_ENG_FAULT_LEFT) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 54, "L MAG FAIL", COLOR_HAZE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 54, "L MAG FAIL", COLOR_HAZE); } if (ac->engineFaults & AC_ENG_FAULT_RIGHT) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 64, "R MAG FAIL", COLOR_HAZE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 64, "R MAG FAIL", COLOR_HAZE); } // Magneto state indicator. Mirrors FS2 chunk5 DrawMagnetoState // (the string drawn alongside the magneto knob graphics on the @@ -468,10 +458,10 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra case 4: mag = "MAG START"; break; default: break; } - drawCenteredString(fb, NATIVE_WIDTH / 2, 74, mag, COLOR_ORANGE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 74, mag, COLOR_ORANGE); } if (ac->paused) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 84, "PAUSED", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 84, "PAUSED", COLOR_WHITE); } // Throttle / Mixture / Flaps / Trim / Fuel position markers. @@ -552,13 +542,13 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra } } if (ac->lightsOn) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 94, "LIGHTS ON", COLOR_ORANGE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 94, "LIGHTS ON", COLOR_ORANGE); } if (ac->carbHeatOn) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 104, "CARB HEAT", COLOR_ORANGE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 104, "CARB HEAT", COLOR_ORANGE); } if (ac->radarView) { - drawCenteredString(fb, NATIVE_WIDTH / 2, 4, "RADAR VIEW", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 4, "RADAR VIEW", COLOR_WHITE); } if (ac->viewDirection != VIEW_FORWARD) { const char *label = "VIEW"; @@ -569,12 +559,12 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra case VIEW_DOWN: label = "DOWN VIEW"; break; default: break; } - drawCenteredString(fb, NATIVE_WIDTH / 2, 34, label, COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 34, label, COLOR_WHITE); } if (ac->slewMode) { // FS2 chunk3 `DrawSlewOverlays`: " 00000 NORTH " at row // 2 col $0A, " 00000 EAST " at row 2 col $4C. - drawCenteredString(fb, NATIVE_WIDTH / 2, 24, "SLEW", COLOR_WHITE); + fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 24, "SLEW", COLOR_WHITE); if (ac->showSlewDigits) { char buf[24]; // worldX/Z are Q16.16; truncate to integer world unit. @@ -607,9 +597,7 @@ static void drawVorCdiNeedle(FramebufferT *fb, int16_t centreX, int16_t centreY, return; } - int defl = deflectionDeg; - if (defl > VOR_CDI_FULL_DEG) defl = VOR_CDI_FULL_DEG; - if (defl < -VOR_CDI_FULL_DEG) defl = -VOR_CDI_FULL_DEG; + int defl = fs2ClampInt(deflectionDeg, -VOR_CDI_FULL_DEG, VOR_CDI_FULL_DEG); int xOffset = (defl * VOR_CDI_HALFWIDTH) / VOR_CDI_FULL_DEG; int16_t nx = (int16_t)(centreX + xOffset); for (int dy = 1; dy <= VOR_CDI_NEEDLE_H; dy++) { diff --git a/port/src/main.c b/port/src/main.c index 571339d..bd410c1 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -21,6 +21,7 @@ #include "radios.h" #include "renderer.h" #include "chunk5Transform.h" +#include "fs2math.h" #include "sceneryData.h" #include "sceneryProjection.h" #include "sceneryVm.h" @@ -400,42 +401,9 @@ static int runScreenshot(const char *path) { SceneryRegionE shotRegion = SCENERY_FS2_1; const char *regionEnv = getenv("SCENERY_REGION"); if (regionEnv != NULL) { - if (strcmp(regionEnv, "FS2.1") == 0) { - shotRegion = SCENERY_FS2_1; - } else if (strcmp(regionEnv, "FS2.1_chicago") == 0) { - shotRegion = SCENERY_FS2_1_CHICAGO; - } else if (strcmp(regionEnv, "FS2.1_la") == 0) { - shotRegion = SCENERY_FS2_1_LA; - } else if (strcmp(regionEnv, "FS2.1_seattle") == 0) { - shotRegion = SCENERY_FS2_1_SEATTLE; - } else if (strcmp(regionEnv, "FS2.1_ny") == 0) { - shotRegion = SCENERY_FS2_1_NY; - } else if (strcmp(regionEnv, "SD1") == 0) { - shotRegion = SCENERY_SD1; - } else if (strcmp(regionEnv, "SD2") == 0) { - shotRegion = SCENERY_SD2; - } else if (strcmp(regionEnv, "SD3") == 0) { - shotRegion = SCENERY_SD3; - } else if (strcmp(regionEnv, "SD4") == 0) { - shotRegion = SCENERY_SD4; - } else if (strcmp(regionEnv, "SD5") == 0) { - shotRegion = SCENERY_SD5; - } else if (strcmp(regionEnv, "SD6") == 0) { - shotRegion = SCENERY_SD6; - } else if (strcmp(regionEnv, "SD7A") == 0) { - shotRegion = SCENERY_SD7A; - } else if (strcmp(regionEnv, "SD7B") == 0) { - shotRegion = SCENERY_SD7B; - } else if (strcmp(regionEnv, "SD11") == 0) { - shotRegion = SCENERY_SD11; - } else if (strcmp(regionEnv, "SD13") == 0) { - shotRegion = SCENERY_SD13; - } else if (strcmp(regionEnv, "SD14A") == 0) { - shotRegion = SCENERY_SD14A; - } else if (strcmp(regionEnv, "SD14B") == 0) { - shotRegion = SCENERY_SD14B; - } else if (strcmp(regionEnv, "SDS1") == 0) { - shotRegion = SCENERY_SDS1; + SceneryRegionE r = sceneryDataRegionFromName(regionEnv); + if (r != SCENERY_NONE) { + shotRegion = r; } } sceneryDataLoad(shotRegion, &screenshotScenery); @@ -917,6 +885,63 @@ int main(int argc, char **argv) { sceneryDataFree(&scenery); } scenery = alt; + // Each city's .SD ships its own primary + // airport with the COM station record + // ($1E) at a fixed scenery position -- + // that's where we want the player parked + // on first load. + // + // The boot Meigs reference (x=95.6m, + // y=3m, z=268m) is what FS2 sets in + // chunk5 InitialZeroPageData; for the + // other regions FS2 leaves the aircraft + // at Meigs and expects the player to + // fly over -- but for parity-with- + // expectation we drop the player at + // the city's primary airport instead. + // Coordinates below are from sweeping + // each .SD's $1E COM record positions + // against the dispatcher's section + // culls (= the position that both + // matches a COM record AND has at + // least one in-frustum scenery section + // visible from that position). + // + // Y=3 is the chunk5 IntegrateClimbRate + // ground clamp = "parked on runway". + int32_t sx_m = 96, sy_m = 3, sz_m = 268; + switch (title.region) { + case SCENERY_FS2_1_CHICAGO: + // KCGX / Meigs Field -- + // FS2's iconic Chicago + // start. Same coords as + // the FS2.1 base boot + // (= peninsula in Lake + // Michigan just south of + // downtown). + sx_m = 96; sz_m = 268; + break; + case SCENERY_FS2_1_LA: + // KLAX (Los Angeles Intl). + sx_m = 200; sz_m = 0; + break; + case SCENERY_FS2_1_SEATTLE: + // KSEA (Sea-Tac). + sx_m = 970; sz_m = 0; + break; + case SCENERY_FS2_1_NY: + // KJFK (New York Kennedy). + sx_m = 400; sz_m = 0; + break; + default: + break; + } + aircraft.worldX = AC_WORLD_UNITS(sx_m); + aircraft.worldY = AC_WORLD_UNITS(sy_m); + aircraft.worldZ = AC_WORLD_UNITS(sz_m); + aircraft.throttle = 0; // parked + aircraft.forwardSpeed = 0; + aircraft.onGround = true; } } @@ -1067,60 +1092,40 @@ int main(int argc, char **argv) { // elevator trim by 4 byte-units; long-held keys // accumulate. Trim biases the pitch yoke output. if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_HOME) { - int t = (int)aircraft.trim - 4; - if (t < -127) t = -127; - aircraft.trim = (int8_t)t; + aircraft.trim = (int8_t)fs2StepClamp(aircraft.trim, -4, -127, 127); } if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_END) { - int t = (int)aircraft.trim + 4; - if (t > 127) t = 127; - aircraft.trim = (int8_t)t; + aircraft.trim = (int8_t)fs2StepClamp(aircraft.trim, +4, -127, 127); } // Flap controls (chunk5 UpdateFlapsIndicator). // INSERT extends flaps in 32-byte-unit clicks; // DELETE retracts. if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_INSERT) { - int f = (int)aircraft.flaps + 32; - if (f > 255) f = 255; - aircraft.flaps = (uint8_t)f; + aircraft.flaps = (uint8_t)fs2StepClamp(aircraft.flaps, +32, 0, 255); } if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_DELETE) { - int f = (int)aircraft.flaps - 32; - if (f < 0) f = 0; - aircraft.flaps = (uint8_t)f; + aircraft.flaps = (uint8_t)fs2StepClamp(aircraft.flaps, -32, 0, 255); } // Mixture controls (chunk5 UpdateMixtureControl). // PAGEUP+Shift = richer, PAGEDN+Shift = leaner. if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT) && ev.key.keysym.sym == SDLK_PAGEUP) { - int m = (int)aircraft.mixture + 16; - if (m > 255) m = 255; - aircraft.mixture = (uint8_t)m; + aircraft.mixture = (uint8_t)fs2StepClamp(aircraft.mixture, +16, 0, 255); } if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT) && ev.key.keysym.sym == SDLK_PAGEDOWN) { - int m = (int)aircraft.mixture - 16; - if (m < 0) m = 0; - aircraft.mixture = (uint8_t)m; + aircraft.mixture = (uint8_t)fs2StepClamp(aircraft.mixture, -16, 0, 255); } if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_BACKQUOTE) { aircraft.radarView = !aircraft.radarView; } if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_LEFTBRACKET && aircraft.radarView) { - int z = (int)aircraft.radarZoom * 3 / 2; - if (z > 32767) { - z = 32767; - } - aircraft.radarZoom = (int16_t)z; + aircraft.radarZoom = (int16_t)fs2ClampInt((int)aircraft.radarZoom * 3 / 2, 128, 32767); } if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_RIGHTBRACKET && aircraft.radarView) { - int z = (int)aircraft.radarZoom * 2 / 3; - if (z < 128) { // 0.5 in Q8.8 - z = 128; - } - aircraft.radarZoom = (int16_t)z; + aircraft.radarZoom = (int16_t)fs2ClampInt((int)aircraft.radarZoom * 2 / 3, 128, 32767); } // Joystick button events. Trigger (0) fires // gun, second (1) drops a bomb, third (2) @@ -1180,8 +1185,11 @@ int main(int argc, char **argv) { if (ev.type == SDL_KEYDOWN && radioInputMode != RADIO_INPUT_NONE) { SDL_Keycode k = ev.key.keysym.sym; int digit = -1; - if (k >= SDLK_0 && k <= SDLK_9) digit = (int)(k - SDLK_0); - else if (k >= SDLK_KP_0 && k <= SDLK_KP_9) digit = (int)(k - SDLK_KP_0); + if (k >= SDLK_0 && k <= SDLK_9) { + digit = (int)(k - SDLK_0); + } else if (k >= SDLK_KP_0 && k <= SDLK_KP_9) { + digit = (int)(k - SDLK_KP_0); + } if (digit >= 0) { RadioE w = (radioInputMode == RADIO_INPUT_NAV1) ? RADIO_NAV1 : (radioInputMode == RADIO_INPUT_NAV2) ? RADIO_NAV2 diff --git a/port/src/panelDigits.c b/port/src/panelDigits.c index bb7fb75..c72ad0f 100644 --- a/port/src/panelDigits.c +++ b/port/src/panelDigits.c @@ -15,6 +15,7 @@ #include #include +#include "camera.h" #include "font.h" #include "panelDigits.h" @@ -69,15 +70,7 @@ static void drawReadout(FramebufferT *fb, const ReadoutT *r, const char *text) { 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); - - // Centre our narrower text inside the erased area. - int textLen = 0; - while (text[textLen] != '\0') { - textLen++; - } - int16_t textW = (int16_t)(textLen * (FONT_WIDTH + 1)); - int16_t textX = (int16_t)(r->x + (clearW - textW) / 2); - fontDrawString(fb, textX, r->y, text, COLOR_WHITE); + fontDrawStringCentered(fb, (int16_t)(r->x + clearW / 2), r->y, text, COLOR_WHITE); } @@ -89,6 +82,49 @@ void panelDigitsDraw(FramebufferT *fb, const AircraftT *ac, const RadiosT *radio 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 @@ -123,7 +159,7 @@ void panelDigitsDraw(FramebufferT *fb, const AircraftT *ac, const RadiosT *radio drawReadout(fb, &readoutHeading, "---"); drawReadout(fb, &readoutRecip, "---"); } else { - int heading = ((int)ac->yaw * 360) / 256; + int heading = byteAngleToDegrees(ac->yaw); heading %= 360; snprintf(buf, sizeof(buf), "%03d", heading); drawReadout(fb, &readoutHeading, buf); @@ -134,8 +170,8 @@ void panelDigitsDraw(FramebufferT *fb, const AircraftT *ac, const RadiosT *radio // VOR1 / VOR2 OBS course + reciprocal. obs is byte angle; // convert to degrees 0..359. - int obs1Deg = ((int)radios->nav1Obs * 360) / 256; - int obs2Deg = ((int)radios->nav2Obs * 360) / 256; + 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); diff --git a/port/src/radios.c b/port/src/radios.c index 56947ec..5e6b6b7 100644 --- a/port/src/radios.c +++ b/port/src/radios.c @@ -1,6 +1,7 @@ // NAV / COM / ADF radio state and lookups. See radios.h. #include +#include "fs2math.h" #include "math6502.h" #include "radios.h" @@ -280,8 +281,7 @@ void radiosEnterDigit(RadiosT *r, RadioE which, uint8_t digit) { khz = (khz % 100) * 10 + digit; // shift the // 3-digit display, dropping the leading digit and // bringing the new digit in at the units place. - if (khz < ADF_FREQ_MIN_KHZ) khz = ADF_FREQ_MIN_KHZ; - if (khz > ADF_FREQ_MAX_KHZ) khz = ADF_FREQ_MAX_KHZ; + khz = fs2ClampInt(khz, ADF_FREQ_MIN_KHZ, ADF_FREQ_MAX_KHZ); r->adfFreq = khzToBcdAdf(khz); return; } @@ -299,8 +299,7 @@ void radiosEnterDigit(RadiosT *r, RadioE which, uint8_t digit) { hund = (hund / 5) * 5; int minHund = (which == RADIO_COM1) ? 11800 : 10800; int maxHund = (which == RADIO_COM1) ? 13695 : 11795; - if (hund < minHund) hund = minHund; - if (hund > maxHund) hund = maxHund; + hund = fs2ClampInt(hund, minHund, maxHund); *freqPtr = intToBcdNavCom(hund); } @@ -346,9 +345,7 @@ void radiosUpdate(RadiosT *r, const AircraftT *ac) { // mapped to a degree count for the CDI. uint8_t fromBearing = (uint8_t)(r->nav1RelativeBearing + 128); int8_t radialDelta = (int8_t)(fromBearing - r->nav1Obs); - int devDeg = ((int)radialDelta * 360 + 128) / 256; - if (devDeg > 32) devDeg = 32; - if (devDeg < -32) devDeg = -32; + int devDeg = fs2ClampInt(((int)radialDelta * 360 + 128) / 256, -32, 32); r->nav1NeedleDefl = (int8_t)devDeg; // TO/FROM: |radialDelta| < 64 (= 90 deg) means // the aircraft is on the OBS-radial half -> @@ -373,9 +370,7 @@ void radiosUpdate(RadiosT *r, const AircraftT *ac) { r->nav2Dme = (uint16_t)((int32_t)d / AC_SCENERY_UNITS_PER_NM); uint8_t fromBearing = (uint8_t)(r->nav2RelativeBearing + 128); int8_t radialDelta = (int8_t)(fromBearing - r->nav2Obs); - int devDeg = ((int)radialDelta * 360 + 128) / 256; - if (devDeg > 32) devDeg = 32; - if (devDeg < -32) devDeg = -32; + int devDeg = fs2ClampInt(((int)radialDelta * 360 + 128) / 256, -32, 32); r->nav2NeedleDefl = (int8_t)devDeg; int adelta = radialDelta < 0 ? -radialDelta : radialDelta; r->nav2Flag = (adelta < 64) ? VOR_FLAG_FR : VOR_FLAG_TO; diff --git a/port/src/renderer.c b/port/src/renderer.c index cbf76aa..0ab718c 100644 --- a/port/src/renderer.c +++ b/port/src/renderer.c @@ -258,16 +258,6 @@ void rendererSetFillColors(RenderStateT *state, ColorE fill, ColorE altFill) { } -void rendererSwapFillColors(RenderStateT *state) { - ColorE tmp = state->fillColor; - state->fillColor = state->altFillColor; - state->altFillColor = tmp; - uint8_t htmp = state->hiresFill; - state->hiresFill = state->hiresAltFill; - state->hiresAltFill = htmp; -} - - // Set the chunk5 hires color code directly. Used by the chunk5 $12 // SetColor handler so the hires bitplane gets bit-faithful color // (rather than going through the modern palette mapping). diff --git a/port/src/sceneryData.c b/port/src/sceneryData.c index b5499be..3841a38 100644 --- a/port/src/sceneryData.c +++ b/port/src/sceneryData.c @@ -12,31 +12,36 @@ typedef struct RegionMetaT { - const char *fileName; // basename inside downloads/scenery/extracted/ + const char *fileName; // RAM-dump basename (sceneryRam_.bin) + const char *sdSourceFile; // demand-load .SD source in downloads/scenery/extracted/ const char *displayName; } RegionMetaT; +// The four FS2.1 region variants are different city subsections of the +// SAME base disk image. They all share `FS2.1` as the demand-load +// source so the HEADER opcode can pull section bytes from disk at +// runtime regardless of which city was the boot region. static const RegionMetaT regionMeta[SCENERY_REGION_COUNT] = { - [SCENERY_NONE] = { NULL, "(no scenery)" }, - [SCENERY_FS2_1] = { "FS2.1", "FS2 base disk - WWI Ace training" }, - [SCENERY_FS2_1_CHICAGO] = { "FS2.1_chicago", "FS2 base disk - Chicago / Meigs Field" }, - [SCENERY_FS2_1_LA] = { "FS2.1_la", "FS2 base disk - Los Angeles" }, - [SCENERY_FS2_1_SEATTLE] = { "FS2.1_seattle", "FS2 base disk - Seattle" }, - [SCENERY_FS2_1_NY] = { "FS2.1_ny", "FS2 base disk - New York / Kennedy" }, - [SCENERY_SD1] = { "A2.SD1", "Dallas-Ft.Worth, Houston, San Antonio" }, - [SCENERY_SD2] = { "A2.SD2", "Phoenix, Albuquerque, El Paso" }, - [SCENERY_SD3] = { "A2.SD3", "San Francisco, Los Angeles, Las Vegas" }, - [SCENERY_SD4] = { "A2.SD4", "Klamath Falls, Seattle, Great Falls" }, - [SCENERY_SD5] = { "A2.SD5", "Salt Lake City, Cheyenne, Denver" }, - [SCENERY_SD6] = { "A2.SD6", "Omaha, Wichita, Kansas City" }, - [SCENERY_SD7A] = { "A2.SD7A", "Washington, Charlotte" }, - [SCENERY_SD7B] = { "A2.SD7B", "Jacksonville, Miami" }, - [SCENERY_SD11] = { "A2.SD11", "Lake Huron, Detroit" }, - [SCENERY_SD13] = { "A2.SD13", "Japan - Tokyo, Osaka" }, - [SCENERY_SD14A] = { "A2.SD14A", "Western European Tour (UK, N. France)" }, - [SCENERY_SD14B] = { "A2.SD14B", "Western European Tour (N. France, W. Germany)" }, - [SCENERY_SDS1] = { "A2.SDS1", "STAR San Francisco & The Bay Area" } + [SCENERY_NONE] = { NULL, NULL, "(no scenery)" }, + [SCENERY_FS2_1] = { "FS2.1", "FS2.1", "FS2 base disk - WWI Ace training" }, + [SCENERY_FS2_1_CHICAGO] = { "FS2.1_chicago", "FS2.1", "FS2 base disk - Chicago / Meigs Field" }, + [SCENERY_FS2_1_LA] = { "FS2.1_la", "FS2.1", "FS2 base disk - Los Angeles" }, + [SCENERY_FS2_1_SEATTLE] = { "FS2.1_seattle", "FS2.1", "FS2 base disk - Seattle" }, + [SCENERY_FS2_1_NY] = { "FS2.1_ny", "FS2.1", "FS2 base disk - New York / Kennedy" }, + [SCENERY_SD1] = { "A2.SD1", "A2.SD1", "Dallas-Ft.Worth, Houston, San Antonio" }, + [SCENERY_SD2] = { "A2.SD2", "A2.SD2", "Phoenix, Albuquerque, El Paso" }, + [SCENERY_SD3] = { "A2.SD3", "A2.SD3", "San Francisco, Los Angeles, Las Vegas" }, + [SCENERY_SD4] = { "A2.SD4", "A2.SD4", "Klamath Falls, Seattle, Great Falls" }, + [SCENERY_SD5] = { "A2.SD5", "A2.SD5", "Salt Lake City, Cheyenne, Denver" }, + [SCENERY_SD6] = { "A2.SD6", "A2.SD6", "Omaha, Wichita, Kansas City" }, + [SCENERY_SD7A] = { "A2.SD7A", "A2.SD7A", "Washington, Charlotte" }, + [SCENERY_SD7B] = { "A2.SD7B", "A2.SD7B", "Jacksonville, Miami" }, + [SCENERY_SD11] = { "A2.SD11", "A2.SD11", "Lake Huron, Detroit" }, + [SCENERY_SD13] = { "A2.SD13", "A2.SD13", "Japan - Tokyo, Osaka" }, + [SCENERY_SD14A] = { "A2.SD14A", "A2.SD14A", "Western European Tour (UK, N. France)" }, + [SCENERY_SD14B] = { "A2.SD14B", "A2.SD14B", "Western European Tour (N. France, W. Germany)" }, + [SCENERY_SDS1] = { "A2.SDS1", "A2.SDS1", "STAR San Francisco & The Bay Area" } }; @@ -200,7 +205,12 @@ static bool loadRawSDFile(SceneryDataT *out, const RegionMetaT *meta) { "../../downloads/scenery/extracted/", "/home/scott/claude/flight/downloads/scenery/extracted/" }; - FILE *f = openFileSearch(meta->fileName, prefixes, + const char *sourceName = meta->sdSourceFile != NULL + ? meta->sdSourceFile : meta->fileName; + if (sourceName == NULL) { + return false; + } + FILE *f = openFileSearch(sourceName, prefixes, sizeof(prefixes) / sizeof(prefixes[0])); if (f == NULL) { return false; @@ -340,3 +350,25 @@ const char *sceneryDataRegionName(SceneryRegionE region) { } return regionMeta[region].displayName; } + + +SceneryRegionE sceneryDataRegionFromName(const char *name) { + if (name == NULL) { + return SCENERY_NONE; + } + for (int i = SCENERY_NONE + 1; i < SCENERY_REGION_COUNT; i++) { + const char *fn = regionMeta[i].fileName; + if (fn == NULL) { + continue; + } + if (strcmp(fn, name) == 0) { + return (SceneryRegionE)i; + } + // Accept the bare "SDxxx" / "SDS1" form without the + // leading "A2." that the on-disk filename carries. + if (strncmp(fn, "A2.", 3) == 0 && strcmp(fn + 3, name) == 0) { + return (SceneryRegionE)i; + } + } + return SCENERY_NONE; +} diff --git a/port/src/sceneryVm.c b/port/src/sceneryVm.c index 1e9fbdc..b471903 100644 --- a/port/src/sceneryVm.c +++ b/port/src/sceneryVm.c @@ -346,19 +346,26 @@ static void doHeader(SceneryStateT *state) { ram[0x08EA + i] = 0; } - // SCENERY_DEMAND_LOAD env var gates the .SD demand-load. Set - // to "0" to load from byte 0 of the section (the natural - // chunk4 ASM behaviour); set to a digit (e.g. "14") to apply a - // source-byte SKIP, which is what MAME's captured RAM appears - // to have done (RAM[$A887] matches sid $44 byte 14, not byte 0). - // The skip lets us experimentally find which entry point makes - // the chunk5 VM walk into the water-color SetColor at $B781. - // Default behaviour: don't demand-load (keep captured-RAM - // residue, which is the only known-working path so far). + // Demand-load the section's bytes from the .SD file into the + // dispatcher's working RAM (mirrors chunk5 LA63A → chunk3's + // SmartPort `ReadBlocks` call). Section ID → file offset uses + // ASM-faithful math from chunk4 `ComputeBlockFromSector` / + // `FetchSectorFromDisk`. + // + // Without this, boot Meigs runs from the captured RAM image + // baked into `port/sceneryRam_FS2.1.bin` (which is byte-faithful + // for that one section) but new sections (= city disks, sectors + // outside the captured set, anything the dispatcher visits while + // flying past the initial cull bounds) get the RAM-residue + // bytes from before, which are stale. + // + // SCENERY_DEMAND_LOAD env still works as a source-byte SKIP + // for offline experiments; default (unset) is `srcSkip = 0` = + // load from byte 0 of each section (= the natural ASM + // behaviour). const char *envDemandLoad = getenv("SCENERY_DEMAND_LOAD"); - if (state->sceneryFile != NULL && state->sceneryFileSize > 0 - && envDemandLoad != NULL) { - int srcSkip = atoi(envDemandLoad); + if (state->sceneryFile != NULL && state->sceneryFileSize > 0) { + int srcSkip = envDemandLoad != NULL ? atoi(envDemandLoad) : 0; if (srcSkip < 0 || srcSkip > 64) srcSkip = 0; uint8_t count = ram[0x08E6]; uint16_t dest = (uint16_t)ram[0x08E7] @@ -407,6 +414,10 @@ static void doHeader(SceneryStateT *state) { } ram[addr] = state->sceneryFile[srcOff + i]; } + if (getenv("SCENERY_DEMAND_TRACE") != NULL) { + fprintf(stderr, "doHeader: demand-load sid=$%02X cnt=$%02X dest=$%04X srcOff=$%06X len=$%X\n", + sectionId, count, dest, srcOff, length); + } } } } diff --git a/port/src/ww1ace.c b/port/src/ww1ace.c index bbcaf91..120b9e9 100644 --- a/port/src/ww1ace.c +++ b/port/src/ww1ace.c @@ -73,12 +73,6 @@ static void enemyFireAtPlayer(WW1AceStateT *s, WW1EnemyT *e, static void enemyManeuver(WW1AceStateT *s, WW1EnemyT *e, int32_t playerX, int32_t playerY, int32_t playerZ); -// Distance helpers operate on metres (Q16.16 worldUnit >> 16). Squaring -// would overflow int32 in Q16.16, so we drop to integer metres before -// squaring. -static int32_t metresFromQ1616(int32_t v_q1616); - - // Tiny LCG (matches Mike Brennan's classic glibc parameters in spirit; // good enough for AI jitter). static uint16_t rngNext(WW1AceStateT *s) { @@ -156,11 +150,6 @@ static void respawnEnemy(WW1AceStateT *s, WW1EnemyT *e, int32_t playerX, int32_t } -static int32_t metresFromQ1616(int32_t v_q1616) { - return v_q1616 >> CAM_POS_FRACT_BITS; -} - - void ww1aceDrawWarReport(const WW1AceStateT *s, FramebufferT *fb) { framebufferFillRect(fb, 0, 0, NATIVE_WIDTH, NATIVE_HEIGHT, COLOR_BLACK); fontDrawString(fb, 60, 8, "***** WAR REPORT *****", COLOR_WHITE); @@ -183,30 +172,6 @@ void ww1aceDrawWarReport(const WW1AceStateT *s, FramebufferT *fb) { } -void ww1aceDropBomb(WW1AceStateT *s) { - if (!s->enabled) { - return; - } - if (s->bombs == 0) { - return; - } - s->bombs--; - // Drop a bomb from the player's reported position with a small - // downward velocity. Hit detection happens in updateBombs when - // the bomb impacts ground (Y <= 0). Caller passes player coords - // via ww1aceUpdate; we store nothing here since the player's - // world position is read from outside in ww1aceUpdate. For the - // drop, we use position (0, alt, 0) relative -- the next - // update tick we'll seed real coords. To keep the drop accurate - // we expose a second helper ww1aceDropBombAt below; this - // legacy entry just bumps the counter. - if (s->bombHits < 0xFFFF) { - // Pessimistic: don't auto-count as a hit anymore. - // Hits are awarded in updateBombs() on actual impact. - } -} - - void ww1aceDropBombAt(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ, int16_t playerVelX_q88, int16_t playerVelZ_q88) { if (!s->enabled || s->bombs == 0) { diff --git a/src/sceneryVm.c b/src/sceneryVm.c deleted file mode 100644 index e69de29..0000000