-- history.lua — unbounded console history with a review cursor. -- -- The game's own console buffer (modules/console.lua) is unusable as history: its -- entries expire after 3 seconds and scroll away. We keep our own copy, appended -- from a wrapper around console.print. -- -- Coalescing mirrors the game exactly. console.print folds a repeated identical -- message into the previous entry and bumps a `times` counter, drawing it as -- "You feel cold (x3)". We do the same and speak the same string, so what is -- heard matches what is on screen. local speech = require("accessibility.speech") local history = {} local MAX_ENTRIES = 1000 local entries = {} local reviewIndex = nil -- nil = following the tail (live) -------------------------------------------------------------------------------- -- Rendering an entry -------------------------------------------------------------------------------- local function entryText(entry) if not entry then return nil end if entry.times and entry.times > 1 then return entry.text .. " (x" .. entry.times .. ")" end return entry.text end history.entryText = entryText -------------------------------------------------------------------------------- -- Appending -------------------------------------------------------------------------------- -- Called from the console.print wrapper. Returns the text that should be spoken, -- or nil if nothing should be. function history.append(text, colorTemplate) text = speech.normalise(text) if not text then return nil end local last = entries[#entries] if last and last.text == text then -- Same fold the game performs. last.times = last.times + 1 return entryText(last) end entries[#entries + 1] = { text = text, times = 1, colorTemplate = colorTemplate, } -- Trim from the front. Keep the review cursor pointing at the same entry. if #entries > MAX_ENTRIES then table.remove(entries, 1) if reviewIndex then reviewIndex = math.max(1, reviewIndex - 1) end end return entryText(entries[#entries]) end function history.count() return #entries end function history.clear() entries = {} reviewIndex = nil end -------------------------------------------------------------------------------- -- Review cursor -------------------------------------------------------------------------------- -- While reviewing, the cursor holds still as new lines arrive; it only follows the -- tail when the user has not moved it. Otherwise reading back through a fight -- would be yanked forward by every new message. local function speakAt(index) if #entries == 0 then speech.interrupt("No messages") return end local entry = entries[index] if not entry then return end local position if index == 1 then position = "First message" elseif index == #entries then position = "Last message" end local text = entryText(entry) if position then -- Most game messages already end in punctuation; joining with ". " blindly -- gives "You hit the rat.. First message". local separator = text:match("[%.%!%?]$") and " " or ". " text = text .. separator .. position end speech.interrupt(text) end function history.reviewPrevious() if #entries == 0 then speech.interrupt("No messages") return end if reviewIndex == nil then -- Entering review from live: start at the newest, then step back one. reviewIndex = #entries end reviewIndex = math.max(1, reviewIndex - 1) speakAt(reviewIndex) end function history.reviewNext() if #entries == 0 then speech.interrupt("No messages") return end if reviewIndex == nil then speakAt(#entries) reviewIndex = #entries return end reviewIndex = math.min(#entries, reviewIndex + 1) speakAt(reviewIndex) end function history.reviewFirst() if #entries == 0 then speech.interrupt("No messages") return end reviewIndex = 1 speakAt(reviewIndex) end function history.reviewLast() if #entries == 0 then speech.interrupt("No messages") return end reviewIndex = nil -- back to following live output speakAt(#entries) end function history.repeatCurrent() if #entries == 0 then speech.interrupt("No messages") return end speakAt(reviewIndex or #entries) end function history.copyCurrent() if #entries == 0 then return end local text = entryText(entries[reviewIndex or #entries]) local ok = pcall(love.system.setClipboardText, text) speech.interrupt(ok and "Copied" or "Copy failed") end return history