Checkpoint.

This commit is contained in:
Scott Duensing 2026-05-14 10:03:23 -05:00
parent 9f8b0de850
commit a9561702fc
23 changed files with 329 additions and 289 deletions

View file

@ -59,6 +59,11 @@ typedef enum ViewDirectionE {
// Compose a uint8 throttle/flaps/mixture value from a percent 0..100. // Compose a uint8 throttle/flaps/mixture value from a percent 0..100.
#define AC_BYTE_PCT(p) ((uint8_t)(((int)(p) * 255) / 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 // Reality-mode instrument failure bits. Mirrors chunk3
// `InstrumentOperationalFlags` (init $FF = all good). The // `InstrumentOperationalFlags` (init $FF = all good). The
// chunk3 `FailureProcTable` clears one of these bits or sets one of // chunk3 `FailureProcTable` clears one of these bits or sets one of

View file

@ -25,6 +25,22 @@
#define CAM_ROT_FRACT_BITS 15 #define CAM_ROT_FRACT_BITS 15
#define CAM_ROT_ONE (1 << CAM_ROT_FRACT_BITS) #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 { typedef struct CameraT {
int32_t worldX; // Q16.16 world units int32_t worldX; // Q16.16 world units
int32_t worldY; int32_t worldY;

View file

@ -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. // 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); 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 #endif

View file

@ -61,4 +61,22 @@ uint8_t fs2VsiNeedlePos(int16_t value16);
// `UpdateSlipSkidIndicator` (chunk4 L2497) consumes. // `UpdateSlipSkidIndicator` (chunk4 L2497) consumes.
uint8_t fs2SlipSkidIndex(int8_t slipValue); 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 #endif

View file

@ -29,7 +29,6 @@ void rendererBegin(RenderStateT *state, FramebufferT *fb);
void rendererSetDrawColor(RenderStateT *state, ColorE color); void rendererSetDrawColor(RenderStateT *state, ColorE color);
void rendererSetHiresColor(RenderStateT *state, uint8_t hiresCode); void rendererSetHiresColor(RenderStateT *state, uint8_t hiresCode);
void rendererSetFillColors(RenderStateT *state, ColorE fill, ColorE altFill); 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); void rendererDrawLine(RenderStateT *state, int16_t x1, int16_t y1, int16_t x2, int16_t y2);

View file

@ -65,4 +65,10 @@ void sceneryDataFree(SceneryDataT *out);
// Human-readable name for a region. Always returns a non-NULL string. // Human-readable name for a region. Always returns a non-NULL string.
const char *sceneryDataRegionName(SceneryRegionE region); 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 #endif

View file

@ -22,16 +22,6 @@
// Default scale factor (window pixels per native pixel). // Default scale factor (window pixels per native pixel).
#define WINDOW_SCALE 4 #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 // Cohen-Sutherland-style outcode bits for frustum clipping. Mirrors
// the layout used by the original disassembly so the clipper logic // the layout used by the original disassembly so the clipper logic
// can be ported one-for-one. // can be ported one-for-one.

View file

@ -97,11 +97,6 @@ void ww1aceInit(WW1AceStateT *s);
// score/bomb counters. Player coords are Q16.16 world-units. // score/bomb counters. Player coords are Q16.16 world-units.
void ww1aceToggle(WW1AceStateT *s, int32_t playerX, int32_t playerZ); 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 // 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 // horizontal velocity (Q8.8 metres/frame). The bomb then falls under
// gravity in `ww1aceUpdate` and tries to score a hit on a ground // gravity in `ww1aceUpdate` and tries to score a hit on a ground

View file

