[Blackout.Lua] ScreenSaver/OLED Protection

Eorzea Time
 
 
 
言語: JP EN FR DE
日本語版のFFXIVPRO利用したい場合は、上記の"JP"を設定して、又はjp.ffxivpro.comを直接に利用してもいいです
4493 users online
フォーラム » Windower » General » [Blackout.Lua] ScreenSaver/OLED protection
[Blackout.Lua] ScreenSaver/OLED protection
 Fenrir.Jinxs
Online
サーバ: Fenrir
Game: FFXI
User: Jinxs
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
Offline
サーバ: Valefor
Game: FFXI
User: Keyser
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
Offline
サーバ: Valefor
Game: FFXI
User: Keyser
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.
Online
Posts: 8
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
Online
サーバ: Fenrir
Game: FFXI
User: Jinxs
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)