Add configurable game/initial screen width and distance, with a bundled first-run config
build / build (push) Successful in 2m41s

- Video Options tab offers width and distance presets independently for
  the Initial Screen (splash/intro/menu) and Game Screen (gameplay)
  profiles, with their preset step/offset configurable via
  questshock.config.json.
- Ships res/questshock.config.default.json as the initial config on
  first run (or whenever questshock.config.json doesn't exist yet),
  refreshed from the APK on every launch without touching a user's own
  saved config.
This commit is contained in:
ml
2026-09-06 09:00:45 +02:00
parent 5fe4cdf7f8
commit e1e6a006d4
14 changed files with 1052 additions and 91 deletions
+76 -14
View File
@@ -48,7 +48,7 @@ static const float kHandColor[2][4] = {
// MenuOverlay.HEIGHT).
#define KEYBOARD_TITLE_BAR_V_FRACTION (90.0f / 768.0f)
#define KEYBOARD_CLOSE_BUTTON_U_FRACTION (1.0f - 160.0f / 1024.0f)
#define MENU_TITLE_BAR_V_FRACTION (56.0f / 552.0f)
#define MENU_TITLE_BAR_V_FRACTION (56.0f / 800.0f)
#define MENU_DRAG_HANDLE_U_FRACTION 1.0f
static XrInstance g_instance = XR_NULL_HANDLE;
@@ -642,35 +642,97 @@ static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float
g_beam_quad_valid[hand] = true;
}
// Hit-tests this hand's ray against the game quad (xr_get_game_quad_extent())
// Hit-tests this hand's ray against the game screen (xr_get_game_quad_extent())
// - the lowest-priority target, tried only once neither the keyboard nor
// menu overlay claimed the ray this frame (or, symmetrically, once this
// hand already has a pending mouse-down on the game quad - see
// hand already has a pending mouse-down on the game screen - see
// g_game_touch_active and the keyboard/menu call sites in
// xr_input_sync_and_draw()). outU/outV (only meaningful when the
// returned hit is true) let the caller drive the game's own mouse cursor
// via xr_mouse.h. Returns plain `hit` - drag/touch dispatch for actual
// clicks is the caller's responsibility (xr_mouse_click()), not this
// function's, unlike xr_input_try_overlay().
//
// Two shapes: a flat plane (centralAngle == 0, the default and the only
// shape this function supported before screen curving existed - kept
// byte-for-byte unchanged as its own branch below, so nobody who never
// touches the new Video Options curve setting sees any change at all), or
// a curved cylinder segment. The curve is horizontal-only (like a real
// curved monitor) - the cylinder's vertical axis runs parallel to the
// screen's own local Y at (x=0, z=Zc) for every y, so y drops out of the
// lateral-surface intersection test entirely; halfHeight bounds the
// vertical extent exactly like the flat case, unchanged.
static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, float fx, float fy,
float fz, bool *outPlaneHit, float *outPlaneDistance,
float *outU, float *outV) {
float distance, halfWidth, halfHeight;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
float distance, halfWidth, halfHeight, radius, centralAngle;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight, &radius, &centralAngle);
bool planeHit = false, hit = false;
float u = 0.0f, v = 0.0f, t = 0.0f;
if (fabsf(fz) > 1e-5f) {
t = (-distance - location->pose.position.z) / fz;
if (t > 0.0f) {
planeHit = true;
float cx = (location->pose.position.x + t * fx) / halfWidth;
float cy = (location->pose.position.y + t * fy) / halfHeight;
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
u = (cx + 1.0f) * 0.5f;
v = (1.0f - cy) * 0.5f;
if (centralAngle <= 0.0f) {
if (fabsf(fz) > 1e-5f) {
t = (-distance - location->pose.position.z) / fz;
if (t > 0.0f) {
planeHit = true;
float cx = (location->pose.position.x + t * fx) / halfWidth;
float cy = (location->pose.position.y + t * fy) / halfHeight;
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
u = (cx + 1.0f) * 0.5f;
v = (1.0f - cy) * 0.5f;
}
}
} else {
// Zc: the cylinder's axis position along Z (see xr_session.c's
// own concavity-derivation comment - same offset the render path
// uses to place the actual XrCompositionLayerCylinderKHR, so the
// two can never disagree about where the surface actually is).
float Zc = -distance + radius;
float x0 = location->pose.position.x;
float dz0 = location->pose.position.z - Zc;
// Ray-vs-infinite-cylinder quadratic (only x/z participate, per
// this function's own comment above) - A<=~0 means the ray runs
// parallel to the axis (never crosses the lateral surface),
// mirroring the flat case's fabsf(fz)>1e-5f guard.
float A = fx * fx + fz * fz;
if (A > 1e-8f) {
float B = 2.0f * (fx * x0 + fz * dz0);
float C = x0 * x0 + dz0 * dz0 - radius * radius;
float disc = B * B - 4.0f * A * C;
if (disc >= 0.0f) {
float sq = sqrtf(disc);
// Smallest positive root - the near intersection, correct
// both for the normal case (viewer inside the cylinder,
// so exactly one root is positive) and if the viewer ever
// ends up outside it.
float t1 = (-B - sq) / (2.0f * A);
float t2 = (-B + sq) / (2.0f * A);
if (t1 > 0.0f)
t = t1;
else if (t2 > 0.0f)
t = t2;
if (t > 0.0f) {
planeHit = true;
float px = location->pose.position.x + t * fx;
float py = location->pose.position.y + t * fy;
float pz = location->pose.position.z + t * fz;
// Signed angle from center (angle 0 is dead ahead,
// matching the flat case's cx==0 being dead center) -
// bounded by the configured centralAngle instead of a
// halfWidth.
float phi = atan2f(px, Zc - pz);
float halfAngle = centralAngle * 0.5f;
float cy = py / halfHeight;
hit = fabsf(phi) <= halfAngle && fabsf(cy) <= 1.0f;
u = (phi / halfAngle + 1.0f) * 0.5f;
v = (1.0f - cy) * 0.5f;
}
}
}
}
*outPlaneHit = planeHit;
*outPlaneDistance = t;
*outU = u;