@ -26,8 +26,8 @@
#define DEFAULT_DECAY_K 218 // ~0.85 self-centring #define DEFAULT_DECAY_K 218 // ~0.85 self-centring
#define RUDDER_DECAY_K 179 // ~0.70 #define RUDDER_DECAY_K 179 // ~0.70
// Speed envelope. // Speed envelope. AC_MAX_FORWARD_SPEED_Q88 lives in aircraft.h so
#define MAX_FORWARD_SPEED_Q88 410 // 1.6 world-units / frame // audio.c can read it without duplicating the literal.
#define DRAG_K 1 // 1/256 ~ 0.4% #define DRAG_K 1 // 1/256 ~ 0.4%
// Lift / gravity. // Lift / gravity.
@ -217,26 +217,23 @@ void aircraftInit(AircraftT *ac) {
static AircraftT recoveryBuf; static AircraftT recoveryBuf;
static bool recoveryValid;
void aircraftArmRecovery(AircraftT *ac) { void aircraftArmRecovery(AircraftT *ac) {
recoveryBuf = *ac; // Set the snapshot flag before capturing so the restored state
recoveryValid = true; // also reports armed -- successive crashes can reuse the same
// snapshot without needing another arm pass.
ac->hasRecoverySnapshot = true; ac->hasRecoverySnapshot = true;
recoveryBuf = *ac;
} }
void aircraftRestoreRecovery(AircraftT *ac) { void aircraftRestoreRecovery(AircraftT *ac) {
if (!recoveryValid) { if (!ac->hasRecoverySnapshot) {
return; return;
} }
// Preserve the recovery-armed flag so further crashes can use
// the same snapshot.
bool hasSnap = ac->hasRecoverySnapshot;
*ac = recoveryBuf; *ac = recoveryBuf;
ac->hasRecoverySnapshot = hasSnap; ac->crashed = false;
ac->crashed = false;
ac->crashType = CRASH_NONE; 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). // (using the >> 7 form keeps int8 input valued at full scale).
// Trim biases yokeVert -- chunk5 RefreshElevatorIndicator // Trim biases yokeVert -- chunk5 RefreshElevatorIndicator
// accumulates trim into the elevator command. // accumulates trim into the elevator command.
int yokePlusTrim = (int)(int8_t)ac->yokeVert + (int)(int8_t)ac->trim; int yokePlusTrim = fs2ClampInt((int)(int8_t)ac->yokeVert + (int)(int8_t)ac->trim, -127, 127);
if (yokePlusTrim > 127) yokePlusTrim = 127;
if (yokePlusTrim < -127) yokePlusTrim = -127;
int16_t pitchTarget = (int16_t)(((int32_t)yokePlusTrim * MAX_PITCH_RATE_Q88) / 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); 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); 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) { if (ac->fuelLeft == 0 && ac->fuelRight == 0) {
effectiveThrottle = 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 engineDelta = (int16_t)((((int32_t)targetSpeed - ac->forwardSpeed) * ENGINE_SMOOTH_K) >> 8);
int16_t dragDelta = (int16_t)(((int32_t)ac->forwardSpeed * DRAG_K) >> 8); int16_t dragDelta = (int16_t)(((int32_t)ac->forwardSpeed * DRAG_K) >> 8);
ac->forwardSpeed = q88Add(ac->forwardSpeed, q88Add(engineDelta, (int16_t)-dragDelta)); ac->forwardSpeed = q88Add(ac->forwardSpeed, q88Add(engineDelta, (int16_t)-dragDelta));

View file

@ -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) { void audioUpdate(const AircraftT *ac) {
if (audio.device == 0) { if (audio.device == 0) {
return; return;
@ -207,9 +202,13 @@ void audioUpdate(const AircraftT *ac) {
// single SDL_LockAudioDevice store; the audio synth itself // single SDL_LockAudioDevice store; the audio synth itself
// still runs in float because SDL streams float samples. // still runs in float because SDL streams float samples.
// speedFraction : Q8.8 from 0..256 (= 0.0..1.0) // speedFraction : Q8.8 from 0..256 (= 0.0..1.0)
int speedT = (int)ac->forwardSpeed * 256 / AUDIO_SPEED_FULL_Q88; int speedT = (int)ac->forwardSpeed * 256 / AC_MAX_FORWARD_SPEED_Q88;
if (speedT < 0) speedT = 0; if (speedT < 0) {
if (speedT > 256) speedT = 256; speedT = 0;
}
if (speedT > 256) {
speedT = 256;
}
// freqQ88 = base + speedT * (max - base) / 256 // freqQ88 = base + speedT * (max - base) / 256
int32_t freqRange = ENGINE_MAX_HZ - ENGINE_BASE_HZ; int32_t freqRange = ENGINE_MAX_HZ - ENGINE_BASE_HZ;
int32_t freqQ88 = ((int32_t)ENGINE_BASE_HZ << 8) int32_t freqQ88 = ((int32_t)ENGINE_BASE_HZ << 8)
@ -271,7 +270,9 @@ void audioUpdate(const AircraftT *ac) {
if (!ac->onGround) { if (!ac->onGround) {
float speedFrac = (float)speedT / 256.0f; float speedFrac = (float)speedT / 256.0f;
windAmp = sqrtf(speedFrac); windAmp = sqrtf(speedFrac);
if (windAmp > 1.0f) windAmp = 1.0f; if (windAmp > 1.0f) {
windAmp = 1.0f;
}
} }
SDL_LockAudioDevice(audio.device); SDL_LockAudioDevice(audio.device);

View file

@ -4,6 +4,7 @@
#include "camera.h" #include "camera.h"
#include "chunk5Setup.h" #include "chunk5Setup.h"
#include "fs2math.h"
#include "math6502.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. // 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]) { void cameraGet2x3Matrix(const CameraT *cam, int8_t outRowX[3], int8_t outRowZ[3]) {
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
int x = cam->rot[i][0] >> 8; int x = fs2ClampInt(cam->rot[i][0] >> 8, -128, 127);
int z = cam->rot[i][2] >> 8; int z = fs2ClampInt(cam->rot[i][2] >> 8, -128, 127);
if (x > 127) x = 127;
if (x < -128) x = -128;
if (z > 127) z = 127;
if (z < -128) z = -128;
outRowX[i] = (int8_t)x; outRowX[i] = (int8_t)x;
outRowZ[i] = (int8_t)z; outRowZ[i] = (int8_t)z;
} }

View file

@ -8,8 +8,10 @@
#include <stdio.h> #include <stdio.h>
#include "editor.h" #include "editor.h"
#include "aircraft.h" #include "aircraft.h"
#include "camera.h"
#include "font.h" #include "font.h"
#include "framebuffer.h" #include "framebuffer.h"
#include "fs2math.h"
#include "radios.h" #include "radios.h"
#include "timeOfDay.h" #include "timeOfDay.h"
#include "wind.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, static void formatValue(char *buf, size_t n, FieldIndexE fi,
const AircraftT *ac, const RadiosT *radios, const AircraftT *ac, const RadiosT *radios,
const TimeOfDayT *tod, const WindStateT *wind); const TimeOfDayT *tod, const WindStateT *wind);
@ -126,28 +127,6 @@ static void modifyField(FieldIndexE fi, int step,
TimeOfDayT *tod, WindStateT *wind); 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, static void formatValue(char *buf, size_t n, FieldIndexE fi,
const AircraftT *ac, const RadiosT *radios, const AircraftT *ac, const RadiosT *radios,
const TimeOfDayT *tod, const WindStateT *wind) { 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_X: snprintf(buf, n, "%6d", metresFromQ1616(ac->worldX)); break;
case F_WORLD_Y: snprintf(buf, n, "%6d", metresFromQ1616(ac->worldY)); 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_WORLD_Z: snprintf(buf, n, "%6d", metresFromQ1616(ac->worldZ)); break;
case F_YAW: snprintf(buf, n, "%6d", (ac->yaw * 360) / 256); break; case F_YAW: snprintf(buf, n, "%6d", byteAngleToDegrees(ac->yaw)); break;
case F_PITCH: snprintf(buf, n, "%6d", (ac->pitch * 360) / 256); break; case F_PITCH: snprintf(buf, n, "%6d", byteAngleToDegrees(ac->pitch)); break;
case F_BANK: snprintf(buf, n, "%6d", (ac->bank * 360) / 256); 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_THROTTLE: snprintf(buf, n, "%6d", (ac->throttle * 100) / 256); break;
case F_MIXTURE: snprintf(buf, n, "%6d", (ac->mixture * 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; 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]); snprintf(buf, n, "%6s", names[tod->season & 3]);
break; 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_WIND_SPD: snprintf(buf, n, "%6d", wind->surface.magnitude); break;
case F_REALITY: snprintf(buf, n, "%6s", ac->realityMode ? "ON" : "OFF"); 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_NAV1_FREQ: snprintf(buf, n, "$%04X", radios->nav1Freq); break;
case F_NAV2_FREQ: snprintf(buf, n, "$%04X", radios->nav2Freq); 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_COM1_FREQ: snprintf(buf, n, "$%04X", radios->com1Freq); break;
case F_ADF_FREQ: snprintf(buf, n, "$%04X", radios->adfFreq); 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_NAV1_OBS: snprintf(buf, n, "%6d", byteAngleToDegrees(radios->nav1Obs)); break;
case F_NAV2_OBS: snprintf(buf, n, "%6d", (radios->nav2Obs * 360) / 256); break; case F_NAV2_OBS: snprintf(buf, n, "%6d", byteAngleToDegrees(radios->nav2Obs)); break;
default: buf[0] = '\0'; break; default: buf[0] = '\0'; break;
} }
} }
@ -194,19 +173,19 @@ static void modifyField(FieldIndexE fi, int step,
switch (fi) { switch (fi) {
case F_WORLD_X: { case F_WORLD_X: {
int16_t m = metresFromQ1616(ac->worldX); 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); ac->worldX = q1616FromMetres(m);
break; break;
} }
case F_WORLD_Y: { case F_WORLD_Y: {
int16_t m = metresFromQ1616(ac->worldY); 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); ac->worldY = q1616FromMetres(m);
break; break;
} }
case F_WORLD_Z: { case F_WORLD_Z: {
int16_t m = metresFromQ1616(ac->worldZ); 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); ac->worldZ = q1616FromMetres(m);
break; 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_PITCH: ac->pitch = (uint8_t)(ac->pitch + step); break;
case F_BANK: ac->bank = (uint8_t)(ac->bank + step); break; case F_BANK: ac->bank = (uint8_t)(ac->bank + step); break;
case F_THROTTLE: { case F_THROTTLE: {
int v = applyStep(ac->throttle, step, 0, 255); int v = fs2StepClamp(ac->throttle, step, 0, 255);
ac->throttle = (uint8_t)v; ac->throttle = (uint8_t)v;
break; break;
} }
case F_MIXTURE: ac->mixture = (uint8_t)applyStep(ac->mixture, step, 0, 255); break; case F_MIXTURE: ac->mixture = (uint8_t)fs2StepClamp(ac->mixture, step, 0, 255); break;
case F_MAGNETOS: ac->magnetos = (uint8_t)applyStep(ac->magnetos, step, 0, 4); 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_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_L: ac->fuelLeft = (uint8_t)fs2StepClamp(ac->fuelLeft, step, 0, 255); break;
case F_FUEL_R: ac->fuelRight = (uint8_t)applyStep(ac->fuelRight, 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)applyStep(ac->flaps, step, 0, 255); break; case F_FLAPS: ac->flaps = (uint8_t)fs2StepClamp(ac->flaps, step, 0, 255); break;
case F_TRIM: { 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; ac->trim = (uint8_t)v;
break; break;
} }
case F_LIGHTS: ac->lightsOn = !ac->lightsOn; break; case F_LIGHTS: ac->lightsOn = !ac->lightsOn; break;
case F_TOD_HOURS: { 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); timeOfDaySet(tod, (uint8_t)hr, tod->minutes);
break; break;
} }
case F_TOD_MIN: { 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); timeOfDaySet(tod, tod->hours, (uint8_t)mn);
break; break;
} }
@ -247,7 +226,7 @@ static void modifyField(FieldIndexE fi, int step,
break; break;
} }
case F_WIND_DIR: wind->surface.direction = (uint8_t)(wind->surface.direction + 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_REALITY: ac->realityMode = !ac->realityMode; break;
case F_NAV1_FREQ: radios->nav1Freq = (uint16_t)(radios->nav1Freq + step); 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; case F_NAV2_FREQ: radios->nav2Freq = (uint16_t)(radios->nav2Freq + step); break;

View file

@ -106,3 +106,13 @@ int16_t fontDrawString(FramebufferT *fb, int16_t x, int16_t y, const char *s, Co
} }
return cursor; 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);
}

