194 lines
4.4 KiB
Lua
194 lines
4.4 KiB
Lua
-- 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
|