-- KeepItemsOnDeath
-- Keep 100% of your inventory when you die. No corpse run.
-- Adds a "Keep Items On Death" checkbox to Settings > Gameplay.
--
-- Built against Dragonwilds ++dominion+live-CL-232224 (UE 5.6.1).
--
-- WHY v1.1 EXISTS. On CL-232224 the mod loaded, drew its settings row, and did nothing else.
-- The tempting conclusion was "the patch renamed things" -- it had not. Every symbol still
-- resolved against the new index. The real fault was that the two events it hung everything on
-- were never the right events, and had only ever worked by luck:
--
--   * NotifyOnNewObject on SurvivalDifficultyComponent -- that component lives on the GameState
--     as a default subobject, so it is not announced like a normally-spawned object.
--   * A hook on PlayerRespawnComponent:Multicast_Respawn -- the death flow goes through the
--     health component's revive path instead. Two plausible names; the wrong one was picked.
--
-- Neither failure produces an error, a warning, or a log line. That is the whole hazard of this
-- API. The fix is not a better guess at the magic moment: the setting is now re-asserted on a
-- timer, so no single event has to be caught, and death is detected by the gravestone actor
-- being constructed -- a direct causal signal rather than a name that sounds right.
--
-- EVERY game symbol below came out of Find-DwApi.ps1 against dumps\CL-230700. Nothing is
-- recalled. The load-bearing ones:
--   DA_DifficultySetting_KeepInventoryOnDeath   ObjectPath, objects\UE4SS_ObjectDump.txt
--   UDifficultySettingData.SettingTag           Property,   types\Dominion.lua:19626
--   UDifficultySettingData.PresetDefaults       Property,   types\Dominion.lua:19633
--   ADominionGameStateBase:GetSurvivalDifficultyComponent   types\Dominion.lua:1777
--   USurvivalDifficultyComponent.ReplicatedDifficultySettings  types\Dominion.lua:38934
--   FDifficultySettingEntry{SettingTag,SettingValue}           types\Dominion.lua:7577
--   /Script/Dominion.PlayerRespawnComponent:Multicast_Respawn  objects\UE4SS_ObjectDump.txt:63465
--   AGravestone:GetInventory / GetOwningCharacterGuid          types\Dominion.lua:3373 / 3371
--   ADominionPlayerControllerBase:IsCharacterGuidEqual         types\Dominion.lua:2646
--   ADominionPlayerController.InventoryComponent               types\Dominion.lua:2372
--   UInventoryComponent:MoveAllItems            Function,   types\Dominion.lua
--   USettingsGameplayWidget + its UCheckboxUIInput fields      types\Dominion.lua:36039
--   ULabeledWidget.LabelText                    Property,   types\Dominion.lua:28275
--   UCheckboxUIInput:SetValue                   Function,   types\Dominion.lua:17517
--   UWidgetBlueprintLibrary:Create              Function,   types\UMG.lua:3194
--   UWidget:GetParent / UPanelWidget:AddChild   types\UMG.lua:2835 / 1413
--
-- THE FINDING THAT SHAPED THIS MOD: the game already implements keep-inventory-on-death
-- natively, as a difficulty setting. It is not something this mod invents. But
-- UDifficultySettingData.PlayerAdjustable is an EWorldSettingUserAdjustability
-- (Never / OnlyCustom / CustomAndCreative / AllModes), so on a Normal or Hard world the toggle
-- need not be offered in the UI at all. That gap is what this mod fills.
--
-- VERIFIED IN GAME 2026-08-02: layer 1 works. Died with a full inventory, kept everything, and
-- no gravestone was created at all.
--
--   LAYER 1 (primary, confirmed working) - flip the game's own KeepInventoryOnDeath setting.
--     Which store the native code reads is not determinable from reflection, so this writes
--     both the asset's PresetDefaults and the live GameState entry array.
--
--   LAYER 2 (safety net) - empty any pre-existing gravestone back into you. Only matters for
--     graves left before the mod was installed, or if layer 1 ever stops taking.

local UEHelpers = require("UEHelpers")

local MOD_NAME = "KeepItemsOnDeath"

-- Exact object paths, from the object dump. A typo returns an invalid object rather than an
-- error, so Test-DwMod.ps1 verifies each against the index before deploy.
local KEEP_INV_ASSET =
    "/Game/Gameplay/DifficultySettings/Player/DA_DifficultySetting_KeepInventoryOnDeath.DA_DifficultySetting_KeepInventoryOnDeath"
