223 lines
7.5 KiB
Lua
223 lines
7.5 KiB
Lua
--- love_file_handler.lua — LÖVE2D file handler for logger.lua
|
|
--
|
|
-- Uses love.filesystem to write logs into LÖVE's save directory, which works
|
|
-- correctly on every platform LÖVE supports (Windows, macOS, Linux, Android).
|
|
--
|
|
-- Requires:
|
|
-- logger.lua in the same directory (or on package.path)
|
|
-- LÖVE2D 11.x+ (love.filesystem must be available)
|
|
--
|
|
-- Usage inside main.lua / conf.lua:
|
|
--
|
|
-- local Logger = require("logger")
|
|
-- local LoveHandler = require("love_file_handler")
|
|
--
|
|
-- local log = Logger.new("game", {
|
|
-- handlers = {
|
|
-- Logger.handlers.ConsoleHandler(), -- print() fallback
|
|
-- LoveHandler.new("logs/game.log"), -- love.filesystem
|
|
-- },
|
|
-- })
|
|
--
|
|
-- function love.load()
|
|
-- log:info("Game started — LÖVE %s", love.getVersion())
|
|
-- end
|
|
|
|
-- ── Sanity check ──────────────────────────────────────────────────────────
|
|
|
|
assert(love and love.filesystem,
|
|
"love_file_handler requires the LÖVE2D runtime (love.filesystem)")
|
|
|
|
-- ── Module table ──────────────────────────────────────────────────────────
|
|
|
|
local LoveHandler = {}
|
|
|
|
-- ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
--- Ensure every directory component in `path` exists.
|
|
-- love.filesystem.createDirectory is recursive on LÖVE 11+.
|
|
local function ensure_dir(path)
|
|
local dir = path:match("^(.*)/[^/]+$")
|
|
if dir and dir ~= "" then
|
|
love.filesystem.createDirectory(dir)
|
|
end
|
|
end
|
|
|
|
--- Return current file size in bytes, or 0 on error / missing file.
|
|
local function file_size(path)
|
|
local info = love.filesystem.getInfo(path)
|
|
return info and info.size or 0
|
|
end
|
|
|
|
--- Shift rotated files: .4→.5, .3→.4, …, base→.1
|
|
local function rotate(path, max_files)
|
|
for i = max_files - 1, 1, -1 do
|
|
local src = path .. "." .. i
|
|
local dest = path .. "." .. (i + 1)
|
|
if love.filesystem.getInfo(src) then
|
|
-- love.filesystem has no rename; copy + delete
|
|
local data = love.filesystem.read(src)
|
|
if data then
|
|
love.filesystem.write(dest, data)
|
|
love.filesystem.remove(src)
|
|
end
|
|
end
|
|
end
|
|
-- move base → .1
|
|
if love.filesystem.getInfo(path) then
|
|
local data = love.filesystem.read(path)
|
|
if data then
|
|
love.filesystem.write(path .. ".1", data)
|
|
love.filesystem.remove(path)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ── Constructor ───────────────────────────────────────────────────────────
|
|
|
|
--- Create a new LÖVE2D file handler.
|
|
--
|
|
-- @param path string Path inside the LÖVE save directory.
|
|
-- Subdirectories are created automatically.
|
|
-- Default: "logs/game.log"
|
|
--
|
|
-- @param opts table Optional settings:
|
|
-- max_size (number) Rotate when the log file reaches this many bytes.
|
|
-- Default: 1 MB (1 048 576). Set to 0 to disable.
|
|
-- max_files (number) How many rotated files to keep. Default: 5.
|
|
-- buffered (bool) Accumulate lines in memory and flush in batches.
|
|
-- Reduces I/O overhead on mobile / web exports.
|
|
-- Default: false.
|
|
-- flush_size (number) Lines to buffer before auto-flushing (buffered mode).
|
|
-- Default: 20.
|
|
-- on_error (func) Called with (err_string) on any I/O error.
|
|
-- Default: prints a warning to love.graphics console.
|
|
--
|
|
-- @return handler, control_table
|
|
-- handler — function accepted by Logger:add_handler()
|
|
-- control — table with :flush(), :close(), :path(), :size() methods
|
|
function LoveHandler.new(path, opts)
|
|
path = path or "logs/game.log"
|
|
opts = opts or {}
|
|
|
|
local max_size = (opts.max_size == nil) and (1024 * 1024) or opts.max_size
|
|
local max_files = opts.max_files or 5
|
|
local buffered = opts.buffered or false
|
|
local flush_size = opts.flush_size or 20
|
|
local on_error = opts.on_error or function(err)
|
|
if love.graphics and love.graphics.print then
|
|
print("[love_file_handler] I/O error: " .. tostring(err))
|
|
end
|
|
end
|
|
|
|
-- State
|
|
local buffer = {} -- pending lines (buffered mode)
|
|
local closed = false
|
|
|
|
ensure_dir(path)
|
|
|
|
-- ── Internal write ────────────────────────────────────────────────────
|
|
|
|
local function raw_write(lines)
|
|
if #lines == 0 then return end
|
|
local text = table.concat(lines, "\n") .. "\n"
|
|
|
|
-- Rotate if needed
|
|
if max_size and max_size > 0 and file_size(path) + #text >= max_size then
|
|
rotate(path, max_files)
|
|
end
|
|
|
|
-- Append
|
|
local ok, err = love.filesystem.append(path, text)
|
|
if not ok then
|
|
on_error(tostring(err))
|
|
end
|
|
end
|
|
|
|
-- ── Public control table ──────────────────────────────────────────────
|
|
|
|
local control = {}
|
|
|
|
--- Flush buffered lines to disk immediately.
|
|
function control:flush()
|
|
if #buffer > 0 then
|
|
raw_write(buffer)
|
|
buffer = {}
|
|
end
|
|
end
|
|
|
|
--- Close the handler (flushes first). After close() the handler is a no-op.
|
|
function control:close()
|
|
self:flush()
|
|
closed = true
|
|
end
|
|
|
|
--- Full path as seen by love.filesystem.
|
|
function control:path()
|
|
return path
|
|
end
|
|
|
|
--- Real filesystem path to the save directory file (for external tools).
|
|
function control:real_path()
|
|
local save_dir = love.filesystem.getSaveDirectory()
|
|
return save_dir .. "/" .. path
|
|
end
|
|
|
|
--- Current log file size in bytes.
|
|
function control:size()
|
|
return file_size(path)
|
|
end
|
|
|
|
--- List rotated files that exist (e.g. {"logs/game.log.1", ...})
|
|
function control:rotated_files()
|
|
local result = {}
|
|
for i = 1, max_files do
|
|
local p = path .. "." .. i
|
|
if love.filesystem.getInfo(p) then
|
|
table.insert(result, p)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
--- Read the entire current log file as a string (nil on error).
|
|
function control:read()
|
|
self:flush()
|
|
return love.filesystem.read(path)
|
|
end
|
|
|
|
-- ── Handler function ──────────────────────────────────────────────────
|
|
|
|
local function handler(record)
|
|
if closed then return end
|
|
|
|
if buffered then
|
|
table.insert(buffer, record.formatted)
|
|
if #buffer >= flush_size then
|
|
raw_write(buffer)
|
|
buffer = {}
|
|
end
|
|
else
|
|
raw_write({ record.formatted })
|
|
end
|
|
end
|
|
|
|
return handler, control
|
|
end
|
|
|
|
-- ── Convenience: hook love.quit to flush ──────────────────────────────────
|
|
|
|
--- Wrap love.quit so a control table's flush() is called automatically.
|
|
-- Call once per handler after Logger.new(); safe to call multiple times.
|
|
--
|
|
-- @param control The control table returned by LoveHandler.new()
|
|
function LoveHandler.auto_flush_on_quit(control)
|
|
local original_quit = love.quit or function() end
|
|
love.quit = function()
|
|
control:flush()
|
|
return original_quit()
|
|
end
|
|
end
|
|
|
|
return LoveHandler
|