|
|
[Blackout.Lua] ScreenSaver/OLED protection
Fenrir.Jinxs
Serveur: Fenrir
Game: FFXI
Posts: 1209
By Fenrir.Jinxs 2026-05-27 09:50:14
Blackout
Blackout is a lightweight utility addon designed to prevent screen burn-in, and manage your game client when you step away from the keyboard.
What It Does & When
Screen Saver Overlay (Default: 5 minutes idle)
After 5 minutes of inactivity, Blackout displays a full-screen solid black overlay to act as a screensaver.
It also automatically toggles the FFXI FPS display off when active and back on when you return, ensuring the screen is completely black.
Auto-Minimize (Default: 10 minutes idle)
After 10 minutes of inactivity, Blackout will automatically minimize the game client to the Windows taskbar using Windower's built-in minimization command.
Any activation includes a timestamp (Activated, Minimized, Deactivated)
Smart Inactivity Detection
The idle timer automatically resets upon any keyboard input or mouse movement.
It tracks character coordinates ($X$, $Y$, $Z$) and camera facing direction, so auto-running, getting moved, or panning the camera will keep you marked as active.
It automatically halts idle timers if you are in combat or dead (so the screen won't go black in the middle of a fight or while waiting for a raise).
Commands
//blackout / //blackout toggle - Manually activate/deactivate the screensaver.
//blackout auto [on|off] - Enable or disable the automatic idle screensaver.
//blackout timeout [seconds] - Set how long to wait before activating the screensaver (default: 300).
//blackout minimize [on|off] - Enable or disable automatic client minimization (default: on).
//blackout minimizetimeout [seconds] - Set how long to wait before minimizing the game client (default: 600).
//blackout combat [on|off] - Allow/prevent screensaver and minimization while in combat.
//blackout dead [on|off] - Allow/prevent screensaver and minimization while dead.
//blackout fps [on|off] - Enable/disable automatic FPS display toggle when screen saving.
//blackout status - Show all current settings and timers. Code _addon.name = 'Blackout'
_addon.author = 'Jinxs'
_addon.version = '3.0'
_addon.commands = {'blackout'}
local texts = require('texts')
local config = require('config')
-- Localize standard library functions
local math_abs = math.abs
local math_pi = math.pi
local os_clock = os.clock
local os_date = os.date
local os_time = os.time
local string_rep = string.rep
local coroutine_sleep = coroutine.sleep
local coroutine_schedule = coroutine.schedule
-- Localize Windower APIs
local windower_ffxi = windower.ffxi
local windower_add_to_chat = windower.add_to_chat
local windower_send_command = windower.send_command
local windower_get_windower_settings = windower.get_windower_settings
local texts_new = texts.new
math.randomseed(os_time())
-- Constants
local EPSILON = 0.001
local EPSILON_FACING = 0.0001
local TWO_PI = 2 * math_pi
local ACTIVE_SLEEP = 3 -- Check every 3 seconds when screensaver is active for quick gamepad/combat wakeup
local INACTIVE_SLEEP = 10 -- Check every 10 seconds when playing normally (near-zero overhead)
-- Default settings
local defaults = {}
defaults.idle_timeout = 300 -- 5 minutes in seconds
defaults.disable_in_combat = true
defaults.disable_if_dead = true
defaults.show_fps_on_off = true
defaults.auto_enabled = true -- Whether the automatic screensaver is active
defaults.minimize_enabled = true
defaults.minimize_timeout = 600 -- 10 minutes in seconds
local settings = config.load(defaults)
local overlay = nil
local overlay_visible = false
local last_activity_time = os_clock()
local player_id = nil
local last_x, last_y, last_z, last_facing = nil, nil, nil, nil
local minimized_triggered = false
local function get_res()
local s = windower_get_windower_settings()
return s.ui_x_res, s.ui_y_res
end
local function create_overlay()
if overlay then
overlay:destroy()
overlay = nil
end
overlay = texts_new()
local w, h = get_res()
overlay:pos(0, 0)
overlay:size(w, h)
overlay:bg_visible(true)
overlay:bg_color(0, 0, 0)
overlay:bg_alpha(255)
overlay:text(string_rep(" ", 5000))
overlay:draggable(false)
overlay:hide()
overlay_visible = false
end
local function show_overlay()
if overlay and not overlay_visible then
overlay:show()
overlay_visible = true
if settings.show_fps_on_off then
windower_send_command('showfps 0')
end
local timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Screen Saver activated.')
if settings.minimize_enabled then
local delay = settings.minimize_timeout - settings.idle_timeout
if delay <= 0 then
minimized_triggered = true
windower_send_command('game_minimize')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Game minimized due to inactivity.')
else
coroutine_schedule(function()
if overlay_visible and not minimized_triggered and settings.minimize_enabled then
minimized_triggered = true
windower_send_command('game_minimize')
local min_timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. min_timestamp .. '] Game minimized due to inactivity.')
end
end, delay)
end
end
end
end
local function hide_overlay(triggered_by_user)
if overlay_visible then
if overlay then
overlay:hide()
end
overlay_visible = false
if settings.show_fps_on_off then
windower_send_command('showfps 1')
end
if triggered_by_user then
local timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Screen Saver deactivated.')
end
end
end
local function reset_activity()
minimized_triggered = false
if overlay_visible then
hide_overlay(true)
last_activity_time = os_clock()
else
-- Micro-optimization: Only update timestamp at most once per second
-- to prevent excessive local variable writes during rapid mouse movement.
local now = os_clock()
if now - last_activity_time > 1 then
last_activity_time = now
end
end
end
-- Cache Player ID
local function update_player_id()
local player = windower_ffxi.get_player()
if player then
player_id = player.id
else
player_id = nil
end
end
-- Check character movement/combat status
local function check_game_state()
-- Optimization: Skip all entity queries and math if direct keyboard/mouse inputs
-- were registered in the last INACTIVE_SLEEP seconds. Skip this skip if screensaver is active.
if not overlay_visible and (os_clock() - last_activity_time < INACTIVE_SLEEP) then
return
end
if not player_id then
update_player_id()
end
if not player_id then return end
local player_mob = windower_ffxi.get_mob_by_id(player_id)
if player_mob then
-- Check movement
local current_x = player_mob.x
local current_y = player_mob.y
local current_z = player_mob.z
local current_facing = player_mob.facing
if last_x then
local diff_facing = math_abs(current_facing - last_facing)
if diff_facing > math_pi then
diff_facing = TWO_PI - diff_facing
end
if math_abs(current_x - last_x) > EPSILON or
math_abs(current_y - last_y) > EPSILON or
math_abs(current_z - last_z) > EPSILON or
diff_facing > EPSILON_FACING then
reset_activity()
end
end
last_x = current_x
last_y = current_y
last_z = current_z
last_facing = current_facing
-- Check player status (combat/death) using player_mob.status
-- status 1 is Engaged (combat), status 3 is Dead
if (settings.disable_in_combat and player_mob.status == 1) or
(settings.disable_if_dead and player_mob.status == 3) then
reset_activity()
end
end
end
-- Idle monitoring loop (checks dynamically based on screensaver state)
local function idle_loop()
while true do
local sleep_time = overlay_visible and ACTIVE_SLEEP or INACTIVE_SLEEP
coroutine_sleep(sleep_time)
check_game_state()
-- Check if we should activate the screensaver
if settings.auto_enabled and not overlay_visible and (os_clock() - last_activity_time >= settings.idle_timeout) then
show_overlay()
end
end
end
-- Register Event Listeners for direct user activity
windower.register_event('keyboard', function(dik, pressed, flags, blocked)
if pressed then
reset_activity()
end
end)
windower.register_event('mouse', function(type, x, y, delta, blocked)
-- Any mouse interaction (type 0: movement/drag, 1: left click, etc.)
reset_activity()
end)
windower.register_event('load', function()
create_overlay()
update_player_id()
coroutine_schedule(idle_loop, INACTIVE_SLEEP)
end)
windower.register_event('login', function()
create_overlay()
update_player_id()
end)
windower.register_event('logout', function()
player_id = nil
last_x, last_y, last_z, last_facing = nil, nil, nil, nil
hide_overlay(false)
end)
windower.register_event('unload', function()
hide_overlay(false)
if overlay then
overlay:destroy()
overlay = nil
end
end)
local function print_help()
windower_add_to_chat(207, '[Blackout] Command Help:')
windower_add_to_chat(207, ' //blackout - Toggles the screensaver overlay manually.')
windower_add_to_chat(207, ' //blackout on - Manually activate the screensaver overlay.')
windower_add_to_chat(207, ' //blackout off - Manually deactivate the screensaver overlay.')
windower_add_to_chat(207, ' //blackout auto [on|off] - Enable/disable the automatic idle screensaver.')
windower_add_to_chat(207, ' //blackout timeout [seconds] - Set/view the idle timeout (default 300).')
windower_add_to_chat(207, ' //blackout minimize [on|off] - Enable/disable automatic client minimization.')
windower_add_to_chat(207, ' //blackout minimizetimeout [seconds] - Set/view the minimize timeout (default 600).')
windower_add_to_chat(207, ' //blackout combat [on|off] - Enable/disable screensaver in combat.')
windower_add_to_chat(207, ' //blackout dead [on|off] - Enable/disable screensaver when dead.')
windower_add_to_chat(207, ' //blackout fps [on|off] - Enable/disable FPS display toggle on screen save.')
windower_add_to_chat(207, ' //blackout status - Show current settings and status.')
windower_add_to_chat(207, ' //blackout help - Display this help menu.')
end
windower.register_event('addon command', function(cmd, ...)
cmd = cmd and cmd:lower() or ''
local args = {...}
if cmd == 'on' then
show_overlay()
elseif cmd == 'off' then
hide_overlay(true)
elseif cmd == 'auto' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.auto_enabled = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Auto screensaver is now ENABLED.')
elseif sub_cmd == 'off' then
settings.auto_enabled = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Auto screensaver is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Auto screensaver is currently ' .. (settings.auto_enabled and 'ENABLED' or 'DISABLED') .. '. Use "//blackout auto on" or "//blackout auto off" to change.')
end
elseif cmd == 'timeout' then
local new_timeout = tonumber(args[1])
if new_timeout and new_timeout > 0 then
settings.idle_timeout = new_timeout
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle timeout set to ' .. new_timeout .. ' seconds.')
else
windower_add_to_chat(207, '[Blackout] Current idle timeout is ' .. settings.idle_timeout .. ' seconds.')
end
elseif cmd == 'minimize' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.minimize_enabled = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle minimization is now ENABLED.')
elseif sub_cmd == 'off' then
settings.minimize_enabled = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle minimization is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Idle minimization is currently ' .. (settings.minimize_enabled and 'ENABLED' or 'DISABLED') .. '. Use "//blackout minimize on" or "//blackout minimize off" to change.')
end
elseif cmd == 'minimizetimeout' or cmd == 'minimize_timeout' then
local new_timeout = tonumber(args[1])
if new_timeout and new_timeout > 0 then
settings.minimize_timeout = new_timeout
config.save(settings)
windower_add_to_chat(207, '[Blackout] Minimize timeout set to ' .. new_timeout .. ' seconds.')
else
windower_add_to_chat(207, '[Blackout] Current minimize timeout is ' .. settings.minimize_timeout .. ' seconds.')
end
elseif cmd == 'combat' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.disable_in_combat = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable in combat is now ENABLED.')
elseif sub_cmd == 'off' then
settings.disable_in_combat = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable in combat is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Disable in combat is currently ' .. (settings.disable_in_combat and 'ENABLED' or 'DISABLED') .. '. Use "//blackout combat on" or "//blackout combat off" to change.')
end
elseif cmd == 'dead' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.disable_if_dead = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable if dead is now ENABLED.')
elseif sub_cmd == 'off' then
settings.disable_if_dead = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable if dead is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Disable if dead is currently ' .. (settings.disable_if_dead and 'ENABLED' or 'DISABLED') .. '. Use "//blackout dead on" or "//blackout dead off" to change.')
end
elseif cmd == 'fps' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.show_fps_on_off = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] FPS toggle is now ENABLED.')
elseif sub_cmd == 'off' then
settings.show_fps_on_off = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] FPS toggle is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] FPS toggle is currently ' .. (settings.show_fps_on_off and 'ENABLED' or 'DISABLED') .. '. Use "//blackout fps on" or "//blackout fps off" to change.')
end
elseif cmd == 'status' then
windower_add_to_chat(207, '[Blackout] Status - Auto: ' .. (settings.auto_enabled and 'ON' or 'OFF') .. ', Timeout: ' .. settings.idle_timeout .. 's, Minimize: ' .. (settings.minimize_enabled and 'ON' or 'OFF') .. ', Minimize Timeout: ' .. settings.minimize_timeout .. 's, Disable in Combat: ' .. tostring(settings.disable_in_combat) .. ', Disable if Dead: ' .. tostring(settings.disable_if_dead) .. ', FPS Toggle: ' .. tostring(settings.show_fps_on_off))
elseif cmd == 'help' or cmd == 'h' or cmd == '?' then
print_help()
elseif cmd == '' or cmd == 'toggle' then
-- Toggle manually
if overlay and overlay_visible then
hide_overlay(true)
else
show_overlay()
end
else
windower_add_to_chat(207, '[Blackout] Unknown command: "' .. cmd .. '"')
print_help()
end
end)
[+]
Valefor.Keylesta
Serveur: Valefor
Game: FFXI
Posts: 208
By Valefor.Keylesta 2026-05-27 10:59:18
Oh man, great minds eh? I just recently upgraded to a new Alienware OLED monitor and started working on a screensaver addon on the side too lol. I got a good chunk of work done on it before I hit a snag and decided to sit on it for a while and work on other stuff (new Bar in the works, don't tell anyone), so this'll be good to have in the meanwhile :D
Valefor.Keylesta
Serveur: Valefor
Game: FFXI
Posts: 208
By Valefor.Keylesta 2026-05-27 11:05:48
If I may, I suggest adding a setting for the bg_alpha scale. Keep default at 255 but open it up to be adjusted in the settings.xml file.
By Nuckinfuts 2026-05-27 12:24:26
Going to test this out when I get home. Upgrading to a 5th gen tandem oled this year... definitely going to need this. Thank you!
Fenrir.Jinxs
Serveur: Fenrir
Game: FFXI
Posts: 1209
By Fenrir.Jinxs 2026-05-27 12:26:55
I wish I knew about the minimize command before I started on this.
added custom bg scale
This has been mostly antigravity built.
Code _addon.name = 'Blackout'
_addon.author = 'Jinxs'
_addon.version = '3.0'
_addon.commands = {'blackout'}
local texts = require('texts')
local config = require('config')
-- Localize standard library functions
local math_abs = math.abs
local math_pi = math.pi
local os_clock = os.clock
local os_date = os.date
local os_time = os.time
local string_rep = string.rep
local coroutine_sleep = coroutine.sleep
local coroutine_schedule = coroutine.schedule
-- Localize Windower APIs
local windower_ffxi = windower.ffxi
local windower_add_to_chat = windower.add_to_chat
local windower_send_command = windower.send_command
local windower_get_windower_settings = windower.get_windower_settings
local texts_new = texts.new
math.randomseed(os_time())
-- Constants
local EPSILON = 0.001
local EPSILON_FACING = 0.0001
local TWO_PI = 2 * math_pi
local ACTIVE_SLEEP = 3 -- Check every 3 seconds when screensaver is active for quick gamepad/combat wakeup
local INACTIVE_SLEEP = 10 -- Check every 10 seconds when playing normally (near-zero overhead)
-- Default settings
local defaults = {}
defaults.idle_timeout = 300 -- 5 minutes in seconds
defaults.disable_in_combat = true
defaults.disable_if_dead = true
defaults.show_fps_on_off = true
defaults.auto_enabled = true -- Whether the automatic screensaver is active
defaults.minimize_enabled = true
defaults.minimize_timeout = 600 -- 10 minutes in seconds
defaults.bg_alpha = 255 -- Default background alpha (opacity)
local settings = config.load(defaults)
local overlay = nil
local overlay_visible = false
local last_activity_time = os_clock()
local player_id = nil
local last_x, last_y, last_z, last_facing = nil, nil, nil, nil
local minimized_triggered = false
local function get_res()
local s = windower_get_windower_settings()
return s.ui_x_res, s.ui_y_res
end
local function create_overlay()
if overlay then
overlay:destroy()
overlay = nil
end
overlay = texts_new()
local w, h = get_res()
overlay:pos(0, 0)
overlay:size(w, h)
overlay:bg_visible(true)
overlay:bg_color(0, 0, 0)
overlay:bg_alpha(settings.bg_alpha)
overlay:text(string_rep(" ", 5000))
overlay:draggable(false)
overlay:hide()
overlay_visible = false
end
local function show_overlay()
if overlay and not overlay_visible then
overlay:show()
overlay_visible = true
if settings.show_fps_on_off then
windower_send_command('showfps 0')
end
local timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Screen Saver activated.')
if settings.minimize_enabled then
local delay = settings.minimize_timeout - settings.idle_timeout
if delay <= 0 then
minimized_triggered = true
windower_send_command('game_minimize')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Game minimized due to inactivity.')
else
coroutine_schedule(function()
if overlay_visible and not minimized_triggered and settings.minimize_enabled then
minimized_triggered = true
windower_send_command('game_minimize')
local min_timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. min_timestamp .. '] Game minimized due to inactivity.')
end
end, delay)
end
end
end
end
local function hide_overlay(triggered_by_user)
if overlay_visible then
if overlay then
overlay:hide()
end
overlay_visible = false
if settings.show_fps_on_off then
windower_send_command('showfps 1')
end
if triggered_by_user then
local timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Screen Saver deactivated.')
end
end
end
local function reset_activity()
minimized_triggered = false
if overlay_visible then
hide_overlay(true)
last_activity_time = os_clock()
else
-- Micro-optimization: Only update timestamp at most once per second
-- to prevent excessive local variable writes during rapid mouse movement.
local now = os_clock()
if now - last_activity_time > 1 then
last_activity_time = now
end
end
end
-- Cache Player ID
local function update_player_id()
local player = windower_ffxi.get_player()
if player then
player_id = player.id
else
player_id = nil
end
end
-- Check character movement/combat status
local function check_game_state()
-- Optimization: Skip all entity queries and math if direct keyboard/mouse inputs
-- were registered in the last INACTIVE_SLEEP seconds. Skip this skip if screensaver is active.
if not overlay_visible and (os_clock() - last_activity_time < INACTIVE_SLEEP) then
return
end
if not player_id then
update_player_id()
end
if not player_id then return end
local player_mob = windower_ffxi.get_mob_by_id(player_id)
if player_mob then
-- Check movement
local current_x = player_mob.x
local current_y = player_mob.y
local current_z = player_mob.z
local current_facing = player_mob.facing
if last_x then
local diff_facing = math_abs(current_facing - last_facing)
if diff_facing > math_pi then
diff_facing = TWO_PI - diff_facing
end
if math_abs(current_x - last_x) > EPSILON or
math_abs(current_y - last_y) > EPSILON or
math_abs(current_z - last_z) > EPSILON or
diff_facing > EPSILON_FACING then
reset_activity()
end
end
last_x = current_x
last_y = current_y
last_z = current_z
last_facing = current_facing
-- Check player status (combat/death) using player_mob.status
-- status 1 is Engaged (combat), status 3 is Dead
if (settings.disable_in_combat and player_mob.status == 1) or
(settings.disable_if_dead and player_mob.status == 3) then
reset_activity()
end
end
end
-- Idle monitoring loop (checks dynamically based on screensaver state)
local function idle_loop()
while true do
local sleep_time = overlay_visible and ACTIVE_SLEEP or INACTIVE_SLEEP
coroutine_sleep(sleep_time)
check_game_state()
-- Check if we should activate the screensaver
if settings.auto_enabled and not overlay_visible and (os_clock() - last_activity_time >= settings.idle_timeout) then
show_overlay()
end
end
end
-- Register Event Listeners for direct user activity
windower.register_event('keyboard', function(dik, pressed, flags, blocked)
if pressed then
reset_activity()
end
end)
windower.register_event('mouse', function(type, x, y, delta, blocked)
-- Any mouse interaction (type 0: movement/drag, 1: left click, etc.)
reset_activity()
end)
windower.register_event('load', function()
create_overlay()
update_player_id()
coroutine_schedule(idle_loop, INACTIVE_SLEEP)
end)
windower.register_event('login', function()
create_overlay()
update_player_id()
end)
windower.register_event('logout', function()
player_id = nil
last_x, last_y, last_z, last_facing = nil, nil, nil, nil
hide_overlay(false)
end)
windower.register_event('unload', function()
hide_overlay(false)
if overlay then
overlay:destroy()
overlay = nil
end
end)
local function print_help()
windower_add_to_chat(207, '[Blackout] Command Help:')
windower_add_to_chat(207, ' //blackout - Toggles the screensaver overlay manually.')
windower_add_to_chat(207, ' //blackout on - Manually activate the screensaver overlay.')
windower_add_to_chat(207, ' //blackout off - Manually deactivate the screensaver overlay.')
windower_add_to_chat(207, ' //blackout auto [on|off] - Enable/disable the automatic idle screensaver.')
windower_add_to_chat(207, ' //blackout timeout [seconds] - Set/view the idle timeout (default 300).')
windower_add_to_chat(207, ' //blackout minimize [on|off] - Enable/disable automatic client minimization.')
windower_add_to_chat(207, ' //blackout minimizetimeout [seconds] - Set/view the minimize timeout (default 600).')
windower_add_to_chat(207, ' //blackout alpha [0-255] - Set screensaver background opacity (default 255).')
windower_add_to_chat(207, ' //blackout combat [on|off] - Enable/disable screensaver in combat.')
windower_add_to_chat(207, ' //blackout dead [on|off] - Enable/disable screensaver when dead.')
windower_add_to_chat(207, ' //blackout fps [on|off] - Enable/disable FPS display toggle on screen save.')
windower_add_to_chat(207, ' //blackout status - Show current settings and status.')
windower_add_to_chat(207, ' //blackout help - Display this help menu.')
end
windower.register_event('addon command', function(cmd, ...)
cmd = cmd and cmd:lower() or ''
local args = {...}
if cmd == 'on' then
show_overlay()
elseif cmd == 'off' then
hide_overlay(true)
elseif cmd == 'auto' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.auto_enabled = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Auto screensaver is now ENABLED.')
elseif sub_cmd == 'off' then
settings.auto_enabled = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Auto screensaver is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Auto screensaver is currently ' .. (settings.auto_enabled and 'ENABLED' or 'DISABLED') .. '. Use "//blackout auto on" or "//blackout auto off" to change.')
end
elseif cmd == 'timeout' then
local new_timeout = tonumber(args[1])
if new_timeout and new_timeout > 0 then
settings.idle_timeout = new_timeout
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle timeout set to ' .. new_timeout .. ' seconds.')
else
windower_add_to_chat(207, '[Blackout] Current idle timeout is ' .. settings.idle_timeout .. ' seconds.')
end
elseif cmd == 'minimize' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.minimize_enabled = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle minimization is now ENABLED.')
elseif sub_cmd == 'off' then
settings.minimize_enabled = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle minimization is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Idle minimization is currently ' .. (settings.minimize_enabled and 'ENABLED' or 'DISABLED') .. '. Use "//blackout minimize on" or "//blackout minimize off" to change.')
end
elseif cmd == 'minimizetimeout' or cmd == 'minimize_timeout' then
local new_timeout = tonumber(args[1])
if new_timeout and new_timeout > 0 then
settings.minimize_timeout = new_timeout
config.save(settings)
windower_add_to_chat(207, '[Blackout] Minimize timeout set to ' .. new_timeout .. ' seconds.')
else
windower_add_to_chat(207, '[Blackout] Current minimize timeout is ' .. settings.minimize_timeout .. ' seconds.')
end
elseif cmd == 'combat' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.disable_in_combat = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable in combat is now ENABLED.')
elseif sub_cmd == 'off' then
settings.disable_in_combat = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable in combat is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Disable in combat is currently ' .. (settings.disable_in_combat and 'ENABLED' or 'DISABLED') .. '. Use "//blackout combat on" or "//blackout combat off" to change.')
end
elseif cmd == 'dead' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.disable_if_dead = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable if dead is now ENABLED.')
elseif sub_cmd == 'off' then
settings.disable_if_dead = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable if dead is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Disable if dead is currently ' .. (settings.disable_if_dead and 'ENABLED' or 'DISABLED') .. '. Use "//blackout dead on" or "//blackout dead off" to change.')
end
elseif cmd == 'alpha' or cmd == 'bg_alpha' then
local new_alpha = tonumber(args[1])
if new_alpha and new_alpha >= 0 and new_alpha <= 255 then
settings.bg_alpha = math.floor(new_alpha)
config.save(settings)
if overlay then
overlay:bg_alpha(settings.bg_alpha)
end
windower_add_to_chat(207, '[Blackout] Background alpha set to ' .. settings.bg_alpha .. '.')
else
windower_add_to_chat(207, '[Blackout] Current background alpha is ' .. settings.bg_alpha .. ' (0-255).')
end
elseif cmd == 'fps' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.show_fps_on_off = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] FPS toggle is now ENABLED.')
elseif sub_cmd == 'off' then
settings.show_fps_on_off = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] FPS toggle is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] FPS toggle is currently ' .. (settings.show_fps_on_off and 'ENABLED' or 'DISABLED') .. '. Use "//blackout fps on" or "//blackout fps off" to change.')
end
elseif cmd == 'status' then
windower_add_to_chat(207, '[Blackout] Status - Auto: ' .. (settings.auto_enabled and 'ON' or 'OFF') .. ', Timeout: ' .. settings.idle_timeout .. 's, Minimize: ' .. (settings.minimize_enabled and 'ON' or 'OFF') .. ', Minimize Timeout: ' .. settings.minimize_timeout .. 's, Alpha: ' .. settings.bg_alpha .. ', Disable in Combat: ' .. tostring(settings.disable_in_combat) .. ', Disable if Dead: ' .. tostring(settings.disable_if_dead) .. ', FPS Toggle: ' .. tostring(settings.show_fps_on_off))
elseif cmd == 'help' or cmd == 'h' or cmd == '?' then
print_help()
elseif cmd == '' or cmd == 'toggle' then
-- Toggle manually
if overlay and overlay_visible then
hide_overlay(true)
else
show_overlay()
end
else
windower_add_to_chat(207, '[Blackout] Unknown command: "' .. cmd .. '"')
print_help()
end
end)
Blackout
Blackout is a lightweight utility addon designed to prevent screen burn-in, and manage your game client when you step away from the keyboard.
What It Does & When
Screen Saver Overlay (Default: 5 minutes idle)
After 5 minutes of inactivity, Blackout displays a full-screen solid black overlay to act as a screensaver.
It also automatically toggles the FFXI FPS display off when active and back on when you return, ensuring the screen is completely black.
Auto-Minimize (Default: 10 minutes idle)
After 10 minutes of inactivity, Blackout will automatically minimize the game client to the Windows taskbar using Windower's built-in minimization command.
Any activation includes a timestamp (Activated, Minimized, Deactivated)
Smart Inactivity Detection
The idle timer automatically resets upon any keyboard input or mouse movement.
It tracks character coordinates ($X$, $Y$, $Z$) and camera facing direction, so auto-running, getting moved, or panning the camera will keep you marked as active.
It automatically halts idle timers if you are in combat or dead (so the screen won't go black in the middle of a fight or while waiting for a raise).
Commands
//blackout / //blackout toggle - Manually activate/deactivate the screensaver.
//blackout auto [on|off] - Enable or disable the automatic idle screensaver.
//blackout timeout [seconds] - Set how long to wait before activating the screensaver (default: 300).
//blackout minimize [on|off] - Enable or disable automatic client minimization (default: on).
//blackout minimizetimeout [seconds] - Set how long to wait before minimizing the game client (default: 600).
//blackout combat [on|off] - Allow/prevent screensaver and minimization while in combat.
//blackout dead [on|off] - Allow/prevent screensaver and minimization while dead.
//blackout fps [on|off] - Enable/disable automatic FPS display toggle when screen saving.
//blackout status - Show all current settings and timers. Code _addon.name = 'Blackout'
_addon.author = 'Jinxs'
_addon.version = '3.0'
_addon.commands = {'blackout'}
local texts = require('texts')
local config = require('config')
-- Localize standard library functions
local math_abs = math.abs
local math_pi = math.pi
local os_clock = os.clock
local os_date = os.date
local os_time = os.time
local string_rep = string.rep
local coroutine_sleep = coroutine.sleep
local coroutine_schedule = coroutine.schedule
-- Localize Windower APIs
local windower_ffxi = windower.ffxi
local windower_add_to_chat = windower.add_to_chat
local windower_send_command = windower.send_command
local windower_get_windower_settings = windower.get_windower_settings
local texts_new = texts.new
math.randomseed(os_time())
-- Constants
local EPSILON = 0.001
local EPSILON_FACING = 0.0001
local TWO_PI = 2 * math_pi
local ACTIVE_SLEEP = 3 -- Check every 3 seconds when screensaver is active for quick gamepad/combat wakeup
local INACTIVE_SLEEP = 10 -- Check every 10 seconds when playing normally (near-zero overhead)
-- Default settings
local defaults = {}
defaults.idle_timeout = 300 -- 5 minutes in seconds
defaults.disable_in_combat = true
defaults.disable_if_dead = true
defaults.show_fps_on_off = true
defaults.auto_enabled = true -- Whether the automatic screensaver is active
defaults.minimize_enabled = true
defaults.minimize_timeout = 600 -- 10 minutes in seconds
local settings = config.load(defaults)
local overlay = nil
local overlay_visible = false
local last_activity_time = os_clock()
local player_id = nil
local last_x, last_y, last_z, last_facing = nil, nil, nil, nil
local minimized_triggered = false
local function get_res()
local s = windower_get_windower_settings()
return s.ui_x_res, s.ui_y_res
end
local function create_overlay()
if overlay then
overlay:destroy()
overlay = nil
end
overlay = texts_new()
local w, h = get_res()
overlay:pos(0, 0)
overlay:size(w, h)
overlay:bg_visible(true)
overlay:bg_color(0, 0, 0)
overlay:bg_alpha(255)
overlay:text(string_rep(" ", 5000))
overlay:draggable(false)
overlay:hide()
overlay_visible = false
end
local function show_overlay()
if overlay and not overlay_visible then
overlay:show()
overlay_visible = true
if settings.show_fps_on_off then
windower_send_command('showfps 0')
end
local timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Screen Saver activated.')
if settings.minimize_enabled then
local delay = settings.minimize_timeout - settings.idle_timeout
if delay <= 0 then
minimized_triggered = true
windower_send_command('game_minimize')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Game minimized due to inactivity.')
else
coroutine_schedule(function()
if overlay_visible and not minimized_triggered and settings.minimize_enabled then
minimized_triggered = true
windower_send_command('game_minimize')
local min_timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. min_timestamp .. '] Game minimized due to inactivity.')
end
end, delay)
end
end
end
end
local function hide_overlay(triggered_by_user)
if overlay_visible then
if overlay then
overlay:hide()
end
overlay_visible = false
if settings.show_fps_on_off then
windower_send_command('showfps 1')
end
if triggered_by_user then
local timestamp = os_date('%H:%M:%S')
windower_add_to_chat(207, '[Blackout] [' .. timestamp .. '] Screen Saver deactivated.')
end
end
end
local function reset_activity()
minimized_triggered = false
if overlay_visible then
hide_overlay(true)
last_activity_time = os_clock()
else
-- Micro-optimization: Only update timestamp at most once per second
-- to prevent excessive local variable writes during rapid mouse movement.
local now = os_clock()
if now - last_activity_time > 1 then
last_activity_time = now
end
end
end
-- Cache Player ID
local function update_player_id()
local player = windower_ffxi.get_player()
if player then
player_id = player.id
else
player_id = nil
end
end
-- Check character movement/combat status
local function check_game_state()
-- Optimization: Skip all entity queries and math if direct keyboard/mouse inputs
-- were registered in the last INACTIVE_SLEEP seconds. Skip this skip if screensaver is active.
if not overlay_visible and (os_clock() - last_activity_time < INACTIVE_SLEEP) then
return
end
if not player_id then
update_player_id()
end
if not player_id then return end
local player_mob = windower_ffxi.get_mob_by_id(player_id)
if player_mob then
-- Check movement
local current_x = player_mob.x
local current_y = player_mob.y
local current_z = player_mob.z
local current_facing = player_mob.facing
if last_x then
local diff_facing = math_abs(current_facing - last_facing)
if diff_facing > math_pi then
diff_facing = TWO_PI - diff_facing
end
if math_abs(current_x - last_x) > EPSILON or
math_abs(current_y - last_y) > EPSILON or
math_abs(current_z - last_z) > EPSILON or
diff_facing > EPSILON_FACING then
reset_activity()
end
end
last_x = current_x
last_y = current_y
last_z = current_z
last_facing = current_facing
-- Check player status (combat/death) using player_mob.status
-- status 1 is Engaged (combat), status 3 is Dead
if (settings.disable_in_combat and player_mob.status == 1) or
(settings.disable_if_dead and player_mob.status == 3) then
reset_activity()
end
end
end
-- Idle monitoring loop (checks dynamically based on screensaver state)
local function idle_loop()
while true do
local sleep_time = overlay_visible and ACTIVE_SLEEP or INACTIVE_SLEEP
coroutine_sleep(sleep_time)
check_game_state()
-- Check if we should activate the screensaver
if settings.auto_enabled and not overlay_visible and (os_clock() - last_activity_time >= settings.idle_timeout) then
show_overlay()
end
end
end
-- Register Event Listeners for direct user activity
windower.register_event('keyboard', function(dik, pressed, flags, blocked)
if pressed then
reset_activity()
end
end)
windower.register_event('mouse', function(type, x, y, delta, blocked)
-- Any mouse interaction (type 0: movement/drag, 1: left click, etc.)
reset_activity()
end)
windower.register_event('load', function()
create_overlay()
update_player_id()
coroutine_schedule(idle_loop, INACTIVE_SLEEP)
end)
windower.register_event('login', function()
create_overlay()
update_player_id()
end)
windower.register_event('logout', function()
player_id = nil
last_x, last_y, last_z, last_facing = nil, nil, nil, nil
hide_overlay(false)
end)
windower.register_event('unload', function()
hide_overlay(false)
if overlay then
overlay:destroy()
overlay = nil
end
end)
local function print_help()
windower_add_to_chat(207, '[Blackout] Command Help:')
windower_add_to_chat(207, ' //blackout - Toggles the screensaver overlay manually.')
windower_add_to_chat(207, ' //blackout on - Manually activate the screensaver overlay.')
windower_add_to_chat(207, ' //blackout off - Manually deactivate the screensaver overlay.')
windower_add_to_chat(207, ' //blackout auto [on|off] - Enable/disable the automatic idle screensaver.')
windower_add_to_chat(207, ' //blackout timeout [seconds] - Set/view the idle timeout (default 300).')
windower_add_to_chat(207, ' //blackout minimize [on|off] - Enable/disable automatic client minimization.')
windower_add_to_chat(207, ' //blackout minimizetimeout [seconds] - Set/view the minimize timeout (default 600).')
windower_add_to_chat(207, ' //blackout combat [on|off] - Enable/disable screensaver in combat.')
windower_add_to_chat(207, ' //blackout dead [on|off] - Enable/disable screensaver when dead.')
windower_add_to_chat(207, ' //blackout fps [on|off] - Enable/disable FPS display toggle on screen save.')
windower_add_to_chat(207, ' //blackout status - Show current settings and status.')
windower_add_to_chat(207, ' //blackout help - Display this help menu.')
end
windower.register_event('addon command', function(cmd, ...)
cmd = cmd and cmd:lower() or ''
local args = {...}
if cmd == 'on' then
show_overlay()
elseif cmd == 'off' then
hide_overlay(true)
elseif cmd == 'auto' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.auto_enabled = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Auto screensaver is now ENABLED.')
elseif sub_cmd == 'off' then
settings.auto_enabled = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Auto screensaver is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Auto screensaver is currently ' .. (settings.auto_enabled and 'ENABLED' or 'DISABLED') .. '. Use "//blackout auto on" or "//blackout auto off" to change.')
end
elseif cmd == 'timeout' then
local new_timeout = tonumber(args[1])
if new_timeout and new_timeout > 0 then
settings.idle_timeout = new_timeout
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle timeout set to ' .. new_timeout .. ' seconds.')
else
windower_add_to_chat(207, '[Blackout] Current idle timeout is ' .. settings.idle_timeout .. ' seconds.')
end
elseif cmd == 'minimize' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.minimize_enabled = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle minimization is now ENABLED.')
elseif sub_cmd == 'off' then
settings.minimize_enabled = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Idle minimization is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Idle minimization is currently ' .. (settings.minimize_enabled and 'ENABLED' or 'DISABLED') .. '. Use "//blackout minimize on" or "//blackout minimize off" to change.')
end
elseif cmd == 'minimizetimeout' or cmd == 'minimize_timeout' then
local new_timeout = tonumber(args[1])
if new_timeout and new_timeout > 0 then
settings.minimize_timeout = new_timeout
config.save(settings)
windower_add_to_chat(207, '[Blackout] Minimize timeout set to ' .. new_timeout .. ' seconds.')
else
windower_add_to_chat(207, '[Blackout] Current minimize timeout is ' .. settings.minimize_timeout .. ' seconds.')
end
elseif cmd == 'combat' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.disable_in_combat = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable in combat is now ENABLED.')
elseif sub_cmd == 'off' then
settings.disable_in_combat = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable in combat is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Disable in combat is currently ' .. (settings.disable_in_combat and 'ENABLED' or 'DISABLED') .. '. Use "//blackout combat on" or "//blackout combat off" to change.')
end
elseif cmd == 'dead' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.disable_if_dead = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable if dead is now ENABLED.')
elseif sub_cmd == 'off' then
settings.disable_if_dead = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] Disable if dead is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] Disable if dead is currently ' .. (settings.disable_if_dead and 'ENABLED' or 'DISABLED') .. '. Use "//blackout dead on" or "//blackout dead off" to change.')
end
elseif cmd == 'fps' then
local sub_cmd = args[1] and args[1]:lower() or ''
if sub_cmd == 'on' then
settings.show_fps_on_off = true
config.save(settings)
windower_add_to_chat(207, '[Blackout] FPS toggle is now ENABLED.')
elseif sub_cmd == 'off' then
settings.show_fps_on_off = false
config.save(settings)
windower_add_to_chat(207, '[Blackout] FPS toggle is now DISABLED.')
else
windower_add_to_chat(207, '[Blackout] FPS toggle is currently ' .. (settings.show_fps_on_off and 'ENABLED' or 'DISABLED') .. '. Use "//blackout fps on" or "//blackout fps off" to change.')
end
elseif cmd == 'status' then
windower_add_to_chat(207, '[Blackout] Status - Auto: ' .. (settings.auto_enabled and 'ON' or 'OFF') .. ', Timeout: ' .. settings.idle_timeout .. 's, Minimize: ' .. (settings.minimize_enabled and 'ON' or 'OFF') .. ', Minimize Timeout: ' .. settings.minimize_timeout .. 's, Disable in Combat: ' .. tostring(settings.disable_in_combat) .. ', Disable if Dead: ' .. tostring(settings.disable_if_dead) .. ', FPS Toggle: ' .. tostring(settings.show_fps_on_off))
elseif cmd == 'help' or cmd == 'h' or cmd == '?' then
print_help()
elseif cmd == '' or cmd == 'toggle' then
-- Toggle manually
if overlay and overlay_visible then
hide_overlay(true)
else
show_overlay()
end
else
windower_add_to_chat(207, '[Blackout] Unknown command: "' .. cmd .. '"')
print_help()
end
end)
|
|