local CHECKBOX_CLASS = "/Game/UI/Settings/WBP_Settings_Checkbox.WBP_Settings_Checkbox_C"
local WIDGET_LIB     = "/Script/UMG.Default__WidgetBlueprintLibrary"

-- The checkbox's component-bound state-changed event. Confirmed in the object dump at
-- objects\UE4SS_ObjectDump.txt:191642, with a single bIsChecked BoolProperty param.
local CHECKBOX_EVENT = CHECKBOX_CLASS ..
    ":BndEvt__WBP_Settings_Checkbox_InputCheckBox_K2Node_ComponentBoundEvent_0_OnCheckBoxComponentStateChanged__DelegateSignature"

-- ESurvivalDifficulty, read out of the object dump (not guessed):
--   Normal 0, Hard 1, Creative 2, Custom 3
local DIFFICULTIES = { Normal = 0, Hard = 1, Creative = 2, Custom = 3 }

-- Gravestones spawn a moment after death, and the respawned pawn has to exist before items can
-- move. Respawn fires before both settle, so the sweep is retried on a short ladder.
local SWEEP_DELAYS_MS = { 1000, 3000, 6000, 10000 }

local function log(fmt, ...)
    -- The trailing \n is required; UE4SS does not add one.
    print(("[%s] " .. fmt .. "\n"):format(MOD_NAME, ...))
end

-- Run a top-level registration so that its failure cannot take the rest of the mod with it.
--
-- This is the single most important line in the file. A raised error at the top level aborts the
-- whole main chunk, and UE4SS reports it only as "Failed to execute main script" -- every
-- registration below the failure point silently never happens. That produced a mod which loaded,
-- drew its settings row, and did nothing else, for two debugging sessions. Register defensively.
local function safe(label, fn)
    local ok, err = pcall(fn)
    if not ok then log("registration failed [%s]: %s", label, tostring(err)) end
    return ok
end

---------------------------------------------------------------------------------------------
-- Persisted on/off state
---------------------------------------------------------------------------------------------

local enabled = true          -- default on; that is the point of installing the mod
local originalPresets = nil   -- captured before the first write, so "off" can restore honestly
local ourCheckbox = nil       -- the checkbox instance this mod added

-- Forward-declared: defined in the UI section, but called from addCheckbox above it. Lua locals
-- are only visible after their declaration, so without this the call would resolve to a nil
-- global and error at runtime.
local ensureCheckboxHook

-- Resolve a path next to this script rather than guessing the process working directory, which
-- for UE4SS is the game binary folder and not the mod folder.
local function stateFile()
    local src = debug.getinfo(1, "S").source
    local dir = tostring(src):match("^@?(.*[\\/])") or ""
    return dir .. "enabled.state"
end

local function loadState()
    local ok = pcall(function()
        local f = io.open(stateFile(), "r")
        if not f then return end
        local v = f:read("*l")
        f:close()
        if v == "0" then enabled = false end
    end)
    if not ok then log("could not read saved state; defaulting to on") end
end

local function saveState()
    local ok = pcall(function()
        local f = io.open(stateFile(), "w")
        if not f then return end
        f:write(enabled and "1" or "0")
        f:close()
    end)
    if not ok then log("could not persist state") end
end

---------------------------------------------------------------------------------------------
-- Layer 1: the game's own difficulty setting
---------------------------------------------------------------------------------------------

-- Read the gameplay tag off the data asset rather than hardcoding a tag string. The tag's text
-- is not in the reflection dump (only the property that holds it), so a hardcoded tag would be
-- exactly the kind of invented symbol that fails silently.
local function getKeepInventoryAsset()
    local asset = StaticFindObject(KEEP_INV_ASSET)
    if not asset:IsValid() then return nil, nil, "asset not loaded" end

    local ok, tag = pcall(function() return asset.SettingTag end)
    if not ok or tag == nil then return nil, nil, "SettingTag unreadable" end
    return asset, tag, nil
end

-- Capture the shipped PresetDefaults once, before anything is overwritten. Without this,
-- turning the toggle off could only guess at what the original values were.
local function captureOriginals(asset)
    if originalPresets ~= nil then return end
    local captured = {}
    local ok = pcall(function()
        local presets = asset.PresetDefaults
        if presets == nil then error("PresetDefaults nil") end
        for _, value in pairs(DIFFICULTIES) do
            captured[value] = presets[value] or 0.0
        end
    end)
    if ok then
        originalPresets = captured
        log("captured stock PresetDefaults")
    end