View file

@ -118,7 +118,7 @@ void hudDraw(FramebufferT *fb, const CameraT *cam) {
fontDrawString(fb, 6, (int16_t)(panelTop + 12), buf, COLOR_ORANGE); fontDrawString(fb, 6, (int16_t)(panelTop + 12), buf, COLOR_ORANGE);
// Heading: 0..359 deg derived from yaw byte angle. // 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); snprintf(buf, sizeof(buf), "HDG %03d", heading);
fontDrawString(fb, 6, (int16_t)(panelTop + 24), buf, COLOR_ORANGE); fontDrawString(fb, 6, (int16_t)(panelTop + 24), buf, COLOR_ORANGE);

View file

@ -59,8 +59,6 @@ static const GaugeT adfGauge = { 180, 175, 12 };
#define VOR2_FLAG_X 168 #define VOR2_FLAG_X 168
#define VOR2_FLAG_Y 173 #define VOR2_FLAG_Y 173
#define DEG2RAD 0.01745329251994f
typedef struct PixelListT { typedef struct PixelListT {
uint8_t count; uint8_t count;
@ -68,7 +66,6 @@ typedef struct PixelListT {
} 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 drawControlIndicators(FramebufferT *fb, const AircraftT *ac);
static void drawFailX(FramebufferT *fb, const GaugeT *g); static void drawFailX(FramebufferT *fb, const GaugeT *g);
static void drawHorizonDisc(FramebufferT *fb, const GaugeT *g, const AircraftT *ac); 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 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, // Draw the four FS2 control-position markers (aileron, rudder,
// throttle, elevator). Each is a small pixel-list sprite that slides // throttle, elevator). Each is a small pixel-list sprite that slides
// along a fixed track on the panel. FS2 anchors: // 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 // bank pushes it sideways. Port's sideslip_q88 is computed in
// aircraft.c as (yawRate - coordinated-turn yawRate); divide // aircraft.c as (yawRate - coordinated-turn yawRate); divide
// down to the same -127..+127 range fs2SlipSkidIndex expects. // down to the same -127..+127 range fs2SlipSkidIndex expects.
int32_t ssClamped = ac->sideslip_q88 / 8; int32_t ssClamped = fs2ClampInt(ac->sideslip_q88 / 8, -127, 127);
if (ssClamped > 127) ssClamped = 127;
if (ssClamped < -127) ssClamped = -127;
uint8_t idx = fs2SlipSkidIndex((int8_t)ssClamped); uint8_t idx = fs2SlipSkidIndex((int8_t)ssClamped);
int16_t anchorX = (int16_t)((int)idx + 0x0E); int16_t anchorX = (int16_t)((int)idx + 0x0E);
drawPixelList(fb, anchorX, 0xB4, &plBall, COLOR_WHITE); drawPixelList(fb, anchorX, 0xB4, &plBall, COLOR_WHITE);
@ -403,6 +388,11 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra
drawSlipSkidBall(fb, ac); 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. // Aileron / rudder / throttle / elevator position markers.
drawControlIndicators(fb, ac); drawControlIndicators(fb, ac);
@ -415,10 +405,10 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra
} }
if (ac->stalled) { 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) { 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) { if (ac->crashed) {
// Mirrors FS2 chunk3 `HandleCrashOrSplash` / // Mirrors FS2 chunk3 `HandleCrashOrSplash` /
@ -434,25 +424,25 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra
case CRASH_NONE: case CRASH_NONE:
default: msg = "CRASH"; break; 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) { 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) { if (ac->editMode) {
drawCenteredString(fb, NATIVE_WIDTH / 2, 24, "EDIT", COLOR_WHITE); fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 24, "EDIT", COLOR_WHITE);
} }
if (ac->realityMode) { 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 // Engine-fault flags: chunk3 SetEngineFault01/23 OR-in the
// bits; we surface them as a status line so the pilot has // bits; we surface them as a status line so the pilot has
// something visible to react to. // something visible to react to.
if (ac->engineFaults & AC_ENG_FAULT_LEFT) { 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) { 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 // Magneto state indicator. Mirrors FS2 chunk5 DrawMagnetoState
// (the string drawn alongside the magneto knob graphics on the // (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; case 4: mag = "MAG START"; break;
default: break; default: break;
} }
drawCenteredString(fb, NATIVE_WIDTH / 2, 74, mag, COLOR_ORANGE); fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 74, mag, COLOR_ORANGE);
} }
if (ac->paused) { 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. // Throttle / Mixture / Flaps / Trim / Fuel position markers.
@ -552,13 +542,13 @@ void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *ra
} }
} }
if (ac->lightsOn) { 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) { 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) { 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) { if (ac->viewDirection != VIEW_FORWARD) {
const char *label = "VIEW"; 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; case VIEW_DOWN: label = "DOWN VIEW"; break;
default: break; default: break;
} }
drawCenteredString(fb, NATIVE_WIDTH / 2, 34, label, COLOR_WHITE); fontDrawStringCentered(fb, NATIVE_WIDTH / 2, 34, label, COLOR_WHITE);
} }
if (ac->slewMode) { if (ac->slewMode) {
// FS2 chunk3 `DrawSlewOverlays`: " 00000 NORTH " at row // FS2 chunk3 `DrawSlewOverlays`: " 00000 NORTH " at row
// 2 col $0A, " 00000 EAST " at row 2 col $4C. // 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) { if (ac->showSlewDigits) {
char buf[24]; char buf[24];
// worldX/Z are Q16.16; truncate to integer world unit. // 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; return;
} }
int defl = deflectionDeg; int defl = fs2ClampInt(deflectionDeg, -VOR_CDI_FULL_DEG, VOR_CDI_FULL_DEG);
if (defl > VOR_CDI_FULL_DEG) defl = VOR_CDI_FULL_DEG;
if (defl < -VOR_CDI_FULL_DEG) defl = -VOR_CDI_FULL_DEG;
int xOffset = (defl * VOR_CDI_HALFWIDTH) / VOR_CDI_FULL_DEG; int xOffset = (defl * VOR_CDI_HALFWIDTH) / VOR_CDI_FULL_DEG;
int16_t nx = (int16_t)(centreX + xOffset); int16_t nx = (int16_t)(centreX + xOffset);
for (int dy = 1; dy <= VOR_CDI_NEEDLE_H; dy++) { for (int dy = 1; dy <= VOR_CDI_NEEDLE_H; dy++) {

View file

@ -21,6 +21,7 @@
#include "radios.h" #include "radios.h"
#include "renderer.h" #include "renderer.h"
#include "chunk5Transform.h" #include "chunk5Transform.h"
#include "fs2math.h"
#include "sceneryData.h" #include "sceneryData.h"
#include "sceneryProjection.h" #include "sceneryProjection.h"
#include "sceneryVm.h" #include "sceneryVm.h"
@ -400,42 +401,9 @@ static int runScreenshot(const char *path) {
SceneryRegionE shotRegion = SCENERY_FS2_1; SceneryRegionE shotRegion = SCENERY_FS2_1;
const char *regionEnv = getenv("SCENERY_REGION"); const char *regionEnv = getenv("SCENERY_REGION");
if (regionEnv != NULL) { if (regionEnv != NULL) {
if (strcmp(regionEnv, "FS2.1") == 0) { SceneryRegionE r = sceneryDataRegionFromName(regionEnv);
shotRegion = SCENERY_FS2_1; if (r != SCENERY_NONE) {
} else if (strcmp(regionEnv, "FS2.1_chicago") == 0) { shotRegion = r;
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;
} }
} }
sceneryDataLoad(shotRegion, &screenshotScenery); sceneryDataLoad(shotRegion, &screenshotScenery);
@ -917,6 +885,63 @@ int main(int argc, char **argv) {
sceneryDataFree(&scenery); sceneryDataFree(&scenery);
} }
scenery = alt; 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 // elevator trim by 4 byte-units; long-held keys
// accumulate. Trim biases the pitch yoke output. // accumulate. Trim biases the pitch yoke output.
if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_HOME) { if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_HOME) {
int t = (int)aircraft.trim - 4; aircraft.trim = (int8_t)fs2StepClamp(aircraft.trim, -4, -127, 127);
if (t < -127) t = -127;
aircraft.trim = (int8_t)t;
} }
if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_END) { if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_END) {
int t = (int)aircraft.trim + 4; aircraft.trim = (int8_t)fs2StepClamp(aircraft.trim, +4, -127, 127);
if (t > 127) t = 127;
aircraft.trim = (int8_t)t;
} }
// Flap controls (chunk5 UpdateFlapsIndicator). // Flap controls (chunk5 UpdateFlapsIndicator).
// INSERT extends flaps in 32-byte-unit clicks; // INSERT extends flaps in 32-byte-unit clicks;
// DELETE retracts. // DELETE retracts.
if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_INSERT) { if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_INSERT) {
int f = (int)aircraft.flaps + 32; aircraft.flaps = (uint8_t)fs2StepClamp(aircraft.flaps, +32, 0, 255);
if (f > 255) f = 255;
aircraft.flaps = (uint8_t)f;
} }
if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_DELETE) { if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_DELETE) {
int f = (int)aircraft.flaps - 32; aircraft.flaps = (uint8_t)fs2StepClamp(aircraft.flaps, -32, 0, 255);
if (f < 0) f = 0;
aircraft.flaps = (uint8_t)f;
} }
// Mixture controls (chunk5 UpdateMixtureControl). // Mixture controls (chunk5 UpdateMixtureControl).
// PAGEUP+Shift = richer, PAGEDN+Shift = leaner. // PAGEUP+Shift = richer, PAGEDN+Shift = leaner.
if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT) if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT)
&& ev.key.keysym.sym == SDLK_PAGEUP) { && ev.key.keysym.sym == SDLK_PAGEUP) {
int m = (int)aircraft.mixture + 16; aircraft.mixture = (uint8_t)fs2StepClamp(aircraft.mixture, +16, 0, 255);
if (m > 255) m = 255;
aircraft.mixture = (uint8_t)m;
} }
if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT) if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT)
&& ev.key.keysym.sym == SDLK_PAGEDOWN) { && ev.key.keysym.sym == SDLK_PAGEDOWN) {
int m = (int)aircraft.mixture - 16; aircraft.mixture = (uint8_t)fs2StepClamp(aircraft.mixture, -16, 0, 255);
if (m < 0) m = 0;
aircraft.mixture = (uint8_t)m;
} }
if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_BACKQUOTE) { if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_BACKQUOTE) {
aircraft.radarView = !aircraft.radarView; aircraft.radarView = !aircraft.radarView;
} }
if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_LEFTBRACKET if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_LEFTBRACKET
&& aircraft.radarView) { && aircraft.radarView) {
int z = (int)aircraft.radarZoom * 3 / 2; aircraft.radarZoom = (int16_t)fs2ClampInt((int)aircraft.radarZoom * 3 / 2, 128, 32767);
if (z > 32767) {
z = 32767;
}
aircraft.radarZoom = (int16_t)z;
} }
if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_RIGHTBRACKET if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_RIGHTBRACKET
&& aircraft.radarView) { && aircraft.radarView) {
int z = (int)aircraft.radarZoom * 2 / 3; aircraft.radarZoom = (int16_t)fs2ClampInt((int)aircraft.radarZoom * 2 / 3, 128, 32767);
if (z < 128) { // 0.5 in Q8.8
z = 128;
}
aircraft.radarZoom = (int16_t)z;
} }
// Joystick button events. Trigger (0) fires // Joystick button events. Trigger (0) fires
// gun, second (1) drops a bomb, third (2) // 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) { if (ev.type == SDL_KEYDOWN && radioInputMode != RADIO_INPUT_NONE) {
SDL_Keycode k = ev.key.keysym.sym; SDL_Keycode k = ev.key.keysym.sym;
int digit = -1; int digit = -1;
if (k >= SDLK_0 && k <= SDLK_9) digit = (int)(k - SDLK_0); if (k >= SDLK_0 && k <= SDLK_9) {
else if (k >= SDLK_KP_0 && k <= SDLK_KP_9) digit = (int)(k - SDLK_KP_0); digit = (int)(k - SDLK_0);
} else if (k >= SDLK_KP_0 && k <= SDLK_KP_9) {
digit = (int)(k - SDLK_KP_0);
}
if (digit >= 0) { if (digit >= 0) {
RadioE w = (radioInputMode == RADIO_INPUT_NAV1) ? RADIO_NAV1 RadioE w = (radioInputMode == RADIO_INPUT_NAV1) ? RADIO_NAV1
: (radioInputMode == RADIO_INPUT_NAV2) ? RADIO_NAV2 : (radioInputMode == RADIO_INPUT_NAV2) ? RADIO_NAV2

View file

@ -15,6 +15,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include "camera.h"
#include "font.h" #include "font.h"
#include "panelDigits.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 clearW = (int16_t)(r->cellsWide * FS2_CHAR_ADVANCE_HIRES + (FS2_CHAR_WIDTH_HIRES - FS2_CHAR_ADVANCE_HIRES));
int16_t clearH = FONT_HEIGHT + 1; 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); 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);
// 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);
} }
@ -89,6 +82,49 @@ void panelDigitsDraw(FramebufferT *fb, const AircraftT *ac, const RadiosT *radio
char freqBuf[8]; char freqBuf[8];
radiosFormatFreq(radios->com1Freq, RADIO_COM1, freqBuf); radiosFormatFreq(radios->com1Freq, RADIO_COM1, freqBuf);
drawReadout(fb, &readoutCom1, 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); radiosFormatFreq(radios->nav1Freq, RADIO_NAV1, freqBuf);
drawReadout(fb, &readoutNav1, freqBuf); drawReadout(fb, &readoutNav1, freqBuf);
// NAV2 vs ADF frequency share the same row; FS2 picks one per // 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, &readoutHeading, "---");
drawReadout(fb, &readoutRecip, "---"); drawReadout(fb, &readoutRecip, "---");
} else { } else {
int heading = ((int)ac->yaw * 360) / 256; int heading = byteAngleToDegrees(ac->yaw);
heading %= 360; heading %= 360;
snprintf(buf, sizeof(buf), "%03d", heading); snprintf(buf, sizeof(buf), "%03d", heading);
drawReadout(fb, &readoutHeading, buf); 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; // VOR1 / VOR2 OBS course + reciprocal. obs is byte angle;
// convert to degrees 0..359. // convert to degrees 0..359.
int obs1Deg = ((int)radios->nav1Obs * 360) / 256; int obs1Deg = byteAngleToDegrees(radios->nav1Obs);
int obs2Deg = ((int)radios->nav2Obs * 360) / 256; int obs2Deg = byteAngleToDegrees(radios->nav2Obs);
snprintf(buf, sizeof(buf), "%03d", obs1Deg); snprintf(buf, sizeof(buf), "%03d", obs1Deg);
drawReadout(fb, &readoutVor1Course, buf); drawReadout(fb, &readoutVor1Course, buf);
snprintf(buf, sizeof(buf), "%03d", (obs1Deg + 180) % 360); snprintf(buf, sizeof(buf), "%03d", (obs1Deg + 180) % 360);

View file

@ -1,6 +1,7 @@
// NAV / COM / ADF radio state and lookups. See radios.h. // NAV / COM / ADF radio state and lookups. See radios.h.
#include <stddef.h> #include <stddef.h>
#include "fs2math.h"
#include "math6502.h" #include "math6502.h"
#include "radios.h" #include "radios.h"
@ -280,8 +281,7 @@ void radiosEnterDigit(RadiosT *r, RadioE which, uint8_t digit) {
khz = (khz % 100) * 10 + digit; // shift the khz = (khz % 100) * 10 + digit; // shift the
// 3-digit display, dropping the leading digit and // 3-digit display, dropping the leading digit and
// bringing the new digit in at the units place. // bringing the new digit in at the units place.
if (khz < ADF_FREQ_MIN_KHZ) khz = ADF_FREQ_MIN_KHZ; khz = fs2ClampInt(khz, ADF_FREQ_MIN_KHZ, ADF_FREQ_MAX_KHZ);
if (khz > ADF_FREQ_MAX_KHZ) khz = ADF_FREQ_MAX_KHZ;
r->adfFreq = khzToBcdAdf(khz); r->adfFreq = khzToBcdAdf(khz);
return; return;
} }
@ -299,8 +299,7 @@ void radiosEnterDigit(RadiosT *r, RadioE which, uint8_t digit) {
hund = (hund / 5) * 5; hund = (hund / 5) * 5;
int minHund = (which == RADIO_COM1) ? 11800 : 10800; int minHund = (which == RADIO_COM1) ? 11800 : 10800;
int maxHund = (which == RADIO_COM1) ? 13695 : 11795; int maxHund = (which == RADIO_COM1) ? 13695 : 11795;
if (hund < minHund) hund = minHund; hund = fs2ClampInt(hund, minHund, maxHund);
if (hund > maxHund) hund = maxHund;
*freqPtr = intToBcdNavCom(hund); *freqPtr = intToBcdNavCom(hund);
} }
@ -346,9 +345,7 @@ void radiosUpdate(RadiosT *r, const AircraftT *ac) {
// mapped to a degree count for the CDI. // mapped to a degree count for the CDI.
uint8_t fromBearing = (uint8_t)(r->nav1RelativeBearing + 128); uint8_t fromBearing = (uint8_t)(r->nav1RelativeBearing + 128);
int8_t radialDelta = (int8_t)(fromBearing - r->nav1Obs); int8_t radialDelta = (int8_t)(fromBearing - r->nav1Obs);
int devDeg = ((int)radialDelta * 360 + 128) / 256; int devDeg = fs2ClampInt(((int)radialDelta * 360 + 128) / 256, -32, 32);
if (devDeg > 32) devDeg = 32;
if (devDeg < -32) devDeg = -32;
r->nav1NeedleDefl = (int8_t)devDeg; r->nav1NeedleDefl = (int8_t)devDeg;
// TO/FROM: |radialDelta| < 64 (= 90 deg) means // TO/FROM: |radialDelta| < 64 (= 90 deg) means
// the aircraft is on the OBS-radial half -> // 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); r->nav2Dme = (uint16_t)((int32_t)d / AC_SCENERY_UNITS_PER_NM);
uint8_t fromBearing = (uint8_t)(r->nav2RelativeBearing + 128); uint8_t fromBearing = (uint8_t)(r->nav2RelativeBearing + 128);
int8_t radialDelta = (int8_t)(fromBearing - r->nav2Obs); int8_t radialDelta = (int8_t)(fromBearing - r->nav2Obs);
int devDeg = ((int)radialDelta * 360 + 128) / 256; int devDeg = fs2ClampInt(((int)radialDelta * 360 + 128) / 256, -32, 32);
if (devDeg > 32) devDeg = 32;
if (devDeg < -32) devDeg = -32;
r->nav2NeedleDefl = (int8_t)devDeg; r->nav2NeedleDefl = (int8_t)devDeg;
int adelta = radialDelta < 0 ? -radialDelta : radialDelta; int adelta = radialDelta < 0 ? -radialDelta : radialDelta;
r->nav2Flag = (adelta < 64) ? VOR_FLAG_FR : VOR_FLAG_TO; r->nav2Flag = (adelta < 64) ? VOR_FLAG_FR : VOR_FLAG_TO;

View file

@ -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 // Set the chunk5 hires color code directly. Used by the chunk5 $12
// SetColor handler so the hires bitplane gets bit-faithful color // SetColor handler so the hires bitplane gets bit-faithful color
// (rather than going through the modern palette mapping). // (rather than going through the modern palette mapping).

View file

@ -12,31 +12,36 @@
typedef struct RegionMetaT { typedef struct RegionMetaT {
const char *fileName; // basename inside downloads/scenery/extracted/ const char *fileName; // RAM-dump basename (sceneryRam_<fileName>.bin)
const char *sdSourceFile; // demand-load .SD source in downloads/scenery/extracted/
const char *displayName; const char *displayName;
} RegionMetaT; } 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] = { static const RegionMetaT regionMeta[SCENERY_REGION_COUNT] = {
[SCENERY_NONE] = { NULL, "(no scenery)" }, [SCENERY_NONE] = { NULL, NULL, "(no scenery)" },
[SCENERY_FS2_1] = { "FS2.1", "FS2 base disk - WWI Ace training" }, [SCENERY_FS2_1] = { "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_CHICAGO] = { "FS2.1_chicago", "FS2.1", "FS2 base disk - Chicago / Meigs Field" },
[SCENERY_FS2_1_LA] = { "FS2.1_la", "FS2 base disk - Los Angeles" }, [SCENERY_FS2_1_LA] = { "FS2.1_la", "FS2.1", "FS2 base disk - Los Angeles" },
[SCENERY_FS2_1_SEATTLE] = { "FS2.1_seattle", "FS2 base disk - Seattle" }, [SCENERY_FS2_1_SEATTLE] = { "FS2.1_seattle", "FS2.1", "FS2 base disk - Seattle" },
[SCENERY_FS2_1_NY] = { "FS2.1_ny", "FS2 base disk - New York / Kennedy" }, [SCENERY_FS2_1_NY] = { "FS2.1_ny", "FS2.1", "FS2 base disk - New York / Kennedy" },
[SCENERY_SD1] = { "A2.SD1", "Dallas-Ft.Worth, Houston, San Antonio" }, [SCENERY_SD1] = { "A2.SD1", "A2.SD1", "Dallas-Ft.Worth, Houston, San Antonio" },
[SCENERY_SD2] = { "A2.SD2", "Phoenix, Albuquerque, El Paso" }, [SCENERY_SD2] = { "A2.SD2", "A2.SD2", "Phoenix, Albuquerque, El Paso" },
[SCENERY_SD3] = { "A2.SD3", "San Francisco, Los Angeles, Las Vegas" }, [SCENERY_SD3] = { "A2.SD3", "A2.SD3", "San Francisco, Los Angeles, Las Vegas" },
[SCENERY_SD4] = { "A2.SD4", "Klamath Falls, Seattle, Great Falls" }, [SCENERY_SD4] = { "A2.SD4", "A2.SD4", "Klamath Falls, Seattle, Great Falls" },
[SCENERY_SD5] = { "A2.SD5", "Salt Lake City, Cheyenne, Denver" }, [SCENERY_SD5] = { "A2.SD5", "A2.SD5", "Salt Lake City, Cheyenne, Denver" },
[SCENERY_SD6] = { "A2.SD6", "Omaha, Wichita, Kansas City" }, [SCENERY_SD6] = { "A2.SD6", "A2.SD6", "Omaha, Wichita, Kansas City" },
[SCENERY_SD7A] = { "A2.SD7A", "Washington, Charlotte" }, [SCENERY_SD7A] = { "A2.SD7A", "A2.SD7A", "Washington, Charlotte" },
[SCENERY_SD7B] = { "A2.SD7B", "Jacksonville, Miami" }, [SCENERY_SD7B] = { "A2.SD7B", "A2.SD7B", "Jacksonville, Miami" },
[SCENERY_SD11] = { "A2.SD11", "Lake Huron, Detroit" }, [SCENERY_SD11] = { "A2.SD11", "A2.SD11", "Lake Huron, Detroit" },
[SCENERY_SD13] = { "A2.SD13", "Japan - Tokyo, Osaka" }, [SCENERY_SD13] = { "A2.SD13", "A2.SD13", "Japan - Tokyo, Osaka" },
[SCENERY_SD14A] = { "A2.SD14A", "Western European Tour (UK, N. France)" }, [SCENERY_SD14A] = { "A2.SD14A", "A2.SD14A", "Western European Tour (UK, N. France)" },
[SCENERY_SD14B] = { "A2.SD14B", "Western European Tour (N. France, W. Germany)" }, [SCENERY_SD14B] = { "A2.SD14B", "A2.SD14B", "Western European Tour (N. France, W. Germany)" },
[SCENERY_SDS1] = { "A2.SDS1", "STAR San Francisco & The Bay Area" } [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/", "../../downloads/scenery/extracted/",
"/home/scott/claude/flight/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])); sizeof(prefixes) / sizeof(prefixes[0]));
if (f == NULL) { if (f == NULL) {
return false; return false;
@ -340,3 +350,25 @@ const char *sceneryDataRegionName(SceneryRegionE region) {
} }
return regionMeta[region].displayName; 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;
}

View file

@ -346,19 +346,26 @@ static void doHeader(SceneryStateT *state) {
ram[0x08EA + i] = 0; ram[0x08EA + i] = 0;
} }
// SCENERY_DEMAND_LOAD env var gates the .SD demand-load. Set // Demand-load the section's bytes from the .SD file into the
// to "0" to load from byte 0 of the section (the natural // dispatcher's working RAM (mirrors chunk5 LA63A → chunk3's
// chunk4 ASM behaviour); set to a digit (e.g. "14") to apply a // SmartPort `ReadBlocks` call). Section ID → file offset uses
// source-byte SKIP, which is what MAME's captured RAM appears // ASM-faithful math from chunk4 `ComputeBlockFromSector` /
// to have done (RAM[$A887] matches sid $44 byte 14, not byte 0). // `FetchSectorFromDisk`.
// The skip lets us experimentally find which entry point makes //
// the chunk5 VM walk into the water-color SetColor at $B781. // Without this, boot Meigs runs from the captured RAM image
// Default behaviour: don't demand-load (keep captured-RAM // baked into `port/sceneryRam_FS2.1.bin` (which is byte-faithful
// residue, which is the only known-working path so far). // 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"); const char *envDemandLoad = getenv("SCENERY_DEMAND_LOAD");
if (state->sceneryFile != NULL && state->sceneryFileSize > 0 if (state->sceneryFile != NULL && state->sceneryFileSize > 0) {
&& envDemandLoad != NULL) { int srcSkip = envDemandLoad != NULL ? atoi(envDemandLoad) : 0;
int srcSkip = atoi(envDemandLoad);
if (srcSkip < 0 || srcSkip > 64) srcSkip = 0; if (srcSkip < 0 || srcSkip > 64) srcSkip = 0;
uint8_t count = ram[0x08E6]; uint8_t count = ram[0x08E6];
uint16_t dest = (uint16_t)ram[0x08E7] uint16_t dest = (uint16_t)ram[0x08E7]
@ -407,6 +414,10 @@ static void doHeader(SceneryStateT *state) {
} }
ram[addr] = state->sceneryFile[srcOff + i]; 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);
}
} }
} }
} }

