Add a Cheats menu tab with engine cheat buttons and unlimited-resource switches
build / build (push) Successful in 3m42s

The new Cheats tab exposes the engine's own cheat handlers as buttons:
give all items and weapons, toggle physics, next level and previous
level. Physics shows its state through the button highlight. The buttons
need a running game and show a message otherwise.

It also has four independent session-only switches for unlimited health,
energy, ammo and items (grenades and drugs). While a switch is on, its
resource is topped up every frame by qs_cheat_refill() from the new
engine patch 26. The switches are not saved to the config.
This commit is contained in:
ml
2026-09-20 14:59:33 +02:00
parent d8324d409e
commit 70c43240bc
8 changed files with 339 additions and 4 deletions
+86 -1
View File
@@ -1250,6 +1250,7 @@ static Uint32 s_begin_control_capture_event_type;
static Uint32 s_cancel_control_capture_event_type;
static Uint32 s_reset_player_controls_event_type;
static Uint32 s_menu_visibility_event_type;
static Uint32 s_cheat_event_type;
void qs_config_load_startup(void) {
// Registers the custom SDL event types qs_config_request_save()/_load()/
@@ -1260,7 +1261,7 @@ void qs_config_load_startup(void) {
// exist yet, rather than lazily from those functions themselves (which
// run on Android's UI thread and could otherwise race each other doing
// this lazily).
Uint32 base = SDL_RegisterEvents(14);
Uint32 base = SDL_RegisterEvents(15);
s_save_event_type = base;
s_load_event_type = base + 1;
s_set_renderer_event_type = base + 2;
@@ -1275,6 +1276,7 @@ void qs_config_load_startup(void) {
s_cancel_control_capture_event_type = base + 11;
s_reset_player_controls_event_type = base + 12;
s_menu_visibility_event_type = base + 13;
s_cheat_event_type = base + 14;
QsParsedConfig parsed;
bool haveConfig = qs_config_read_file(&parsed);
@@ -1407,6 +1409,14 @@ void qs_config_request_cancel_control_capture(void) {
SDL_PushEvent(&event);
}
void qs_config_request_cheat(int cheat_id) {
SDL_Event event;
SDL_zero(event);
event.type = s_cheat_event_type;
event.user.code = cheat_id;
SDL_PushEvent(&event);
}
void qs_config_notify_menu_visibility(bool visible) {
SDL_Event event;
SDL_zero(event);
@@ -1449,6 +1459,77 @@ static void qs_apply_menu_pause(bool menu_visible) {
}
}
// hkeyfunc.c - the engine's own cheat hotkey handlers, and the refill added by
// android/engine-patches/26-android-cheat-refill.patch.
extern uchar toggle_giveall_func(ushort keycode, uint32_t context, intptr_t data);
extern uchar toggle_physics_func(ushort keycode, uint32_t context, intptr_t data);
extern uchar toggle_up_level_func(ushort keycode, uint32_t context, intptr_t data);
extern uchar toggle_down_level_func(ushort keycode, uint32_t context, intptr_t data);
extern void qs_cheat_refill(int what);
extern uchar physics_running;
#define QS_CHEAT_GIVE_ALL 0
#define QS_CHEAT_PHYSICS 1
#define QS_CHEAT_LEVEL_UP 2
#define QS_CHEAT_LEVEL_DOWN 3
#define QS_CHEAT_UNLIMITED_HEALTH 4
#define QS_CHEAT_UNLIMITED_ENERGY 5
#define QS_CHEAT_UNLIMITED_AMMO 6
#define QS_CHEAT_UNLIMITED_ITEMS 7
// Bits of s_cheat_unlimited; the values match qs_cheat_refill() in patch 26.
#define QS_CHEAT_MASK_HEALTH 1
#define QS_CHEAT_MASK_ENERGY 2
#define QS_CHEAT_MASK_AMMO 4
#define QS_CHEAT_MASK_ITEMS 8
static int s_cheat_unlimited = 0;
int qs_config_cheat_unlimited_mask(void) { return s_cheat_unlimited; }
bool qs_config_cheat_physics_running(void) { return physics_running != 0; }
void qs_config_frame_tick(void) {
if (s_cheat_unlimited && s_screen_profile_is_game)
qs_cheat_refill(s_cheat_unlimited);
}
static void qs_apply_cheat(int cheat_id) {
if (cheat_id >= QS_CHEAT_UNLIMITED_HEALTH && cheat_id <= QS_CHEAT_UNLIMITED_ITEMS) {
static const struct {
int mask;
const char *on, *off;
} kUnlimited[] = {
{QS_CHEAT_MASK_HEALTH, "Unlimited health on", "Unlimited health off"},
{QS_CHEAT_MASK_ENERGY, "Unlimited energy on", "Unlimited energy off"},
{QS_CHEAT_MASK_AMMO, "Unlimited ammo on", "Unlimited ammo off"},
{QS_CHEAT_MASK_ITEMS, "Unlimited items on", "Unlimited items off"},
};
const int i = cheat_id - QS_CHEAT_UNLIMITED_HEALTH;
s_cheat_unlimited ^= kUnlimited[i].mask;
message_info((s_cheat_unlimited & kUnlimited[i].mask) ? kUnlimited[i].on : kUnlimited[i].off);
return;
}
// The engine's own handlers assume a running game (player state, levels).
if (!s_screen_profile_is_game) {
message_info("Cheats need a running game");
return;
}
switch (cheat_id) {
case QS_CHEAT_GIVE_ALL:
toggle_giveall_func(0, 0, 0);
break;
case QS_CHEAT_PHYSICS:
toggle_physics_func(0, 0, 0);
break;
case QS_CHEAT_LEVEL_UP:
toggle_up_level_func(0, 0, 0);
break;
case QS_CHEAT_LEVEL_DOWN:
toggle_down_level_func(0, 0, 0);
break;
}
}
bool qs_config_handle_event(const SDL_Event *event) {
if (event->type == s_save_event_type) {
if (qs_config_write_file())
@@ -1531,6 +1612,10 @@ bool qs_config_handle_event(const SDL_Event *event) {
xr_input_capture_cancel();
return true;
}
if (event->type == s_cheat_event_type) {
qs_apply_cheat(event->user.code);
return true;
}
if (event->type == s_menu_visibility_event_type) {
qs_apply_menu_pause(event->user.code != 0);
return true;
+14
View File
@@ -104,6 +104,20 @@ void qs_config_notify_control_capture_result(int action_index, const char *raw_n
// engine's thread at pump_events() time.
void qs_config_notify_menu_visibility(bool visible);
// Cheats tab (MenuOverlay.java). Same enqueue-only requests as above; the work
// runs on the engine's thread. cheat_id: 0 give everything, 1 toggle physics,
// 2 next level, 3 previous level, 4-7 toggle unlimited health, energy, ammo,
// items (grenades and drugs). Cheats need a running game (the unlimited
// switches can be set anywhere). The getters are plain scalar reads, safe from
// the UI thread; the unlimited mask has bit 0 health, 1 energy, 2 ammo, 3 items.
void qs_config_request_cheat(int cheat_id);
int qs_config_cheat_unlimited_mask(void);
bool qs_config_cheat_physics_running(void);
// Called once per frame from xr_input.c on the engine's thread; applies the
// unlimited-resource cheats that are switched on.
void qs_config_frame_tick(void);
// Called from xr_input.c's xr_input_suggest_bindings() when the OpenXR runtime
// rejects a supported interaction profile's suggested bindings at startup.
// That is an app bug, not a config problem, so it shows
@@ -270,3 +270,19 @@ Java_de_ladkau_questshock_MenuOverlay_nativeSetControllerOptionsTabOpen(JNIEnv *
jboolean open) {
xr_input_set_controller_options_tab_open(open == JNI_TRUE);
}
// Cheats tab: same enqueue-only pattern as the other requests.
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestCheat(JNIEnv *env, jclass clazz, jint cheatId) {
qs_config_request_cheat(cheatId);
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetCheatUnlimitedMask(JNIEnv *env, jclass clazz) {
return qs_config_cheat_unlimited_mask();
}
JNIEXPORT jboolean JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetCheatPhysicsRunning(JNIEnv *env, jclass clazz) {
return qs_config_cheat_physics_running();
}
+1
View File
@@ -2000,6 +2000,7 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
(menuVisible || freeView[xr_input_raw_input_hand(raw)])) &&
g_raw_down[raw];
}
qs_config_frame_tick();
xr_input_update_cyber_scales(wantDown);
xr_input_apply_free_view(freeView);
for (size_t i = 0; i < QS_NUM_KEY_ACTIONS; i++) {
@@ -143,14 +143,27 @@ public class MenuOverlay extends OverlayPanel {
private TextView statsView;
// Content area (tabs) - only one of mainMenuTab/videoOptionsTab/
// controllerOptionsTab is VISIBLE at a time; currentTab tracks which.
// controllerOptionsTab/cheatsTab is VISIBLE at a time; currentTab tracks which.
private View mainMenuTab;
private View videoOptionsTab;
private View controllerOptionsTab;
private View cheatsTab;
private View currentTab;
private Button mainMenuTabButton;
private Button videoOptionsTabButton;
private Button controllerOptionsTabButton;
private Button cheatsTabButton;
// Cheats tab
private TextView cheatsNoteView;
private Button giveAllButton;
private Button physicsButton;
private Button levelUpButton;
private Button levelDownButton;
private Button unlimitedHealthButton;
private Button unlimitedEnergyButton;
private Button unlimitedAmmoButton;
private Button unlimitedItemsButton;
// Main Menu tab's own two states (only one VISIBLE at a time, within
// mainMenuTab) - mainMenuContent is the normal state (button row +
@@ -251,14 +264,18 @@ public class MenuOverlay extends OverlayPanel {
mainMenuTab = buildMainMenuTab();
videoOptionsTab = buildVideoOptionsTab();
controllerOptionsTab = buildControllerOptionsTab();
cheatsTab = buildCheatsTab();
content.addView(mainMenuTab, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
content.addView(videoOptionsTab, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
content.addView(controllerOptionsTab, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
content.addView(cheatsTab, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
videoOptionsTab.setVisibility(View.GONE);
controllerOptionsTab.setVisibility(View.GONE);
cheatsTab.setVisibility(View.GONE);
currentTab = mainMenuTab;
panel.addView(content, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f));
@@ -923,6 +940,86 @@ public class MenuOverlay extends OverlayPanel {
return profile.image;
}
// Cheats tab: the engine's own cheat functions as buttons, plus four switches
// for unlimited health, energy, ammo and items. The physics and unlimited
// buttons show their state through the selected highlight.
private static final int CHEAT_GIVE_ALL = 0;
private static final int CHEAT_PHYSICS = 1;
private static final int CHEAT_LEVEL_UP = 2;
private static final int CHEAT_LEVEL_DOWN = 3;
private static final int CHEAT_UNLIMITED_HEALTH = 4;
private static final int CHEAT_UNLIMITED_ENERGY = 5;
private static final int CHEAT_UNLIMITED_AMMO = 6;
private static final int CHEAT_UNLIMITED_ITEMS = 7;
private View buildCheatsTab() {
LinearLayout tab = new LinearLayout(activity);
tab.setOrientation(LinearLayout.VERTICAL);
tab.setGravity(Gravity.CENTER);
tab.setPadding(24, 16, 24, 16);
cheatsNoteView = new TextView(activity);
cheatsNoteView.setTextColor(0xFFFFD060);
cheatsNoteView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15);
cheatsNoteView.setGravity(Gravity.CENTER);
cheatsNoteView.setPadding(0, 0, 0, 16);
cheatsNoteView.setText(Strings.get("cheats_note", currentLanguage));
tab.addView(cheatsNoteView);
LinearLayout firstRow = new LinearLayout(activity);
firstRow.setOrientation(LinearLayout.HORIZONTAL);
giveAllButton = addSelectableButton(firstRow, Strings.get("cheat_give_all", currentLanguage),
v -> nativeRequestCheat(CHEAT_GIVE_ALL));
physicsButton = addSelectableButton(firstRow, Strings.get("cheat_physics", currentLanguage),
v -> {
nativeRequestCheat(CHEAT_PHYSICS);
refreshCheatsSelection();
});
tab.addView(firstRow);
LinearLayout secondRow = new LinearLayout(activity);
secondRow.setOrientation(LinearLayout.HORIZONTAL);
levelUpButton = addSelectableButton(secondRow, Strings.get("cheat_level_up", currentLanguage),
v -> nativeRequestCheat(CHEAT_LEVEL_UP));
levelDownButton = addSelectableButton(secondRow, Strings.get("cheat_level_down", currentLanguage),
v -> nativeRequestCheat(CHEAT_LEVEL_DOWN));
tab.addView(secondRow);
LinearLayout thirdRow = new LinearLayout(activity);
thirdRow.setOrientation(LinearLayout.HORIZONTAL);
unlimitedHealthButton = addSelectableButton(thirdRow,
Strings.get("cheat_unlimited_health", currentLanguage), v -> toggleUnlimited(CHEAT_UNLIMITED_HEALTH));
unlimitedEnergyButton = addSelectableButton(thirdRow,
Strings.get("cheat_unlimited_energy", currentLanguage), v -> toggleUnlimited(CHEAT_UNLIMITED_ENERGY));
tab.addView(thirdRow);
LinearLayout fourthRow = new LinearLayout(activity);
fourthRow.setOrientation(LinearLayout.HORIZONTAL);
unlimitedAmmoButton = addSelectableButton(fourthRow,
Strings.get("cheat_unlimited_ammo", currentLanguage), v -> toggleUnlimited(CHEAT_UNLIMITED_AMMO));
unlimitedItemsButton = addSelectableButton(fourthRow,
Strings.get("cheat_unlimited_items", currentLanguage), v -> toggleUnlimited(CHEAT_UNLIMITED_ITEMS));
tab.addView(fourthRow);
return tab;
}
// The requests run on the engine's thread a frame later, so the physics
// and unlimited highlights catch up on the next periodic refresh.
private void refreshCheatsSelection() {
physicsButton.setSelected(nativeGetCheatPhysicsRunning());
int mask = nativeGetCheatUnlimitedMask();
unlimitedHealthButton.setSelected((mask & 1) != 0);
unlimitedEnergyButton.setSelected((mask & 2) != 0);
unlimitedAmmoButton.setSelected((mask & 4) != 0);
unlimitedItemsButton.setSelected((mask & 8) != 0);
}
private void toggleUnlimited(int cheatId) {
nativeRequestCheat(cheatId);
refreshCheatsSelection();
}
private View buildTabBar() {
LinearLayout tabBar = new LinearLayout(activity);
tabBar.setOrientation(LinearLayout.HORIZONTAL);
@@ -969,7 +1066,21 @@ public class MenuOverlay extends OverlayPanel {
controllerOptionsTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
registerHoverable(controllerOptionsTabButton);
tabBar.addView(controllerOptionsTabButton,
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.3f));
View separator3 = new View(activity);
separator3.setBackgroundColor(0xFFFFFFFF);
tabBar.addView(separator3, new LinearLayout.LayoutParams(2, ViewGroup.LayoutParams.MATCH_PARENT));
cheatsTabButton = new Button(activity);
cheatsTabButton.setText(Strings.get("cheats", currentLanguage));
cheatsTabButton.setBackground(selectableButtonBackground());
cheatsTabButton.setTextColor(buttonTextColors());
cheatsTabButton.setOnClickListener(v -> switchTab(cheatsTab));
cheatsTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
registerHoverable(cheatsTabButton);
tabBar.addView(cheatsTabButton,
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 0.7f));
return tabBar;
}
@@ -992,6 +1103,7 @@ public class MenuOverlay extends OverlayPanel {
mainMenuTabButton.setSelected(tab == mainMenuTab);
videoOptionsTabButton.setSelected(tab == videoOptionsTab);
controllerOptionsTabButton.setSelected(tab == controllerOptionsTab);
cheatsTabButton.setSelected(tab == cheatsTab);
nativeSetControllerOptionsTabOpen(tab == controllerOptionsTab);
resetHover();
if (tab == videoOptionsTab) {
@@ -1007,6 +1119,8 @@ public class MenuOverlay extends OverlayPanel {
controllerScrollView.scrollTo(0, 0);
controllerScrollBar.setFraction(0f);
refreshControllerOptionsSelection();
} else if (tab == cheatsTab) {
refreshCheatsSelection();
}
relayoutAndRedraw();
}
@@ -1172,6 +1286,8 @@ public class MenuOverlay extends OverlayPanel {
refreshLanguageSelection();
} else if (currentTab == controllerOptionsTab) {
refreshControllerOptionsSelection();
} else if (currentTab == cheatsTab) {
refreshCheatsSelection();
}
// The stats text's width changes every tick (digit count, "ms"
@@ -1227,6 +1343,16 @@ public class MenuOverlay extends OverlayPanel {
gameScreenDistanceLabel.setText(Strings.get("distance", currentLanguage));
controllerOptionsTabButton.setText(Strings.get("controller_options", currentLanguage));
cheatsTabButton.setText(Strings.get("cheats", currentLanguage));
cheatsNoteView.setText(Strings.get("cheats_note", currentLanguage));
giveAllButton.setText(Strings.get("cheat_give_all", currentLanguage));
physicsButton.setText(Strings.get("cheat_physics", currentLanguage));
levelUpButton.setText(Strings.get("cheat_level_up", currentLanguage));
levelDownButton.setText(Strings.get("cheat_level_down", currentLanguage));
unlimitedHealthButton.setText(Strings.get("cheat_unlimited_health", currentLanguage));
unlimitedEnergyButton.setText(Strings.get("cheat_unlimited_energy", currentLanguage));
unlimitedAmmoButton.setText(Strings.get("cheat_unlimited_ammo", currentLanguage));
unlimitedItemsButton.setText(Strings.get("cheat_unlimited_items", currentLanguage));
controllerGenericNoteView.setText(Strings.get("generic_profile_note", currentLanguage));
controlResetButton.setText(Strings.get("reset_to_defaults", currentLanguage));
for (int i = 0; i < ACTION_KEYS.length; i++) {
@@ -1351,6 +1477,12 @@ public class MenuOverlay extends OverlayPanel {
// or reassigned - see switchTab().
private static native void nativeSetControllerOptionsTabOpen(boolean open);
// Cheats tab - see qs_config.h. cheatId: 0 give all, 1 toggle physics, 2 next level,
// 3 previous level, 4-7 toggle unlimited health/energy/ammo/items.
private static native void nativeRequestCheat(int cheatId);
private static native int nativeGetCheatUnlimitedMask();
private static native boolean nativeGetCheatPhysicsRunning();
// Called from xr_overlay.c's xr_overlay_init_jni(), on the render
// thread, right after it resolves this class via the activity's own
// ClassLoader (plain FindClass() can't see app classes from a thread
@@ -119,6 +119,19 @@ final class Strings {
put("action_mfd_right_4", "Right MFD: Target", "MFD droit : cible", "Rechtes MFD: Ziel");
put("action_mfd_right_5", "Right MFD: Info", "MFD droit : info", "Rechtes MFD: Info");
put("action_fire_weapon", "Fire Weapon", "Tirer", "Waffe abfeuern");
// Cheats tab.
put("cheats", "Cheats", "Triches", "Cheats");
put("cheats_note", "Cheats need a running game.", "Les triches nécessitent une partie en cours.",
"Cheats benötigen ein laufendes Spiel.");
put("cheat_give_all", "Give All Items & Weapons", "Tout obtenir", "Alles erhalten");
put("cheat_physics", "Physics", "Physique", "Physik");
put("cheat_level_up", "Next Level", "Niveau suivant", "Nächstes Level");
put("cheat_level_down", "Previous Level", "Niveau précédent", "Vorheriges Level");
put("cheat_unlimited_health", "Unlimited Health", "Santé illimitée", "Unbegrenzt Leben");
put("cheat_unlimited_energy", "Unlimited Energy", "Énergie illimitée", "Unbegrenzt Energie");
put("cheat_unlimited_ammo", "Unlimited Ammo", "Munitions illimitées", "Unbegrenzt Munition");
put("cheat_unlimited_items", "Unlimited Items", "Objets illimités", "Unbegrenzt Items");
put("action_free_left_thumbstick_view", "Free View (Left Stick)",
"Vue libre (stick gauche)", "Freie Ansicht (linker Stick)");
put("action_free_right_thumbstick_view", "Free View (Right Stick)",