end

local function applyPresetDefaults(asset, on)
    local wrote = false
    for name, difficulty in pairs(DIFFICULTIES) do
        local target = 1.0
        if not on then
            target = (originalPresets and originalPresets[difficulty]) or 0.0
        end
        local ok, err = pcall(function() asset.PresetDefaults[difficulty] = target end)
        if ok then
            wrote = true
        else
            log("PresetDefaults[%s] write failed: %s", name, tostring(err))
        end
    end
    return wrote
end

local function applyReplicatedEntry(tag, on)
    local gs = UEHelpers.GetGameStateBase()
    if not gs:IsValid() then return false, "no GameState" end

    return pcall(function()
        local comp = gs:GetSurvivalDifficultyComponent()
        if not comp:IsValid() then error("no SurvivalDifficultyComponent") end

        local entries = comp.ReplicatedDifficultySettings.CustomDifficultySettings
        if entries == nil then error("CustomDifficultySettings nil") end

        -- Arrays here are 1-based. Find the entry for our tag and flip it in place.
        local count = #entries
        for i = 1, count do
            local entry = entries[i]
            if entry.SettingTag == tag then
                entry.SettingValue = on and 1.0 or 0.0
                return ("updated entry %d/%d"):format(i, count)
            end
        end
        return ("no entry for this tag (array holds %d); PresetDefaults carries it"):format(count)
    end)
end

-- Read the value actually in effect, so success is observed rather than assumed. Returns nil
-- when the asset is not loaded yet.
local function readCurrentValue()
    local asset = StaticFindObject(KEEP_INV_ASSET)
    if not asset:IsValid() then return nil end

    local ok, v = pcall(function() return asset.PresetDefaults[DIFFICULTIES.Normal] end)
    if not ok then return nil end
    return v
end

local function applySetting(quiet)
    local asset, tag, err = getKeepInventoryAsset()
    if asset == nil then
        if not quiet then log("cannot read the KeepInventoryOnDeath asset (%s)", tostring(err)) end
        return false
    end

    captureOriginals(asset)

    local presetsOk = applyPresetDefaults(asset, enabled)
    local entryOk, result = applyReplicatedEntry(tag, enabled)

    -- Read back rather than trusting the write. A silently-discarded property write is the
    -- failure mode this whole toolchain exists to catch, and the earlier version of this mod
    -- had no way to tell "applied" from "never ran".
    local nowValue = readCurrentValue()

    log("setting -> %s | readback=%s (presets:%s, entry:%s)",
        enabled and "KEEP ITEMS" or "stock behaviour",
        tostring(nowValue), tostring(presetsOk), tostring(result))

    return presetsOk or entryOk
end