View file

@ -73,12 +73,6 @@ static void enemyFireAtPlayer(WW1AceStateT *s, WW1EnemyT *e,
static void enemyManeuver(WW1AceStateT *s, WW1EnemyT *e, static void enemyManeuver(WW1AceStateT *s, WW1EnemyT *e,
int32_t playerX, int32_t playerY, int32_t playerZ); 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; // Tiny LCG (matches Mike Brennan's classic glibc parameters in spirit;
// good enough for AI jitter). // good enough for AI jitter).
static uint16_t rngNext(WW1AceStateT *s) { 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) { void ww1aceDrawWarReport(const WW1AceStateT *s, FramebufferT *fb) {
framebufferFillRect(fb, 0, 0, NATIVE_WIDTH, NATIVE_HEIGHT, COLOR_BLACK); framebufferFillRect(fb, 0, 0, NATIVE_WIDTH, NATIVE_HEIGHT, COLOR_BLACK);
fontDrawString(fb, 60, 8, "***** WAR REPORT *****", COLOR_WHITE); 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, void ww1aceDropBombAt(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ,
int16_t playerVelX_q88, int16_t playerVelZ_q88) { int16_t playerVelX_q88, int16_t playerVelZ_q88) {
if (!s->enabled || s->bombs == 0) { if (!s->enabled || s->bombs == 0) {

View file