diff --git a/README.md b/README.md index 99b5325..6714b0c 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,28 @@ arguments = defos.get_arguments() --- +**Prevent screen dimming and sleep** when the game is active (e.g. when using a gamepad). On HTML5, requires an active/visible tab and a browser that supports the Screen Wake Lock API (Chrome 84+, Firefox 126+, Safari 16.4+). Use `defos.is_keep_awake_supported()` to check browser support before calling `defos.set_keep_awake(true)` on HTML5. + +```lua +defos.set_keep_awake(bool_value) +local supported = defos.is_keep_awake_supported() +``` + +Example usage with gamepad input: +```lua +local keep_awake = false +function on_input(self, action_id, action) + if action.gamepad ~= nil then + if not keep_awake then + keep_awake = true + defos.set_keep_awake(true) + end + end +end +``` + +--- + If you'd like to see any other features, open an issue. ## Example diff --git a/defos/api/defos.script_api b/defos/api/defos.script_api index 4fde369..3573b68 100644 --- a/defos/api/defos.script_api +++ b/defos/api/defos.script_api @@ -406,6 +406,22 @@ type: table desc: Table of command line arguments + - name: set_keep_awake + type: function + desc: Prevents or allows the screen from dimming or going to sleep. Useful when the game is controlled by a gamepad. On HTML5, requires an active/visible tab and browser support (Chrome 84+, Firefox 126+, Safari 16.4+). Platforms - Linux, Windows, OSX, HTML5. + parameters: + - name: keep_awake + type: boolean + desc: Whether to prevent screen dimming and sleep + + - name: is_keep_awake_supported + type: function + desc: Checks if the keep awake feature is supported on the current platform/browser. Platforms - Linux, Windows, OSX, HTML5. + returns: + - name: supported + type: boolean + desc: Whether keep awake is supported + - name: CURSOR_ARROW type: number desc: Default arrow cursor diff --git a/defos/ext.manifest b/defos/ext.manifest index 2116d39..f0d79ab 100644 --- a/defos/ext.manifest +++ b/defos/ext.manifest @@ -3,9 +3,9 @@ name: "defos" platforms: osx: context: - frameworks: ["AppKit"] + frameworks: ["AppKit", "IOKit"] linux: context: frameworks: ["X11", "Xrender"] - libs: ["Xrender", "Xfixes"] + libs: ["Xrender", "Xfixes", "Xss"] diff --git a/defos/src/defos.cpp b/defos/src/defos.cpp index 8851381..b138fdd 100644 --- a/defos/src/defos.cpp +++ b/defos/src/defos.cpp @@ -629,6 +629,20 @@ void defos_emit_event(DefosEvent event) assert(top == lua_gettop(L)); } +// Keep awake + +static int set_keep_awake(lua_State *L) +{ + defos_set_keep_awake(checkboolean(L, 1)); + return 0; +} + +static int is_keep_awake_supported(lua_State *L) +{ + lua_pushboolean(L, defos_is_keep_awake_supported()); + return 1; +} + // Lua module initialization static const luaL_reg Module_methods[] = @@ -685,6 +699,8 @@ static const luaL_reg Module_methods[] = {"get_bundle_root", get_bundle_root}, {"get_arguments", get_arguments}, {"get_parameters", get_arguments}, // For backwards compatibility + {"set_keep_awake", set_keep_awake}, + {"is_keep_awake_supported", is_keep_awake_supported}, {0, 0}}; static void LuaInit(lua_State *L) @@ -754,6 +770,7 @@ dmExtension::Result InitializeDefos(dmExtension::Params *params) dmExtension::Result FinalizeDefos(dmExtension::Params *params) { + defos_set_keep_awake(false); defos_final(); return dmExtension::RESULT_OK; } diff --git a/defos/src/defos_html5.cpp b/defos/src/defos_html5.cpp index 2ddf795..bd31aab 100644 --- a/defos/src/defos_html5.cpp +++ b/defos/src/defos_html5.cpp @@ -410,4 +410,71 @@ DisplayID defos_get_current_display() { return NULL; } +void defos_set_keep_awake(bool keep_awake) { + EM_ASM({ + var keep = $0 !== 0; + + // Initialize shared state and helpers once. + if (!Module.__defosjs_keepAwakeInitialized) { + Module.__defosjs_keepAwakeInitialized = true; + Module.__defosjs_keepAwakeDesired = false; + Module.__defosjs_wakeLock = null; + + Module.__defosjs_requestWakeLock = function() { + if (!Module.__defosjs_keepAwakeDesired) { return; } + if (!('wakeLock' in navigator)) { return; } + if (Module.__defosjs_wakeLock) { return; } + navigator.wakeLock.request('screen').then(function(lock) { + // Check whether the desired state changed while the promise was in flight. + if (!Module.__defosjs_keepAwakeDesired) { + lock.release(); + return; + } + Module.__defosjs_wakeLock = lock; + lock.addEventListener('release', function() { + Module.__defosjs_wakeLock = null; + // Re-acquire if still desired (e.g. browser released due to tab hide). + if (Module.__defosjs_keepAwakeDesired) { + Module.__defosjs_requestWakeLock(); + } + }); + }).catch(function(err) { + console.error('defos: wake lock request failed:', err); + }); + }; + + // Re-try when the document becomes visible again. + document.addEventListener('visibilitychange', function() { + if (document.visibilityState === 'visible' && + Module.__defosjs_keepAwakeDesired && + !Module.__defosjs_wakeLock) { + Module.__defosjs_requestWakeLock(); + } + }); + } + + Module.__defosjs_keepAwakeDesired = keep; + + if (keep) { + Module.__defosjs_requestWakeLock(); + } else { + if (Module.__defosjs_wakeLock) { + Module.__defosjs_wakeLock.release().then(function() { + Module.__defosjs_wakeLock = null; + }).catch(function(err) { + console.error('defos: wake lock release failed:', err); + Module.__defosjs_wakeLock = null; + }); + } + } + }, keep_awake ? 1 : 0); +} + +bool defos_is_keep_awake_supported() { + int supported = EM_ASM_INT({ + return ('wakeLock' in navigator) ? 1 : 0; + }); + return supported != 0; +} + #endif diff --git a/defos/src/defos_linux.cpp b/defos/src/defos_linux.cpp index 7b762b7..9c25056 100644 --- a/defos/src/defos_linux.cpp +++ b/defos/src/defos_linux.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -43,6 +44,7 @@ static Display *disp; static int screen; static Window win; static Window root; +static bool g_xss_supported = false; // TODO: add support checking static Atom UTF8_STRING; @@ -104,6 +106,13 @@ void defos_init() is_cursor_actually_visible = true; window_has_focus = true; + // Probe for XScreenSaver extension availability. + // event/error base and version outputs are required by the API but not used beyond the check. + int xss_event_base, xss_error_base; + int xss_major = 0, xss_minor = 0; + g_xss_supported = XScreenSaverQueryExtension(disp, &xss_event_base, &xss_error_base) && + XScreenSaverQueryVersion(disp, &xss_major, &xss_minor); + current_cursor = NULL; memset(default_cursors, 0, DEFOS_CURSOR_INTMAX * sizeof(CustomCursor*)); } @@ -926,4 +935,20 @@ static void send_message(Window &window, Atom type, long a, long b, long c, long XSendEvent(disp, root, False, SubstructureNotifyMask | SubstructureRedirectMask, &event); } +static bool g_keep_awake = false; + +void defos_set_keep_awake(bool keep_awake) +{ + if (keep_awake == g_keep_awake) { return; } + g_keep_awake = keep_awake; + if (!g_xss_supported) { return; } + XScreenSaverSuspend(disp, keep_awake ? True : False); + XFlush(disp); +} + +bool defos_is_keep_awake_supported() +{ + return g_xss_supported; +} + #endif diff --git a/defos/src/defos_mac.mm b/defos/src/defos_mac.mm index bf7b2b8..a7f03cc 100644 --- a/defos/src/defos_mac.mm +++ b/defos/src/defos_mac.mm @@ -817,4 +817,35 @@ static void disable_mouse_tracking() { mouse_tracker = nil; } +#import + +static IOPMAssertionID g_assertion_id = kIOPMNullAssertionID; + +void defos_set_keep_awake(bool keep_awake) +{ + if (keep_awake && g_assertion_id == kIOPMNullAssertionID) + { + CFStringRef reason = CFSTR("Defold game active"); + IOReturn ret = IOPMAssertionCreateWithName( + kIOPMAssertionTypePreventUserIdleDisplaySleep, + kIOPMAssertionLevelOn, + reason, + &g_assertion_id); + if (ret != kIOReturnSuccess) + { + g_assertion_id = kIOPMNullAssertionID; + } + } + else if (!keep_awake && g_assertion_id != kIOPMNullAssertionID) + { + IOPMAssertionRelease(g_assertion_id); + g_assertion_id = kIOPMNullAssertionID; + } +} + +bool defos_is_keep_awake_supported() +{ + return true; +} + #endif diff --git a/defos/src/defos_private.h b/defos/src/defos_private.h index aa481e3..d487900 100644 --- a/defos/src/defos_private.h +++ b/defos/src/defos_private.h @@ -124,3 +124,6 @@ extern void defos_reset_cursor(); extern void defos_get_displays(dmArray &displayList); extern void defos_get_display_modes(DisplayID displayID, dmArray &modeList); extern DisplayID defos_get_current_display(); + +extern void defos_set_keep_awake(bool keep_awake); +extern bool defos_is_keep_awake_supported(); diff --git a/defos/src/defos_win.cpp b/defos/src/defos_win.cpp index da6cd0c..2152129 100644 --- a/defos/src/defos_win.cpp +++ b/defos/src/defos_win.cpp @@ -656,6 +656,27 @@ DisplayID defos_get_current_display() return copy_string(monitorInfo.szDevice); } +static bool g_keep_awake = false; + +void defos_set_keep_awake(bool keep_awake) +{ + if (keep_awake == g_keep_awake) { return; } + g_keep_awake = keep_awake; + if (keep_awake) + { + SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED); + } + else + { + SetThreadExecutionState(ES_CONTINUOUS); + } +} + +bool defos_is_keep_awake_supported() +{ + return true; +} + /******************** * internal functions ********************/ diff --git a/example/example.gui b/example/example.gui index 18fea8d..4219f15 100644 --- a/example/example.gui +++ b/example/example.gui @@ -769,6 +769,41 @@ nodes { overridden_fields: 18 template_node_child: true } +nodes { + position { + x: 635.0 + y: 350.0 + } + scale { + x: 0.75 + y: 0.75 + } + type: TYPE_TEMPLATE + id: "toggle_keep_awake" + inherit_alpha: true + template: "/dirtylarry/button.gui" +} +nodes { + type: TYPE_BOX + id: "toggle_keep_awake/larrybutton" + parent: "toggle_keep_awake" + template_node_child: true +} +nodes { + size { + x: 250.0 + y: 100.0 + } + type: TYPE_TEXT + text: "Keep screen awake" + id: "toggle_keep_awake/larrylabel" + line_break: true + parent: "toggle_keep_awake/larrybutton" + overridden_fields: 4 + overridden_fields: 8 + overridden_fields: 18 + template_node_child: true +} nodes { position { x: 525.0 diff --git a/example/example.gui_script b/example/example.gui_script index c04294f..fad6c44 100644 --- a/example/example.gui_script +++ b/example/example.gui_script @@ -95,6 +95,15 @@ function init(self) if gui.get_flipbook(gui.get_node("toggle_borderless/larrybutton")) == hash("button_pressed") then defos.toggle_borderless() end + if gui.get_flipbook(gui.get_node("toggle_keep_awake/larrybutton")) == hash("button_pressed") then + if defos.is_keep_awake_supported() then + self.keep_awake = not self.keep_awake + defos.set_keep_awake(self.keep_awake) + gui.set_text(gui.get_node("toggle_keep_awake/larrylabel"), self.keep_awake and "Allow screen sleep" or "Keep screen awake") + else + print("defos: Keep-awake / Wake Lock is not supported on this platform.") + end + end end) end @@ -106,6 +115,7 @@ function init(self) self.cursor_visible = true self.cursor_clipped = false self.cursor_locked = false + self.keep_awake = false msg.post(".", "acquire_input_focus") -- Console related commands only work on Windows and in bundled builds not in editor builds @@ -253,4 +263,12 @@ function on_input(self, action_id, action) defos.set_cursor(self.cursors[self.current_cursor]) end) + + dirtylarry:button("toggle_keep_awake", action_id, action, function() + if system_name ~= "HTML5" then + self.keep_awake = not self.keep_awake + defos.set_keep_awake(self.keep_awake) + gui.set_text(gui.get_node("toggle_keep_awake/larrylabel"), self.keep_awake and "Allow screen sleep" or "Keep screen awake") + end + end) end