-- Re-assert the setting if it is not currently what we want.
--
-- This exists because the previous version hung the whole mod off two one-shot events that
-- never fired: NotifyOnNewObject on the difficulty component (it lives on the GameState as a
-- default subobject, so it is not caught like a normally-spawned object) and a hook on
-- PlayerRespawnComponent:Multicast_Respawn (the death flow actually goes through the health
-- component's revive path). Both symbols resolve perfectly against the index -- they were simply
-- never the moments this mod needed. Re-asserting on a timer removes the dependence on catching
-- any single moment at all.
local lastState = nil
local function ensureApplied()
    local current = readCurrentValue()
    if current == nil then return end                 -- world/asset not up yet

    local want = enabled and 1.0 or ((originalPresets and originalPresets[DIFFICULTIES.Normal]) or 0.0)
    if current == want then
        lastState = current
        return
    end

    -- Only speaks up when it actually changes something, so the log does not fill with ticks.
    if lastState ~= current then
        log("setting drifted (found %s, want %s) - reapplying", tostring(current), tostring(want))
    end
    applySetting(true)
    lastState = readCurrentValue()
end

---------------------------------------------------------------------------------------------
-- Layer 2: recover from your own gravestone
---------------------------------------------------------------------------------------------

local function getPlayerInventory()
    local pc = UEHelpers.GetPlayerController()
    if not pc:IsValid() then return nil, "no PlayerController" end

    local ok, inv = pcall(function() return pc.InventoryComponent end)
    if not ok or inv == nil or not inv:IsValid() then
        return nil, "no InventoryComponent on the PlayerController"
    end
    return inv, pc
end

-- Is this gravestone ours?
--
-- CanPlayerAccessGravestone is the obvious check, but it is declared BlueprintAuthorityOnly --
-- so it returns false whenever it is called without authority, which is why the first version
-- of this mod silently skipped every grave. IsCharacterGuidEqual lives on
-- ADominionPlayerControllerBase with no authority gate, so it is the reliable test; the access
-- check is kept only as a secondary yes.
local function isOwnGravestone(stone, pc)
    local ok, mine = pcall(function()
        return pc:IsCharacterGuidEqual(stone:GetOwningCharacterGuid())
    end)
    if ok and mine then return true end

    local ok2, allowed = pcall(function() return stone:CanPlayerAccessGravestone(pc) end)
    return ok2 and allowed or false
end

-- Returns the number of gravestones emptied.
local function sweepGravestones(reason)
    if not enabled then return 0 end

    local inv, pcOrErr = getPlayerInventory()
    if inv == nil then
        log("sweep (%s): %s", reason, tostring(pcOrErr))
        return 0
    end
    local pc = pcOrErr

    -- FindAllOf returns nil, not an empty table, when nothing matches. The unprefixed class name
    -- is the one that works at runtime -- the object dump lists /Script/Dominion.Gravestone.
    local stones = FindAllOf("Gravestone")
    if stones == nil then return 0 end

    local emptied, moved, skipped = 0, 0, 0

    for _, stone in ipairs(stones) do
        -- pcall per stone: one bad handle must not abandon the rest.
        local ok, err = pcall(function()
            if not stone:IsValid() then return end

            -- Never touch a grave that is not ours, or the sweep would loot co-op partners.
            if not isOwnGravestone(stone, pc) then skipped = skipped + 1 return end

            local grave = stone:GetInventory()
            if not grave:IsValid() then return end

            local before = grave:GetNumFilledSlots()
            if before == 0 then return end

            -- The game's own transfer function, so stack counts, durability and gameplay tags
            -- survive exactly as they were.
            grave:MoveAllItems(inv)

            local after = grave:GetNumFilledSlots()
            moved   = moved + (before - after)
            emptied = emptied + 1

            if after > 0 then
                log("recovered %d of %d stacks - inventory full, %d left in the grave",
                    before - after, before, after)
            end
        end)
        if not ok then log("gravestone access failed: %s", tostring(err)) end
    end

    if moved > 0 then
        log("sweep (%s): recovered %d stack(s) from %d gravestone(s)", reason, moved, emptied)
    elseif skipped > 0 then
        log("sweep (%s): %d gravestone(s) found, none of them yours", reason, skipped)
    end
    return emptied
end

-- Death, gravestone spawn and respawn are not simultaneous. A short ladder of delayed sweeps
-- covers the ordering without polling forever.
local function scheduleSweeps(reason)
    if not enabled then return end
    for _, ms in ipairs(SWEEP_DELAYS_MS) do
        ExecuteWithDelay(ms, function()
            -- ExecuteWithDelay runs off the game thread. Every UObject touch below has to be
            -- wrapped, or the client takes a minidump rather than raising a Lua error.
            ExecuteInGameThread(function()
                local ok, err = pcall(sweepGravestones, reason)
                if not ok then log("sweep error: %s", tostring(err)) end
            end)
        end)
    end
end

---------------------------------------------------------------------------------------------
-- The Settings > Gameplay checkbox
---------------------------------------------------------------------------------------------

local function setToggle(on, source)
    enabled = on and true or false
    saveState()
    log("toggled %s via %s", enabled and "ON" or "OFF", source)

    local ok, err = pcall(applySetting)
    if not ok then log("apply error: %s", tostring(err)) end

    if enabled then sweepGravestones("toggle on") end
end

-- Build a checkbox row and drop it into the same panel as an existing one, so it inherits the
-- page's layout and styling rather than needing any of its own.
--
-- The anchor supplies the parent panel only -- it does NOT control placement. UPanelWidget
-- exposes AddChild but no InsertChildAt or ShiftChild (verified against types\UMG.lua), so the
-- row always appends and lands at the BOTTOM of the page, under "AutoRun Enabled". Confirmed
-- in game 2026-08-02. Moving it would mean detaching and re-adding every row below the anchor
-- while preserving each one's slot padding; deliberately not done for a cosmetic gain.
local function addCheckbox(page)
    local anchor = page.GravestoneProtectionCheckbox
    if anchor == nil or not anchor:IsValid() then
        anchor = page.HideHelmetCheckbox
    end
    if anchor == nil or not anchor:IsValid() then
        -- Not an error on its own: the page's bound widgets are not populated the instant it is
        -- constructed, and the main menu's settings page does not carry the same rows as the
        -- in-world one. The caller retries.
        return false
    end

    local panel = anchor:GetParent()
    if not panel:IsValid() then
        return false
    end

    local cls = StaticFindObject(CHECKBOX_CLASS)
    if not cls:IsValid() then
        log("UI: checkbox widget class not loaded")
        return false
    end

    local lib = StaticFindObject(WIDGET_LIB)
    if not lib:IsValid() then
        log("UI: WidgetBlueprintLibrary CDO not found")
        return false
    end

    local box = lib:Create(UEHelpers.GetWorld(), cls, UEHelpers.GetPlayerController())
    if box == nil or not box:IsValid() then
        log("UI: checkbox creation failed")
        return false
    end

    -- LabelText comes from ULabeledWidget, the shared base of every settings row.
    pcall(function() box.LabelText = FText("Keep Items On Death") end)

    panel:AddChild(box)
    pcall(function() box:SetValue(enabled) end)

    ourCheckbox = box
    log("UI: added the Keep Items On Death row to Settings > Gameplay")

    -- Safe to hook now: building the widget proves its Blueprint class is loaded.
    ensureCheckboxHook()
    return true
end

-- Registering on the C++ base rather than the concrete WBP_ class: the same lesson as the
-- crafting popup, where six derived widgets were live and targeting one caught only that one.
safe("settings page notify", function()
NotifyOnNewObject("/Script/Dominion.SettingsGameplayWidget", function(page)
    -- The page's bound widgets are not populated the instant it is constructed, so a single
    -- attempt races the page and sometimes finds no anchor row. Retry on a short ladder and
    -- stop at the first success.
    local done = false
    for _, ms in ipairs({ 200, 700, 1500, 3000 }) do
        ExecuteWithDelay(ms, function()
            ExecuteInGameThread(function()
                if done then return end
                local ok, res = pcall(addCheckbox, page)
                if not ok then
                    log("UI error: %s", tostring(res))
                    done = true
                elseif res then
                    done = true
                end
            end)
        end)
    end
end)
end)

-- Every settings checkbox on the page routes through this one bound event, so the handler has
-- to confirm the instance is ours before acting -- otherwise toggling "Hide Helmet" would
-- flip this mod.
--
-- THIS REGISTRATION MUST BE LAZY AND MUST BE GUARDED, and getting that wrong cost a lot of time.
-- WBP_Settings_Checkbox_C is a *Blueprint* class: its asset is not loaded at mod-load time (it
-- loads when the settings screen is first opened), so RegisterHook cannot resolve the function
-- and raises a HARD error. At top level that error aborts the entire main chunk, silently
-- killing every registration below it -- which is exactly what happened: the gravestone trigger,
-- the re-assert loop and the world-ready hook were all dead code, and the only symptom was a
-- mod that drew its UI and did nothing else.
--
-- So: register it only once the class is definitely loaded (i.e. after we have built a checkbox
-- from it), only once, and never let a failure escape.
local checkboxHookRegistered = false

ensureCheckboxHook = function()
    if checkboxHookRegistered then return end

    local ok, err = pcall(function()
        RegisterHook(CHECKBOX_EVENT, function(Context, bIsChecked)
            if ourCheckbox == nil then return end

            -- Callback parameters arrive wrapped as RemoteUnrealParam; :get() unwraps them.
            local ok2 = pcall(function()
                local self = Context:get()
                if not self:IsValid() or not ourCheckbox:IsValid() then return end
                if self:GetFullName() ~= ourCheckbox:GetFullName() then return end
                setToggle(bIsChecked:get(), "settings checkbox")
            end)
            if not ok2 then log("checkbox handler error") end
        end)
    end)

    if ok then
        checkboxHookRegistered = true
        log("checkbox hook registered")
    else
        -- Not fatal. The row still displays; it just will not report clicks, and the console
        -- command and keybind remain as controls.
        log("could not hook the checkbox (%s) - use dw_keepitems or Ctrl+Shift+F6", tostring(err))
        checkboxHookRegistered = true      -- do not retry every time the page opens
    end
end

---------------------------------------------------------------------------------------------
-- Wiring
---------------------------------------------------------------------------------------------

-- A gravestone being constructed IS the death event. This is a direct causal signal rather than
-- a guess at which of several respawn/revive functions the flow happens to use, and it is what
-- the previous version got wrong. Registered on the C++ base so any Blueprint subclass counts --
-- the live actor is ABP_PlayerGravestone_C.
safe("gravestone notify", function()
    NotifyOnNewObject("/Script/Dominion.Gravestone", function()
        log("gravestone appeared - you died")
        local ok, err = pcall(applySetting)
        if not ok then log("apply error: %s", tostring(err)) end
        scheduleSweeps("gravestone spawned")
    end)
end)

-- Both revive and respawn are hooked, because which one a given death uses is not something
-- reflection can answer. They are cheap and idempotent; whichever fires, the setting is
-- re-asserted and a sweep is scheduled.
for _, target in ipairs({
    "/Script/Dominion.PlayerHealthComponent:Multicast_Revive",
    "/Script/Dominion.PlayerRespawnComponent:Multicast_Respawn",
}) do
    -- pcall: an unresolvable hook target should disable that one hook, not the whole mod.
    local ok = pcall(function()
        RegisterHook(target, function()
            log("respawn/revive detected")
            local ok2, err = pcall(applySetting)
            if not ok2 then log("apply error: %s", tostring(err)) end
            scheduleSweeps("respawn")
        end)
    end)
    if not ok then log("could not hook %s", target) end
end

-- World-ready. The player controller is the signal the loader documentation recommends, and it
-- is the one that demonstrably fires.
safe("world-ready notify", function()
    NotifyOnNewObject("/Script/Engine.PlayerController", function()
        ExecuteWithDelay(2000, function()
            ExecuteInGameThread(function()
                local ok, err = pcall(function() applySetting() end)
                if not ok then log("apply error: %s", tostring(err)) end
                scheduleSweeps("world load")
            end)
        end)
    end)
end)

-- The safety net that makes the rest of it forgiving: re-assert every few seconds. Costs a
-- single float read when nothing is wrong.
safe("re-assert loop", function()
    LoopAsync(5000, function()
        -- LoopAsync runs OFF the game thread; every UObject touch has to be wrapped or the
        -- client takes a minidump instead of raising a Lua error.
        ExecuteInGameThread(function()
            pcall(ensureApplied)
        end)
        return false        -- keep looping
    end)
end)

---------------------------------------------------------------------------------------------
-- Console command and keybind
---------------------------------------------------------------------------------------------

local function handleCommand(_, parameters)
    local arg = parameters and parameters[1] and tostring(parameters[1]):lower() or nil

    if arg == "on" or arg == "off" then
        setToggle(arg == "on", "console")
    elseif arg == "status" then
        log("currently %s", enabled and "ON" or "OFF")
    else
        -- No argument: recover whatever is recoverable right now.
        log("manual recovery requested (currently %s)", enabled and "ON" or "OFF")
        local ok, err = pcall(applySetting)
        if not ok then log("apply error: %s", tostring(err)) end
        if sweepGravestones("manual") == 0 then
            log("no gravestones of yours found with items in them")
        end
    end

    -- Keep the settings checkbox in step when the change came from elsewhere.
    if ourCheckbox ~= nil then
        pcall(function()
            if ourCheckbox:IsValid() then ourCheckbox:SetValue(enabled) end
        end)
    end
    return true
end

-- Global handler so this works from the game console (Tilde / F10) as well as UE4SS's own.
safe("console command", function()
    if type(RegisterConsoleCommandGlobalHandler) == "function" then
        RegisterConsoleCommandGlobalHandler("dw_keepitems", handleCommand)
    else
        RegisterConsoleCommandHandler("dw_keepitems", handleCommand)
    end
end)

-- Ctrl+Shift+F6 flips the toggle. F-key rather than numpad, for keyboards without one. F10 is
-- the game console; F9 is DwDumper, F8 is DwSkillReadout and F7 is FastCrafts.
safe("keybind", function()
    RegisterKeyBind(Key.F6, { ModifierKey.CONTROL, ModifierKey.SHIFT }, function()
        setToggle(not enabled, "keybind")
        if ourCheckbox ~= nil then
            pcall(function()
                if ourCheckbox:IsValid() then ourCheckbox:SetValue(enabled) end
            end)
        end
    end)
end)

loadState()

-- Reaching this line is itself the diagnostic: if it is absent from the log, the main chunk
-- aborted somewhere above and an unknown amount of the mod never registered.
log("loaded OK, currently %s.", enabled and "ON" or "OFF")
log("Toggle in Settings > Gameplay, with 'dw_keepitems on|off|status', or Ctrl+Shift+F6.")
