From fa00843e8fa5bddf9d2c70d277179ff676262f87 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sat, 23 Sep 2023 09:11:24 +1200 Subject: [PATCH 01/19] Fix separator to be a windows one for the user data folder --- src/win32/FileSystemWin32.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/win32/FileSystemWin32.cpp b/src/win32/FileSystemWin32.cpp index dba5345391c..6ec0f2e7058 100644 --- a/src/win32/FileSystemWin32.cpp +++ b/src/win32/FileSystemWin32.cpp @@ -40,7 +40,7 @@ namespace FileSystem { } std::wstring path(appdata_path); - path += L"/Pioneer"; + path += L"\\Pioneer"; if (!PathFileExistsW(path.c_str())) { if (SHCreateDirectoryExW(0, path.c_str(), 0) != ERROR_SUCCESS) { From 003f19b50cd3e1aa4d916eed2e396133ee9784e7 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sat, 23 Sep 2023 09:13:00 +1200 Subject: [PATCH 02/19] Allow Lua scripts to create folders and do io io operations in the user data foder Do not allow them to use popen --- src/lua/LuaFileSystem.cpp | 44 +++++++++++++++++++++++++++++++++++++++ src/lua/core/Sandbox.cpp | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/lua/LuaFileSystem.cpp b/src/lua/LuaFileSystem.cpp index af64ff9c178..3178ea3f062 100644 --- a/src/lua/LuaFileSystem.cpp +++ b/src/lua/LuaFileSystem.cpp @@ -151,6 +151,49 @@ static int l_filesystem_join_path(lua_State *l) } } + +/* + * Function: MakeUserDataDirectory + * + * > local path = FileSystem.MakeUserDataDirectory( dir_name ) + * + * Creating the given directory if it's missing, returning the name + * full name of the directory + * + * Availability: + * + * ???? + * + * Status: + * + * experimental + */ +static int l_filesystem_make_user_directory(lua_State* l) +{ + try { + std::string dir = luaL_checkstring(l, 1); + + FileSystem::userFiles.MakeDirectory(dir); + + FileSystem::FileInfo f = FileSystem::userFiles.Lookup(dir); + + if (f.IsDir()) + { + std::string fullDirName = f.GetAbsolutePath(); + lua_pushlstring(l, fullDirName.c_str(), fullDirName.size()); + return 1; + } else + { + lua_pushlstring(l, "", 1 ); + return 0; + } + } + catch (const std::invalid_argument&) { + luaL_error(l, "unable to create directory"); + return 0; + } +} + void LuaFileSystem::Register() { lua_State *l = Lua::manager->GetLuaState(); @@ -160,6 +203,7 @@ void LuaFileSystem::Register() static const luaL_Reg l_methods[] = { { "ReadDirectory", l_filesystem_read_dir }, { "JoinPath", l_filesystem_join_path }, + { "MakeUserDataDirectory", l_filesystem_make_user_directory }, { 0, 0 } }; diff --git a/src/lua/core/Sandbox.cpp b/src/lua/core/Sandbox.cpp index b84923a2b04..3d879f687ec 100644 --- a/src/lua/core/Sandbox.cpp +++ b/src/lua/core/Sandbox.cpp @@ -6,6 +6,7 @@ #include "LuaUtils.h" #include "core/Log.h" #include "utils.h" +#include "FileSystem.h" static int l_d_null_userdata(lua_State *L) { @@ -104,6 +105,25 @@ static int l_log_warning(lua_State *L) return 0; } +static lua_CFunction l_original_io_open = nullptr; + +static int l_patched_io_open(lua_State* L) +{ + std::string path = lua_tostring(L, 1); + path = FileSystem::NormalisePath(path); + + std::string userDir = FileSystem::GetUserDir(); + // TODO: should we add a file separator here? + + if (path.rfind(userDir, 0) == 0) + { + // path starts with the userDir, we're good to go + return l_original_io_open(L); + } + luaL_error(L, "attempt to access filesystem outside of the user data folder"); + return 0; +} + static const luaL_Reg STANDARD_LIBS[] = { { "_G", luaopen_base }, { LUA_COLIBNAME, luaopen_coroutine }, @@ -112,6 +132,7 @@ static const luaL_Reg STANDARD_LIBS[] = { { LUA_BITLIBNAME, luaopen_bit32 }, { LUA_MATHLIBNAME, luaopen_math }, { LUA_DBLIBNAME, luaopen_debug }, + { LUA_IOLIBNAME, luaopen_io }, { "util", luaopen_utils }, { "package", luaopen_import }, { 0, 0 } @@ -165,6 +186,26 @@ void pi_lua_open_standard_base(lua_State *L) lua_pushcfunction(L, l_log_verbose); lua_setglobal(L, "logVerbose"); + // IO library adjustments + lua_getglobal(L, LUA_IOLIBNAME); + + lua_getfield(L, -1, "open"); + assert(lua_iscfunction(L, -1)); + l_original_io_open = lua_tocfunction(L, -1); + + lua_pop(L, 1); // pop the io table + lua_getglobal(L, LUA_IOLIBNAME); + + // patch io.open so we can check the path + lua_pushcfunction(L, l_patched_io_open); + lua_setfield(L, -2, "open"); + + // remove io.popen as we don't want people running apps + lua_pushnil(L); + lua_setfield(L, -2, "popen"); + + lua_pop(L, 1); // pop the io table + // standard library adjustments (math library) lua_getglobal(L, LUA_MATHLIBNAME); From 8ef98353a697e2a78108575bcda1b29f5f37c0d6 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sat, 23 Sep 2023 09:14:20 +1200 Subject: [PATCH 03/19] Add an export tab to the lightlog, allowing users to write out various elements (or all of the ) into a flight log in their user data folder --- data/pigui/modules/info-view/06-flightlog.lua | 323 +++++++++++++++--- 1 file changed, 280 insertions(+), 43 deletions(-) diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index 81e83c4acbc..e8d99a3749f 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -8,6 +8,8 @@ local FlightLog = require 'FlightLog' local Format = require 'Format' local Color = _G.Color local Vector2 = _G.Vector2 +local FileSystem = require 'FileSystem' +local Character = require 'Character' local pionillium = ui.fonts.pionillium local icons = ui.theme.icons @@ -24,8 +26,131 @@ local function formatDate(date) return date and Format.Date(date) or nil end --- Title text is gray, followed by the variable text: -local function headerText(title, text, wrap) +-- Based on flight state, compose a reasonable string for location +local function composeLocationString(location) + return string.interp(l["FLIGHTLOG_"..location[1]], + { primary_info = location[2], + secondary_info = location[3] or "", + tertiary_info = location[4] or "",}) +end + + +function CustomEntryIterator() + local generator = FlightLog.GetCustomEntry() + + local function my_generator() + local systemp, time, money, location, entry = generator() + if systemp ~= nil then + local entry = { + type = "custom", + systemp = systemp, + time = time, + money = money, + location = location, + entry = entry, + sort_date = time + } + + function entry:write_header( formatter ) + formatter:write( l.LOG_CUSTOM ):write(":"):newline() + end + + function entry:write_details( formatter ) + formatter:headerText(l.DATE, formatDate(self.time)) + formatter:headerText(l.LOCATION, composeLocationString(self.location)) + formatter:headerText(l.IN_SYSTEM, ui.Format.SystemPath(self.systemp)) + formatter:headerText(l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name) + formatter:headerText(l.CASH, Format.Money(self.money)) + end + + return entry + end + return nil + end + + return my_generator +end + +function StationPathIterator() + local generator = FlightLog.GetStationPaths() + + local function my_generator() + local systemp, deptime, money, entry = generator() + if systemp ~= nil then + local entry = { + type = "station", + systemp = systemp, + deptime = deptime, + money = money, + entry = entry, + sort_date = deptime + } + + function entry:write_header( formatter ) + formatter:write( l.LOG_STATION ):write(":"):newline() + end + + function entry:write_details( formatter ) + local station_type = "FLIGHTLOG_" .. self.systemp:GetSystemBody().type + formatter:headerText(l.DATE, formatDate(self.deptime)) + formatter:headerText(l.STATION, string.interp(l[station_type], + { primary_info = self.systemp:GetSystemBody().name, + secondary_info = self.systemp:GetSystemBody().parent.name })) + -- formatter:headerText(l.LOCATION, self.systemp:GetSystemBody().parent.name) + formatter:headerText(l.IN_SYSTEM, ui.Format.SystemPath(self.systemp)) + formatter:headerText(l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name) + formatter:headerText(l.CASH, Format.Money(self.money)) + end + return entry + end + return nil + end + + return my_generator +end + +function SystemPathIterator() + local generator = FlightLog.GetSystemPaths() + + local function my_generator() + local systemp, arrtime, deptime, entry = generator() + if systemp ~= nil then + local entry = { + type = "system", + systemp = systemp, + arrtime = arrtime, + deptime = deptime, + entry = entry, + sort_date = arrtime + } + + if nil == entry.sort_date then + entry.sort_date = entry.deptime + end + + function entry:write_header( formatter ) + formatter:write( l.LOG_SYSTEM ):write(":"):newline() + end + + function entry:write_details( formatter ) + formatter:headerText(l.ARRIVAL_DATE, formatDate(self.arrtime)) + formatter:headerText(l.DEPARTURE_DATE, formatDate(self.deptime)) + formatter:headerText(l.IN_SYSTEM, ui.Format.SystemPath(self.systemp)) + formatter:headerText(l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name) + end + return entry + end + return nil + end + + return my_generator +end + +local ui_formatter = {} +local text_formatter = {} + +function ui_formatter:headerText(title, text, wrap) + -- Title text is gray, followed by the variable text: if not text then return end ui.textColored(gray, string.gsub(title, ":", "") .. ":") ui.sameLine() @@ -37,15 +162,42 @@ local function headerText(title, text, wrap) end end +function text_formatter:write( string ) + self.file:write( string ) + return self +end + +function text_formatter:newline() + self.file:write( "\n" ) + return self +end -local entering_text_custom = false -local entering_text_system = false -local entering_text_station = false +function text_formatter:close() + self.file:close() +end + +function text_formatter:headerText(title, text, wrap) + -- Title text is gray, followed by the variable text: + if not text then return end + self:write( string.gsub(title, ":", "") ):write( ": " ) + + -- TODO wrap? + self:write( text ):newline() + return self +end + +function text_formatter:separator() + self.file:write( "\n----------------------------------------------\n\n" ) +end + +entering_text_custom = false +entering_text_system = false +entering_text_station = false -- Display Entry text, and Edit button, to update flightlog -local function inputText(entry, counter, entering_text, log, str, clicked) +function inputText(entry, counter, entering_text, log, str, clicked) if #entry > 0 then - headerText(l.ENTRY, entry, true) + ui_formatter:headerText(l.ENTRY, entry, true) end if clicked or entering_text == counter then @@ -64,28 +216,16 @@ local function inputText(entry, counter, entering_text, log, str, clicked) return entering_text end --- Based on flight state, compose a reasonable string for location -local function composeLocationString(location) - return string.interp(l["FLIGHTLOG_"..location[1]], - { primary_info = location[2], - secondary_info = location[3] or "", - tertiary_info = location[4] or "",}) -end - -local function renderCustomLog() +local function renderCustomLog( formatter ) local counter = 0 local was_clicked = false - for systemp, time, money, location, entry in FlightLog.GetCustomEntry() do + for entry in CustomEntryIterator() do counter = counter + 1 - headerText(l.DATE, formatDate(time)) - headerText(l.LOCATION, composeLocationString(location)) - headerText(l.IN_SYSTEM, ui.Format.SystemPath(systemp)) - headerText(l.ALLEGIANCE, systemp:GetStarSystem().faction.name) - headerText(l.CASH, Format.Money(money)) + entry:write_details( formatter ) ::input:: - entering_text_custom = inputText(entry, counter, + entering_text_custom = inputText(entry.entry, counter, entering_text_custom, FlightLog.UpdateCustomEntry, "custom", was_clicked) ui.nextColumn() @@ -110,23 +250,16 @@ local function renderCustomLog() end -- See comments on previous function -local function renderStationLog() +local function renderStationLog( formatter ) local counter = 0 local was_clicked = false - for systemp, deptime, money, entry in FlightLog.GetStationPaths() do + for entry in StationPathIterator() do counter = counter + 1 - local station_type = "FLIGHTLOG_" .. systemp:GetSystemBody().type - headerText(l.DATE, formatDate(deptime)) - headerText(l.STATION, string.interp(l[station_type], - { primary_info = systemp:GetSystemBody().name, secondary_info = systemp:GetSystemBody().parent.name })) - -- headerText(l.LOCATION, systemp:GetSystemBody().parent.name) - headerText(l.IN_SYSTEM, ui.Format.SystemPath(systemp)) - headerText(l.ALLEGIANCE, systemp:GetStarSystem().faction.name) - headerText(l.CASH, Format.Money(money)) + entry:write_details( formatter ) ::input:: - entering_text_station = inputText(entry, counter, + entering_text_station = inputText(entry.entry, counter, entering_text_station, FlightLog.UpdateStationEntry, "station", was_clicked) ui.nextColumn() @@ -142,19 +275,16 @@ local function renderStationLog() end -- See comments on previous function -local function renderSystemLog() +local function renderSystemLog( formatter ) local counter = 0 local was_clicked = false - for systemp, arrtime, deptime, entry in FlightLog.GetSystemPaths() do + for entry in SystemPathIterator() do counter = counter + 1 - headerText(l.ARRIVAL_DATE, formatDate(arrtime)) - headerText(l.DEPARTURE_DATE, formatDate(deptime)) - headerText(l.IN_SYSTEM, ui.Format.SystemPath(systemp)) - headerText(l.ALLEGIANCE, systemp:GetStarSystem().faction.name) + entry:write_details( formatter ) ::input:: - entering_text_system = inputText(entry, counter, + entering_text_system = inputText(entry.entry, counter, entering_text_system, FlightLog.UpdateSystemEntry, "sys", was_clicked) ui.nextColumn() @@ -176,10 +306,112 @@ local function displayLog(logFn) ui.columns(2, "##flightLogColumns", false) ui.setColumnWidth(0, width - iconSize.x) - logFn() + logFn(ui_formatter) ui.columns(1) end +local function checkbox(label, checked, tooltip) +-- local color = colors.buttonBlue +-- local changed, ret +-- ui.withStyleColors({["Button"]=color,["ButtonHovered"]=color:tint(0.1),["CheckMark"]=color:tint(0.2)},function() +-- changed, ret = ui.checkbox(label, checked) +-- end) +-- if ui.isItemHovered() and tooltip then +-- Engine.pigui.SetTooltip(tooltip) -- bypass the mouse check, Game.player isn't valid yet +-- end + + changed, ret = ui.checkbox(label, checked) + + + return changed, ret +end + +local export_player_info = true +local export_custom_log = true +local export_station_log = true +local export_system_log = true + +local function exportLogs() + -- TODO localize? + local foldername = FileSystem.MakeUserDataDirectory( "player_logs" ) + + local player = Character.persistent.player + + local base_save_name = player.name + + local log_filename = FileSystem.JoinPath( foldername, base_save_name .. '.log' ) + + text_formatter.file = io.open( log_filename, "w" ) + + if export_player_info then + text_formatter:headerText( l.NAME_PERSON, player.name ) + -- TODO: localize + text_formatter:headerText( "Title", player.title ) + text_formatter:headerText( l.RATING, l[player:GetCombatRating()] ) + text_formatter:headerText( l.KILLS, string.format('%d',player.killcount) ) + text_formatter:separator() + text_formatter:newline() + end + + all_entries = {} + + if export_custom_log then + for entry in CustomEntryIterator() do + table.insert( all_entries, entry ) + end + end + + if export_station_log then + for entry in StationPathIterator() do + table.insert( all_entries, entry ) + end + end + + if export_system_log then + for entry in SystemPathIterator() do + table.insert( all_entries, entry ) + end + end + + local function sortf( a, b ) + return a.sort_date < b.sort_date + end + + table.sort( all_entries, sortf ) + + for i, entry in ipairs(all_entries) do + entry:write_header(text_formatter) + entry:write_details(text_formatter) + if #entry.entry > 0 then + text_formatter:headerText(l.ENTRY, entry.entry, true) + end + text_formatter:separator() + end + + text_formatter:close() + +end + +local function displayExportOptions() + ui.spacing() + local c + local flight_log = true; + + c,export_custom_log = checkbox(l.LOG_CUSTOM, export_custom_log) + c,export_station_log = checkbox(l.LOG_STATION, export_station_log) + c,export_system_log = checkbox(l.LOG_SYSTEM, export_system_log) + c,export_player_info = checkbox(l.PERSONAL_INFORMATION, export_player_info) + + ui.separator() + ui.spacing() + + if ui.button(l.SAVE) then + exportLogs() + end + +end + + local function drawFlightHistory() ui.tabBarFont("#flightlog", { @@ -187,7 +419,7 @@ local function drawFlightHistory() draw = function() ui.spacing() -- input field for custom log: - headerText(l.LOG_NEW, "") + ui_formatter:headerText(l.LOG_NEW, "") ui.sameLine() local text, changed = ui.inputText("##inputfield", "", {"EnterReturnsTrue"}) if changed then @@ -205,6 +437,11 @@ local function drawFlightHistory() { name = l.LOG_SYSTEM, draw = function() displayLog(renderSystemLog) + end }, + + { name = "Export", + draw = function() + displayExportOptions() end } }, pionillium.heading) From db9f4744f164a63f30df78e84eace68d93c3fde1 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sat, 23 Sep 2023 09:37:21 +1200 Subject: [PATCH 04/19] Now able to write player logs in basic html --- data/pigui/modules/info-view/06-flightlog.lua | 89 +++++++++++++++---- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index e8d99a3749f..8f730ac42bd 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -147,8 +147,6 @@ function SystemPathIterator() end local ui_formatter = {} -local text_formatter = {} - function ui_formatter:headerText(title, text, wrap) -- Title text is gray, followed by the variable text: if not text then return end @@ -162,6 +160,13 @@ function ui_formatter:headerText(title, text, wrap) end end +local text_formatter = {} + +function text_formatter:open( file ) + self.file = file + return self +end + function text_formatter:write( string ) self.file:write( string ) return self @@ -190,6 +195,44 @@ function text_formatter:separator() self.file:write( "\n----------------------------------------------\n\n" ) end +local html_formatter = {} + +function html_formatter:write( string ) + self.file:write( string ) + return self +end + +function html_formatter:open( file ) + self.file = file + self.file:write( "\n" ) + return self +end + +function html_formatter:newline() + self.file:write( "
\n" ) + return self +end + +function html_formatter:close() + self.file:write( "" ) + self.file:close() +end + +function html_formatter:headerText(title, text, wrap) + -- Title text is gray, followed by the variable text: + if not text then return end + self:write( "" ):write( string.gsub(title, ":", "") ):write( ": " ) + + -- TODO wrap? + self:write( text ):newline() + return self +end + +function html_formatter:separator() + self.file:write( "\n
\n" ) +end + + entering_text_custom = false entering_text_system = false entering_text_station = false @@ -330,6 +373,7 @@ local export_player_info = true local export_custom_log = true local export_station_log = true local export_system_log = true +local export_html = true local function exportLogs() -- TODO localize? @@ -339,18 +383,28 @@ local function exportLogs() local base_save_name = player.name - local log_filename = FileSystem.JoinPath( foldername, base_save_name .. '.log' ) + local formatter + local extension + if export_html then + formatter = html_formatter + extension = '.html' + else + formatter = text_formatter + extension = '.log' + end - text_formatter.file = io.open( log_filename, "w" ) + local log_filename = FileSystem.JoinPath( foldername, base_save_name .. extension ) + + formatter:open( io.open( log_filename, "w" ) ) if export_player_info then - text_formatter:headerText( l.NAME_PERSON, player.name ) + formatter:headerText( l.NAME_PERSON, player.name ) -- TODO: localize - text_formatter:headerText( "Title", player.title ) - text_formatter:headerText( l.RATING, l[player:GetCombatRating()] ) - text_formatter:headerText( l.KILLS, string.format('%d',player.killcount) ) - text_formatter:separator() - text_formatter:newline() + formatter:headerText( "Title", player.title ) + formatter:headerText( l.RATING, l[player:GetCombatRating()] ) + formatter:headerText( l.KILLS, string.format('%d',player.killcount) ) + formatter:separator() + formatter:newline() end all_entries = {} @@ -380,15 +434,15 @@ local function exportLogs() table.sort( all_entries, sortf ) for i, entry in ipairs(all_entries) do - entry:write_header(text_formatter) - entry:write_details(text_formatter) + entry:write_header(formatter) + entry:write_details(formatter) if #entry.entry > 0 then - text_formatter:headerText(l.ENTRY, entry.entry, true) + formatter:headerText(l.ENTRY, entry.entry, true) end - text_formatter:separator() + formatter:separator() end - text_formatter:close() + formatter:close() end @@ -397,10 +451,12 @@ local function displayExportOptions() local c local flight_log = true; + c,export_player_info = checkbox(l.PERSONAL_INFORMATION, export_player_info) c,export_custom_log = checkbox(l.LOG_CUSTOM, export_custom_log) c,export_station_log = checkbox(l.LOG_STATION, export_station_log) c,export_system_log = checkbox(l.LOG_SYSTEM, export_system_log) - c,export_player_info = checkbox(l.PERSONAL_INFORMATION, export_player_info) + c,export_html = checkbox("HTML", export_html) + ui.separator() ui.spacing() @@ -439,6 +495,7 @@ local function drawFlightHistory() displayLog(renderSystemLog) end }, + -- TODO localize { name = "Export", draw = function() displayExportOptions() From 252a7838e47eefff354db7ae7ede8892e6567353 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sat, 23 Sep 2023 20:34:37 +1200 Subject: [PATCH 05/19] Push some table-oriented views into the FlightLog generic system --- data/libs/FlightLog.lua | 223 +++++++++++++++++++++++++++++----------- 1 file changed, 162 insertions(+), 61 deletions(-) diff --git a/data/libs/FlightLog.lua b/data/libs/FlightLog.lua index c03088e34be..04e6542cac8 100644 --- a/data/libs/FlightLog.lua +++ b/data/libs/FlightLog.lua @@ -22,6 +22,126 @@ local FlightLogSystem = {} local FlightLogStation = {} local FlightLogCustom = {} +-- a generic log entry: +LogEntry = { + -- the index of the entry from the list which it came + index = 0, + -- a date that can be used to sort entries on TODO: remove this + sort_date = 0, + -- the entry text associated with this log entry + entry = "", + -- the type this logentry is + type = "unknown", + -- call this function to update the entry - will always be overloaded + UpdateEntry = function ( self, entry ) end, + new = function ( self, o ) + setmetatable( o, self ) + self.__index = self + return o + end +} + +-- a system log entry: +SystemLogEntry = LogEntry:new( { + type = "system", + -- the systemp for this system entered + systemp = nil, + -- arrival time for the the system (can be nil if we started the game here) + arrtime = nil, + -- departure time for the system (can be nil if still in system) + deptime = nil, + + new = function ( self, index, systemp, arrtime, deptime, entry ) + o = { + index = index, + systemp = systemp, + arrtime = arrtime, + deptime = deptime, + entry = entry + } + setmetatable( o, self ) + self.__index = self + + if nil == arrtime then + o.sort_date = deptime + else + o.sort_date = arrtime + end + + return o + end, + + UpdateEntry = function ( self, entry ) + FlightLogSystem[self.index][4] = entry + self.entry = entry + end +} ) + +-- a custom log entry: +CustomLogEntry = LogEntry:new( { + type = "custom", + -- the systemp for this system entered + systemp = nil, + -- time of the log entry + time = nil, + -- money the player had at time of entry + money = nil, + -- location of the entry + location = nil, + + new = function ( self, index, systemp, time, money, location, entry ) + o = { + index = index, + systemp = systemp, + time = time, + money = money, + location = location, + entry = entry, + sort_date = time + } + setmetatable( o, self ) + self.__index = self + + return o + end, + + UpdateEntry = function ( self, entry ) + FlightLogCustom[self.index][5] = entry + self.entry = entry + end +} ) + +-- a station log entry: +StationLogEntry = LogEntry:new( { + type = "station", + -- the systemp for this system entered + systemp = nil, + -- time of the log entry + deptime = nil, + -- money the player had at time of entry + money = nil, + + new = function ( self, index, systemp, deptime, money, entry ) + o = { + index = index, + systemp = systemp, + deptime = deptime, + money = money, + entry = entry, + sort_date = deptime + } + setmetatable( o, self ) + self.__index = self + + return o + end, + + UpdateEntry = function ( self, entry ) + FlightLogStation[self.index][4] = entry + self.entry = entry + end +} ) + local FlightLog FlightLog = { @@ -32,7 +152,7 @@ FlightLog = { -- -- Method: GetSystemPaths -- --- Returns an iterator returning a SystemPath object for each system visited +-- Returns an iterator returning a SystemLogEntry object for each system visited -- by the player, backwards in turn, starting with the most recent. If count -- is specified, returns no more than that many systems. -- @@ -54,8 +174,8 @@ FlightLog = { -- Print the names and departure times of the last five systems visited by -- the player -- --- > for systemp,arrtime,deptime,entry in FlightLog.GetSystemPaths(5) do --- > print(systemp:GetStarSystem().name, Format.Date(deptime)) +-- > for entry in FlightLog.GetSystemPaths(5) do +-- > print(entry.systemp:GetStarSystem().name, Format.Date(entry.deptime)) -- > end GetSystemPaths = function (maximum) @@ -65,45 +185,26 @@ FlightLog = { if counter < maximum then counter = counter + 1 if FlightLogSystem[counter] then - return FlightLogSystem[counter][1], - FlightLogSystem[counter][2], - FlightLogSystem[counter][3], - FlightLogSystem[counter][4] - end - end - return nil, nil, nil, nil - end - end, + local entry = SystemLogEntry:new( + counter, + FlightLogSystem[counter][1], + FlightLogSystem[counter][2], + FlightLogSystem[counter][3], + FlightLogSystem[counter][4] ) --- --- Method: UpdateSystemEntry --- --- Update the free text field in system log. --- --- > UpdateSystemEntry(index, entry) --- --- Parameters: --- --- index - Index in log, 1 being most recent (current) system --- entry - New text string to insert instead --- --- Example: --- --- Replace the second most recent system record, i.e. the previously --- visited system. --- --- > UpdateSystemEntry(2, "At Orion's shoulder, I see attackships on fire") --- + return entry - UpdateSystemEntry = function (index, entry) - FlightLogSystem[index][4] = entry + end + end + return nil + end end, -- -- Method: GetStationPaths -- --- Returns an iterator returning a SystemPath object for each station visited +-- Returns an iterator returning a StationLogEntry object for each station visited -- by the player, backwards in turn, starting with the most recent. If count -- is specified, returns no more than that many stations. -- @@ -115,18 +216,17 @@ FlightLog = { -- -- Return: -- --- iterator - A function which will generate the paths from the log, returning --- one each time it is called until it runs out, after which it --- returns nil. It also returns, as two additional value, the game --- time at which the player docked, and palyer's financial balance. +-- iterator - A function which will generate the StationLogEntry from the log, +-- returning one each time it is called until it runs out, after +-- which it returns nil. -- -- Example: -- -- Print the names and arrival times of the last five stations visited by -- the player -- --- > for systemp, deptime, money, entry in FlightLog.GetStationPaths(5) do --- > print(systemp:GetSystemBody().name, Format.Date(deptime)) +-- > for entry in FlightLog.GetStationPaths(5) do +-- > print(entry.systemp:GetSystemBody().name, Format.Date(entry.deptime)) -- > end GetStationPaths = function (maximum) @@ -136,10 +236,12 @@ FlightLog = { if counter < maximum then counter = counter + 1 if FlightLogStation[counter] then - return FlightLogStation[counter][1], - FlightLogStation[counter][2], - FlightLogStation[counter][3], - FlightLogStation[counter][4] + return StationLogEntry:new( + counter, + FlightLogStation[counter][1], + FlightLogStation[counter][2], + FlightLogStation[counter][3], + FlightLogStation[counter][4] ) end end return nil, nil, nil, nil @@ -232,10 +334,9 @@ FlightLog = { -- -- Method: GetCustomEntry -- --- Returns an iterator returning custom entries for each system the --- player has created a custom log entry for, backwards in turn, --- starting with the most recent. If count is specified, returns no --- more than that many entries. +-- Returns an iterator returning a CustomLogEntry, backwards in turn, +-- starting with the most recent for each log entry the player has made. +-- If count is specified, returns no more than that many entries. -- -- > iterator = FlightLog.GetCustomEntry(count) -- @@ -247,16 +348,13 @@ FlightLog = { -- -- iterator - A function which will generate the entries from the -- log, returning one each time it is called until it --- runs out, after which it returns nil. Each entry --- consists of the system's path, date, money, location, --- text; 'location' being an text array with flight state --- and appropriate additional information. - +-- runs out, after which it returns nil. Each entry is +-- a CustomLogEntry -- -- Example: -- --- > for systemp, date, money, location, entry in FlightLog.GetCustomEntry(5) do --- > print(location[1], location[2], Format.Date(deptime)) +-- > for entry in FlightLog.GetCustomEntry(5) do +-- > print(entry.location[1], entry.location[2], Format.Date(entry.deptime)) -- > end -- @@ -267,14 +365,17 @@ FlightLog = { if counter < maximum then counter = counter + 1 if FlightLogCustom[counter] then - return FlightLogCustom[counter][1], --path - FlightLogCustom[counter][2], --time - FlightLogCustom[counter][3], --money - FlightLogCustom[counter][4], --location - FlightLogCustom[counter][5] --manual entry + return CustomLogEntry:new( + counter, + FlightLogCustom[counter][1], --path + FlightLogCustom[counter][2], --time + FlightLogCustom[counter][3], --money + FlightLogCustom[counter][4], --location + FlightLogCustom[counter][5] --manual entry + ) end end - return nil, nil, nil, nil, nil + return end end, From 2dc351d8151ef60850c734f8d1c387ade02db8cf Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sat, 23 Sep 2023 20:34:54 +1200 Subject: [PATCH 06/19] Push some table-oriented views into the FlightLog generic system --- data/pigui/modules/info-view/06-flightlog.lua | 65 +++++-------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index 8f730ac42bd..56b26b86d1c 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -39,17 +39,8 @@ function CustomEntryIterator() local generator = FlightLog.GetCustomEntry() local function my_generator() - local systemp, time, money, location, entry = generator() - if systemp ~= nil then - local entry = { - type = "custom", - systemp = systemp, - time = time, - money = money, - location = location, - entry = entry, - sort_date = time - } + local entry = generator() + if entry ~= nil then function entry:write_header( formatter ) formatter:write( l.LOG_CUSTOM ):write(":"):newline() @@ -75,17 +66,8 @@ function StationPathIterator() local generator = FlightLog.GetStationPaths() local function my_generator() - local systemp, deptime, money, entry = generator() - if systemp ~= nil then - local entry = { - type = "station", - systemp = systemp, - deptime = deptime, - money = money, - entry = entry, - sort_date = deptime - } - + local entry = generator() + if entry ~= nil then function entry:write_header( formatter ) formatter:write( l.LOG_STATION ):write(":"):newline() end @@ -113,21 +95,8 @@ function SystemPathIterator() local generator = FlightLog.GetSystemPaths() local function my_generator() - local systemp, arrtime, deptime, entry = generator() - if systemp ~= nil then - local entry = { - type = "system", - systemp = systemp, - arrtime = arrtime, - deptime = deptime, - entry = entry, - sort_date = arrtime - } - - if nil == entry.sort_date then - entry.sort_date = entry.deptime - end - + local entry = generator() + if entry ~= nil then function entry:write_header( formatter ) formatter:write( l.LOG_SYSTEM ):write(":"):newline() end @@ -238,19 +207,19 @@ entering_text_system = false entering_text_station = false -- Display Entry text, and Edit button, to update flightlog -function inputText(entry, counter, entering_text, log, str, clicked) - if #entry > 0 then - ui_formatter:headerText(l.ENTRY, entry, true) +function inputText(entry, counter, entering_text, str, clicked) + if #entry.entry > 0 then + ui_formatter:headerText(l.ENTRY, entry.entry, true) end if clicked or entering_text == counter then ui.spacing() ui.pushItemWidth(-1.0) - local updated_entry, return_pressed = ui.inputText("##" ..str..counter, entry, {"EnterReturnsTrue"}) + local updated_entry, return_pressed = ui.inputText("##" ..str..counter, entry.entry, {"EnterReturnsTrue"}) ui.popItemWidth() entering_text = counter if return_pressed then - log(counter, updated_entry) + entry:UpdateEntry(updated_entry) entering_text = -1 end end @@ -268,8 +237,8 @@ local function renderCustomLog( formatter ) entry:write_details( formatter ) ::input:: - entering_text_custom = inputText(entry.entry, counter, - entering_text_custom, FlightLog.UpdateCustomEntry, "custom", was_clicked) + entering_text_custom = inputText(entry, counter, + entering_text_custom, "custom", was_clicked) ui.nextColumn() was_clicked = false @@ -302,8 +271,8 @@ local function renderStationLog( formatter ) entry:write_details( formatter ) ::input:: - entering_text_station = inputText(entry.entry, counter, - entering_text_station, FlightLog.UpdateStationEntry, "station", was_clicked) + entering_text_station = inputText(entry, counter, + entering_text_station, "station", was_clicked) ui.nextColumn() was_clicked = false @@ -327,8 +296,8 @@ local function renderSystemLog( formatter ) entry:write_details( formatter ) ::input:: - entering_text_system = inputText(entry.entry, counter, - entering_text_system, FlightLog.UpdateSystemEntry, "sys", was_clicked) + entering_text_system = inputText(entry, counter, + entering_text_system, "sys", was_clicked) ui.nextColumn() was_clicked = false From d269d5b5750d2f5d07d70e0e5de4b397a20406ff Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Tue, 3 Oct 2023 21:50:29 +1300 Subject: [PATCH 07/19] Move classes to use Pioneer lua utils class definitions --- data/libs/FlightLog.lua | 169 +++++++++++++++------------------------- 1 file changed, 61 insertions(+), 108 deletions(-) diff --git a/data/libs/FlightLog.lua b/data/libs/FlightLog.lua index 04e6542cac8..fe2253f2406 100644 --- a/data/libs/FlightLog.lua +++ b/data/libs/FlightLog.lua @@ -13,6 +13,8 @@ local Event = require 'Event' local Format = require 'Format' local Serializer = require 'Serializer' +local utils = require 'utils' + -- default values (private) local FlightLogSystemQueueLength = 1000 local FlightLogStationQueueLength = 1000 @@ -23,124 +25,75 @@ local FlightLogStation = {} local FlightLogCustom = {} -- a generic log entry: -LogEntry = { +LogEntry = utils.class("FlightLog.LogEntry") + +function LogEntry:Constructor( type, index, sort_date, entry ) -- the index of the entry from the list which it came - index = 0, + self.index = index -- a date that can be used to sort entries on TODO: remove this - sort_date = 0, + self.sort_date = sort_date -- the entry text associated with this log entry - entry = "", + self.entry = entry -- the type this logentry is - type = "unknown", - -- call this function to update the entry - will always be overloaded - UpdateEntry = function ( self, entry ) end, - new = function ( self, o ) - setmetatable( o, self ) - self.__index = self - return o - end -} + self.type = type +end --- a system log entry: -SystemLogEntry = LogEntry:new( { - type = "system", - -- the systemp for this system entered - systemp = nil, - -- arrival time for the the system (can be nil if we started the game here) - arrtime = nil, - -- departure time for the system (can be nil if still in system) - deptime = nil, - - new = function ( self, index, systemp, arrtime, deptime, entry ) - o = { - index = index, - systemp = systemp, - arrtime = arrtime, - deptime = deptime, - entry = entry - } - setmetatable( o, self ) - self.__index = self - - if nil == arrtime then - o.sort_date = deptime - else - o.sort_date = arrtime - end +SystemLogEntry = utils.class("FlightLog.SystemLogEntry", LogEntry) - return o - end, +function SystemLogEntry:Constructor( index, systemp, arrtime, deptime, entry ) - UpdateEntry = function ( self, entry ) - FlightLogSystem[self.index][4] = entry - self.entry = entry + if nil == arrtime then + sort_date = deptime + else + sort_date = arrtime end -} ) + + LogEntry.Constructor( self, "system", index, sort_date, entry ) + + self.systemp = systemp + self.arrtime = arrtime + self.deptime = deptime + +end + +function SystemLogEntry:UpdateEntry( entry ) + FlightLogSystem[self.index][4] = entry + self.entry = entry +end -- a custom log entry: -CustomLogEntry = LogEntry:new( { - type = "custom", - -- the systemp for this system entered - systemp = nil, - -- time of the log entry - time = nil, - -- money the player had at time of entry - money = nil, - -- location of the entry - location = nil, - - new = function ( self, index, systemp, time, money, location, entry ) - o = { - index = index, - systemp = systemp, - time = time, - money = money, - location = location, - entry = entry, - sort_date = time - } - setmetatable( o, self ) - self.__index = self - - return o - end, +CustomLogEntry = utils.class("FlightLog.CustomLogEntry", LogEntry) + +function CustomLogEntry:Constructor( index, systemp, time, money, location, entry ) + LogEntry.Constructor( self, "custom", index, time, entry ) + + self.systemp = systemp + self.time = time + self.money = money + self.location = location +end + +function CustomLogEntry:UpdateEntry( entry ) + FlightLogCustom[self.index][5] = entry + self.entry = entry +end + - UpdateEntry = function ( self, entry ) - FlightLogCustom[self.index][5] = entry - self.entry = entry - end -} ) - -- a station log entry: -StationLogEntry = LogEntry:new( { - type = "station", - -- the systemp for this system entered - systemp = nil, - -- time of the log entry - deptime = nil, - -- money the player had at time of entry - money = nil, - - new = function ( self, index, systemp, deptime, money, entry ) - o = { - index = index, - systemp = systemp, - deptime = deptime, - money = money, - entry = entry, - sort_date = deptime - } - setmetatable( o, self ) - self.__index = self - - return o - end, +StationLogEntry = utils.class("FlightLog.StationLogEntry", LogEntry) - UpdateEntry = function ( self, entry ) - FlightLogStation[self.index][4] = entry - self.entry = entry - end -} ) +function StationLogEntry:Constructor( index, systemp, deptime, money, entry ) + LogEntry.Constructor( self, "station", index, deptime, entry ) + + self.systemp = systemp + self.deptime = deptime + self.money = money +end + +function StationLogEntry:UpdateEntry( entry ) + FlightLogStation[self.index][4] = entry + self.entry = entry +end local FlightLog FlightLog = { @@ -186,7 +139,7 @@ FlightLog = { counter = counter + 1 if FlightLogSystem[counter] then - local entry = SystemLogEntry:new( + local entry = SystemLogEntry.New( counter, FlightLogSystem[counter][1], FlightLogSystem[counter][2], @@ -236,7 +189,7 @@ FlightLog = { if counter < maximum then counter = counter + 1 if FlightLogStation[counter] then - return StationLogEntry:new( + return StationLogEntry.New( counter, FlightLogStation[counter][1], FlightLogStation[counter][2], @@ -365,7 +318,7 @@ FlightLog = { if counter < maximum then counter = counter + 1 if FlightLogCustom[counter] then - return CustomLogEntry:new( + return CustomLogEntry.New( counter, FlightLogCustom[counter][1], --path FlightLogCustom[counter][2], --time From 28410fc2c299bfdbbbbaa59e1e2cb71dfc0f3518 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Wed, 4 Oct 2023 20:59:46 +1300 Subject: [PATCH 08/19] Move more of the flight log code into the base module. Now only one tab for the flight log with an options panel on the side to filter and save --- data/libs/FlightLog.lua | 256 +++++++++++- data/libs/utils.lua | 40 ++ data/pigui/modules/info-view/06-flightlog.lua | 363 ++++++------------ 3 files changed, 390 insertions(+), 269 deletions(-) diff --git a/data/libs/FlightLog.lua b/data/libs/FlightLog.lua index fe2253f2406..0ee2e910c61 100644 --- a/data/libs/FlightLog.lua +++ b/data/libs/FlightLog.lua @@ -15,6 +15,15 @@ local Serializer = require 'Serializer' local utils = require 'utils' +local Character = require 'Character' + + +-- required for formatting / localisation +local ui = require 'pigui' +local Lang = require 'Lang' +local l = Lang.GetResource("ui-core") +-- end of formating / localisation stuff + -- default values (private) local FlightLogSystemQueueLength = 1000 local FlightLogStationQueueLength = 1000 @@ -25,20 +34,95 @@ local FlightLogStation = {} local FlightLogCustom = {} -- a generic log entry: -LogEntry = utils.class("FlightLog.LogEntry") +LogEntry = utils.class("FlightLog.LogEntry.Base") + +function LogEntry:GetType() + local full_type = self.Class().meta.class + return string.sub( full_type, #"FlightLog.LogEntry."+1, #full_type ) +end + +-- return true if this has a modifiable entry field +function LogEntry:HasEntry() + return true +end -function LogEntry:Constructor( type, index, sort_date, entry ) + +-- return true if this has a Delete() method +function LogEntry:SupportsDelete() + return false +end + +function LogEntry:Constructor( index, sort_date, entry ) -- the index of the entry from the list which it came self.index = index -- a date that can be used to sort entries on TODO: remove this self.sort_date = sort_date -- the entry text associated with this log entry self.entry = entry - -- the type this logentry is - self.type = type + self.type = self:GetType() +end + +-- return the name for this log entry type +function LogEntry:GetLocalizedName() + return "Error" +end + +-- return an array of key value pairs, the key being localized and the +-- value being formatted appropriately. +function LogEntry:GetDataPairs() + return { "ERROR", "This should never be seen" } +end + +function LogEntry:UpdateEntry( entry ) + self.entry = entry +end + +-- convenience helper function +-- Sometimes date is empty, e.g. departure date prior to departure +-- TODO: maybe not return this at all then! +function LogEntry.formatDate(date) + return date and Format.Date(date) or nil +end + +-- Based on flight state, compose a reasonable string for location +function LogEntry.composeLocationString(location) + return string.interp(l["FLIGHTLOG_"..location[1]], + { primary_info = location[2], + secondary_info = location[3] or "", + tertiary_info = location[4] or "",}) +end + +CurrentStatusLogEntry = utils.class("FilghtLog.LogEntry.CurrentStatus", LogEntry ) + +function CurrentStatusLogEntry:HasEntry() + return false +end + + +function CurrentStatusLogEntry:Constructor() + LogEntry.Constructor( self, 0, Game.time, "" ) +end + +function CurrentStatusLogEntry:GetLocalizedName() + return l.PERSONAL_INFORMATION; end -SystemLogEntry = utils.class("FlightLog.SystemLogEntry", LogEntry) +-- return an array of key value pairs, the key being localized and the +-- value being formatted appropriately. +function CurrentStatusLogEntry:GetDataPairs() + + local player = Character.persistent.player + + return { + { l.NAME_PERSON, player.name }, + -- TODO: localize + { "Title", player.title }, + { l.RATING, l[player:GetCombatRating()] }, + { l.KILLS, string.format('%d',player.killcount) } + } +end + +SystemLogEntry = utils.class("FlightLog.LogEntry.System", LogEntry) function SystemLogEntry:Constructor( index, systemp, arrtime, deptime, entry ) @@ -48,7 +132,7 @@ function SystemLogEntry:Constructor( index, systemp, arrtime, deptime, entry ) sort_date = arrtime end - LogEntry.Constructor( self, "system", index, sort_date, entry ) + LogEntry.Constructor( self, index, sort_date, entry ) self.systemp = systemp self.arrtime = arrtime @@ -56,16 +140,31 @@ function SystemLogEntry:Constructor( index, systemp, arrtime, deptime, entry ) end +function SystemLogEntry:GetLocalizedName() + return l.LOG_SYSTEM; +end + +-- return an array of key value pairs, the key being localized and the +-- value being formatted appropriately. +function SystemLogEntry:GetDataPairs() + return { + { l.ARRIVAL_DATE, self.formatDate(self.arrtime) }, + { l.DEPARTURE_DATE, self.formatDate(self.deptime) }, + { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, + { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name } + } +end + function SystemLogEntry:UpdateEntry( entry ) + LogEntry.UpdateEntry( self, entry ) FlightLogSystem[self.index][4] = entry - self.entry = entry end -- a custom log entry: -CustomLogEntry = utils.class("FlightLog.CustomLogEntry", LogEntry) +CustomLogEntry = utils.class("FlightLog.LogEntry.Custom", LogEntry) function CustomLogEntry:Constructor( index, systemp, time, money, location, entry ) - LogEntry.Constructor( self, "custom", index, time, entry ) + LogEntry.Constructor( self, index, time, entry ) self.systemp = systemp self.time = time @@ -73,26 +172,79 @@ function CustomLogEntry:Constructor( index, systemp, time, money, location, entr self.location = location end +function CustomLogEntry:GetLocalizedName() + return l.LOG_CUSTOM; +end + +-- return an array of key value pairs, the key being localized and the +-- value being formatted appropriately. +function CustomLogEntry:GetDataPairs() + return { + { l.DATE, self.formatDate(self.time) }, + { l.LOCATION, self.composeLocationString(self.location) }, + { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, + { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name }, + { l.CASH, Format.Money(self.money) } + } +end + + function CustomLogEntry:UpdateEntry( entry ) + LogEntry.UpdateEntry( self, entry ) FlightLogCustom[self.index][5] = entry - self.entry = entry +end + +-- return true if this has a Delete() method +function CustomLogEntry:SupportsDelete() + return true +end + +function CustomLogEntry:Delete() + table.remove(FlightLogCustom, index) + + -- TODO: now all the indexes are wrong. + -- we should do away with them all! end -- a station log entry: -StationLogEntry = utils.class("FlightLog.StationLogEntry", LogEntry) +StationLogEntry = utils.class("FlightLog.LogEntry.Station", LogEntry) function StationLogEntry:Constructor( index, systemp, deptime, money, entry ) - LogEntry.Constructor( self, "station", index, deptime, entry ) + LogEntry.Constructor( self, index, deptime, entry ) self.systemp = systemp self.deptime = deptime self.money = money end +function StationLogEntry:GetLocalizedName() + return l.LOG_STATION; +end + +-- return an array of key value pairs, the key being localized and the +-- value being formatted appropriately. +function StationLogEntry:GetDataPairs() + + local station_type = "FLIGHTLOG_" .. self.systemp:GetSystemBody().type + + + return { + { l.DATE, self.formatDate(self.deptime) }, + { l.STATION, string.interp(l[station_type], + { primary_info = self.systemp:GetSystemBody().name, + secondary_info = self.systemp:GetSystemBody().parent.name }) }, +-- { l.LOCATION, self.systemp:GetSystemBody().parent.name }, + { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, + { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name }, + { l.CASH, Format.Money(self.money) }, + } +end + + function StationLogEntry:UpdateEntry( entry ) + LogEntry.UpdateEntry( self, entry ) FlightLogStation[self.index][4] = entry - self.entry = entry end local FlightLog @@ -101,7 +253,6 @@ FlightLog = { -- -- Group: Methods -- - -- -- Method: GetSystemPaths -- @@ -432,6 +583,83 @@ FlightLog = { } +-- +-- Method: GetLogEntries +-- +-- Parameters: +-- +-- types - An array of the types we want to fetch, nil if all of them +-- maximum - the maximum number of results to return +-- Return: +-- +-- iterator - A function which will generate the entries from the +-- log, returning one each time it is called until it +-- runs out, after which it returns nil. Each entry is +-- a child class of LogEntry +-- +-- Example: +-- +-- > for entry in FlightLog.GetLogEntries( { "Custom", "System", "Station" ) do +-- > print( entry.GetType(), entry.entry ) +-- > end +function FlightLog:GetLogEntries(types, maximum) + + -- TODO: actually just store a list of all of them as they are at startup + local type_set = utils.set.New(types) + + local all_entries = {} + + if nil == types or type_set:contains( 'Custom' ) then + for entry in self.GetCustomEntry() do + table.insert( all_entries, entry ) + end + end + + if nil == types or type_set:contains( 'Station' ) then + for entry in self.GetStationPaths() do + table.insert( all_entries, entry ) + end + end + + if nil == types or type_set:contains( 'System' ) then + for entry in self.GetSystemPaths() do + table.insert( all_entries, entry ) + end + end + + + + local function sortf( a, b ) + return a.sort_date > b.sort_date + end + + + + table.sort( all_entries, sortf ) + + -- note regardless of sort order, current status always comes first. + local currentStatus = nil + if nil == types or type_set:contains( "CurrentStatus" ) then + currentStatus = CurrentStatusLogEntry.New() + end + + local counter = 0 + maximum = maximum or #all_entries + return function () + if currentStatus then + t = currentStatus + currentStatus = nil + return t + end + if counter < maximum then + counter = counter + 1 + return all_entries[counter] + end + return nil + end +end + + -- LOGGING -- onLeaveSystem diff --git a/data/libs/utils.lua b/data/libs/utils.lua index 63d0eb6f27b..7d2015bbfd9 100644 --- a/data/libs/utils.lua +++ b/data/libs/utils.lua @@ -780,4 +780,44 @@ utils.getFromIntervals = function(array, value) return array[utils.getIndexFromIntervals(array, value)][1] end +-- a simple set class +utils.set = utils.class( "utils.set" ) + +-- Constructor +-- Build a set +-- Parameters: +-- +-- array - an array, every element of which will be added to the set. If not provided the set starts empty +-- number - some value in intervals +function utils.set:Constructor( array ) + self.set = {} + if nil ~= array then + for _, value in pairs( array ) do + self.set[value] = true + end + end +end + +function utils.set:add( value ) + self.set[value] = true +end + +function utils.set:remove( value ) + self.set[value] = nil +end + +function utils.set:contains( value ) + return self.set[value] ~= nil +end + +function utils.set:to_array() + local array = {} + for v, _ in pairs( self.set ) do + array[#array+1] = v + end + return array +end + return utils + + diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index 56b26b86d1c..fb185d99199 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -21,98 +21,32 @@ local l = Lang.GetResource("ui-core") local iconSize = ui.rescaleUI(Vector2(28, 28)) local buttonSpaceSize = iconSize --- Sometimes date is empty, e.g. departure date prior to departure -local function formatDate(date) - return date and Format.Date(date) or nil -end - --- Based on flight state, compose a reasonable string for location -local function composeLocationString(location) - return string.interp(l["FLIGHTLOG_"..location[1]], - { primary_info = location[2], - secondary_info = location[3] or "", - tertiary_info = location[4] or "",}) -end - - -function CustomEntryIterator() - local generator = FlightLog.GetCustomEntry() - - local function my_generator() - local entry = generator() - if entry ~= nil then - - function entry:write_header( formatter ) - formatter:write( l.LOG_CUSTOM ):write(":"):newline() - end - function entry:write_details( formatter ) - formatter:headerText(l.DATE, formatDate(self.time)) - formatter:headerText(l.LOCATION, composeLocationString(self.location)) - formatter:headerText(l.IN_SYSTEM, ui.Format.SystemPath(self.systemp)) - formatter:headerText(l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name) - formatter:headerText(l.CASH, Format.Money(self.money)) - end +local include_player_info = true +local include_custom_log = true +local include_station_log = true +local include_system_log = true +local export_html = true - return entry - end - return nil - end +local function getIncludedSet() + o = {} + if include_player_info then o[#o+1] = "CurrentStatus" end + if include_custom_log then o[#o+1] = "Custom" end + if include_station_log then o[#o+1] = "Station" end + if include_system_log then o[#o+1] = "System" end - return my_generator + return o; end -function StationPathIterator() - local generator = FlightLog.GetStationPaths() - - local function my_generator() - local entry = generator() - if entry ~= nil then - function entry:write_header( formatter ) - formatter:write( l.LOG_STATION ):write(":"):newline() - end - function entry:write_details( formatter ) - local station_type = "FLIGHTLOG_" .. self.systemp:GetSystemBody().type - formatter:headerText(l.DATE, formatDate(self.deptime)) - formatter:headerText(l.STATION, string.interp(l[station_type], - { primary_info = self.systemp:GetSystemBody().name, - secondary_info = self.systemp:GetSystemBody().parent.name })) - -- formatter:headerText(l.LOCATION, self.systemp:GetSystemBody().parent.name) - formatter:headerText(l.IN_SYSTEM, ui.Format.SystemPath(self.systemp)) - formatter:headerText(l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name) - formatter:headerText(l.CASH, Format.Money(self.money)) - end - return entry - end - return nil +function writeLogEntry( entry, formatter, write_header ) + if write_header then + formatter:write( entry:GetLocalizedName() ):newline() end - return my_generator -end - -function SystemPathIterator() - local generator = FlightLog.GetSystemPaths() - - local function my_generator() - local entry = generator() - if entry ~= nil then - function entry:write_header( formatter ) - formatter:write( l.LOG_SYSTEM ):write(":"):newline() - end - - function entry:write_details( formatter ) - formatter:headerText(l.ARRIVAL_DATE, formatDate(self.arrtime)) - formatter:headerText(l.DEPARTURE_DATE, formatDate(self.deptime)) - formatter:headerText(l.IN_SYSTEM, ui.Format.SystemPath(self.systemp)) - formatter:headerText(l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name) - end - return entry - end - return nil + for _, pair in pairs( entry:GetDataPairs() ) do + formatter:headerText( pair[1], pair[2] ) end - - return my_generator end local ui_formatter = {} @@ -129,6 +63,17 @@ function ui_formatter:headerText(title, text, wrap) end end +function ui_formatter:write( string ) + ui.text( string ) + ui.sameLine() + return self +end + +function ui_formatter:newline() + ui.text( "" ) + return self +end + local text_formatter = {} function text_formatter:open( file ) @@ -201,10 +146,7 @@ function html_formatter:separator() self.file:write( "\n
\n" ) end - -entering_text_custom = false -entering_text_system = false -entering_text_station = false +entering_text = false -- Display Entry text, and Edit button, to update flightlog function inputText(entry, counter, entering_text, str, clicked) @@ -228,100 +170,56 @@ function inputText(entry, counter, entering_text, str, clicked) return entering_text end -local function renderCustomLog( formatter ) - local counter = 0 - local was_clicked = false - for entry in CustomEntryIterator() do - counter = counter + 1 - - entry:write_details( formatter ) +local function renderLog( formatter ) - ::input:: - entering_text_custom = inputText(entry, counter, - entering_text_custom, "custom", was_clicked) - ui.nextColumn() - - was_clicked = false - if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##custom"..counter) then - was_clicked = true - -- If edit field was clicked, we want to edit _this_ iteration's field, - -- not next record's. Quick, behind you, velociraptor! - goto input - end - - if ui.iconButton(icons.trashcan, buttonSpaceSize, l.REMOVE .. "##custom" .. counter) then - FlightLog.DeleteCustomEntry(counter) - -- if we were already in edit mode, reset it, or else it carries over to next iteration - entering_text_custom = false - end - - ui.nextColumn() - ui.separator() - ui.spacing() + ui.spacing() + -- input field for custom log: + ui_formatter:headerText(l.LOG_NEW, "") + ui.sameLine() + local text, changed = ui.inputText("##inputfield", "", {"EnterReturnsTrue"}) + if changed then + FlightLog.MakeCustomEntry(text) end -end + ui.separator() --- See comments on previous function -local function renderStationLog( formatter ) local counter = 0 local was_clicked = false - for entry in StationPathIterator() do - counter = counter + 1 - - entry:write_details( formatter ) - - ::input:: - entering_text_station = inputText(entry, counter, - entering_text_station, "station", was_clicked) - ui.nextColumn() - - was_clicked = false - if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##station"..counter) then - was_clicked = true - goto input + for entry in FlightLog:GetLogEntries(getIncludedSet()) do + counter = counter + 1 + + writeLogEntry( entry, formatter, true ) + + if entry:HasEntry() then + ::input:: + entering_text = inputText(entry, counter, + entering_text, "custom", was_clicked) + ui.nextColumn() + + was_clicked = false + if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##custom"..counter) then + was_clicked = true + -- If edit field was clicked, we want to edit _this_ iteration's field, + -- not next record's. Quick, behind you, velociraptor! + goto input + end + else + ui.nextColumn() end - ui.nextColumn() - ui.separator() - end -end - --- See comments on previous function -local function renderSystemLog( formatter ) - local counter = 0 - local was_clicked = false - for entry in SystemPathIterator() do - counter = counter + 1 - - entry:write_details( formatter ) - - ::input:: - entering_text_system = inputText(entry, counter, - entering_text_system, "sys", was_clicked) - ui.nextColumn() - - was_clicked = false - if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##system"..counter) then - was_clicked = true - goto input + if entry:SupportsDelete() then + if ui.iconButton(icons.trashcan, buttonSpaceSize, l.REMOVE .. "##custom" .. counter) then + entry:Delete() + -- if we were already in edit mode, reset it, or else it carries over to next iteration + entering_text = false + end end ui.nextColumn() ui.separator() + ui.spacing() end end -local function displayLog(logFn) - ui.spacing() - -- reserve a narrow right column for edit / remove icon - local width = ui.getContentRegion().x - ui.columns(2, "##flightLogColumns", false) - ui.setColumnWidth(0, width - iconSize.x) - - logFn(ui_formatter) - ui.columns(1) -end - local function checkbox(label, checked, tooltip) -- local color = colors.buttonBlue -- local changed, ret @@ -338,12 +236,6 @@ local function checkbox(label, checked, tooltip) return changed, ret end -local export_player_info = true -local export_custom_log = true -local export_station_log = true -local export_system_log = true -local export_html = true - local function exportLogs() -- TODO localize? local foldername = FileSystem.MakeUserDataDirectory( "player_logs" ) @@ -366,45 +258,10 @@ local function exportLogs() formatter:open( io.open( log_filename, "w" ) ) - if export_player_info then - formatter:headerText( l.NAME_PERSON, player.name ) - -- TODO: localize - formatter:headerText( "Title", player.title ) - formatter:headerText( l.RATING, l[player:GetCombatRating()] ) - formatter:headerText( l.KILLS, string.format('%d',player.killcount) ) - formatter:separator() - formatter:newline() - end - - all_entries = {} - - if export_custom_log then - for entry in CustomEntryIterator() do - table.insert( all_entries, entry ) - end - end - - if export_station_log then - for entry in StationPathIterator() do - table.insert( all_entries, entry ) - end - end - - if export_system_log then - for entry in SystemPathIterator() do - table.insert( all_entries, entry ) - end - end - - local function sortf( a, b ) - return a.sort_date < b.sort_date - end + for entry in FlightLog:GetLogEntries(getIncludedSet()) do - table.sort( all_entries, sortf ) + writeLogEntry(entry, formatter, true) - for i, entry in ipairs(all_entries) do - entry:write_header(formatter) - entry:write_details(formatter) if #entry.entry > 0 then formatter:headerText(l.ENTRY, entry.entry, true) end @@ -415,19 +272,20 @@ local function exportLogs() end -local function displayExportOptions() +local function displayFilterOptions() ui.spacing() local c local flight_log = true; - c,export_player_info = checkbox(l.PERSONAL_INFORMATION, export_player_info) - c,export_custom_log = checkbox(l.LOG_CUSTOM, export_custom_log) - c,export_station_log = checkbox(l.LOG_STATION, export_station_log) - c,export_system_log = checkbox(l.LOG_SYSTEM, export_system_log) + c,include_player_info = checkbox(l.PERSONAL_INFORMATION, include_player_info) + c,include_custom_log = checkbox(l.LOG_CUSTOM, include_custom_log) + c,include_station_log = checkbox(l.LOG_STATION, include_station_log) + c,include_system_log = checkbox(l.LOG_SYSTEM, include_system_log) + ui.spacing() + ui.separator() + ui.spacing() c,export_html = checkbox("HTML", export_html) - - ui.separator() ui.spacing() if ui.button(l.SAVE) then @@ -438,44 +296,37 @@ end local function drawFlightHistory() - ui.tabBarFont("#flightlog", { - - { name = l.LOG_CUSTOM, - draw = function() - ui.spacing() - -- input field for custom log: - ui_formatter:headerText(l.LOG_NEW, "") - ui.sameLine() - local text, changed = ui.inputText("##inputfield", "", {"EnterReturnsTrue"}) - if changed then - FlightLog.MakeCustomEntry(text) - end - ui.separator() - displayLog(renderCustomLog) - end }, - - { name = l.LOG_STATION, - draw = function() - displayLog(renderStationLog) - end }, - - { name = l.LOG_SYSTEM, - draw = function() - displayLog(renderSystemLog) - end }, - - -- TODO localize - { name = "Export", - draw = function() - displayExportOptions() - end } - - }, pionillium.heading) + + ui.spacing() + -- reserve a narrow right column for edit / remove icon + local width = ui.getContentRegion().x + + + ui.columns(2, "##flightLogColumns", false) + ui.setColumnWidth(0, width - (iconSize.x*3)/2) + ui.setColumnWidth(1, (iconSize.x*3)/2) + + renderLog(ui_formatter) end -local function drawLog () - ui.withFont(pionillium.body, function() - drawFlightHistory() +local function drawScreen() + + local width = ui.getContentRegion().x + + ui.columns(2, "flightLogTop", false) + + ui.setColumnWidth(0, (width*3)/4) + + ui.child( "FlightLogList", function() + ui.withFont(pionillium.body, function() + drawFlightHistory() + end) + end) + ui.nextColumn() + ui.child( "FlightLogConfig", function() + ui.withFont(pionillium.body, function() + displayFilterOptions() + end) end) end @@ -484,7 +335,9 @@ InfoView:registerView({ name = l.FLIGHT_LOG, icon = ui.theme.icons.bookmark, showView = true, - draw = drawLog, + draw = drawScreen, refresh = function() end, - debugReload = function() package.reimport() end + debugReload = function() + package.reimport() + end }) From 2d516c34c715d11423ed40724f55f2614e5aa86e Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Thu, 5 Oct 2023 21:40:36 +1300 Subject: [PATCH 09/19] More flight log changes - remove the tabs and allow the user to filter what is shown based on checkboxes on the right Also split system and arrivals into separate events if there are other log entries between More consolidation and refactor of the code into flightlog.lua This changes the way that flightlogs are persisted but will still load the old version --- data/libs/FlightLog.lua | 794 +++++++++--------- data/pigui/modules/info-view/06-flightlog.lua | 21 +- 2 files changed, 396 insertions(+), 419 deletions(-) diff --git a/data/libs/FlightLog.lua b/data/libs/FlightLog.lua index 0ee2e910c61..b981ec7e61d 100644 --- a/data/libs/FlightLog.lua +++ b/data/libs/FlightLog.lua @@ -17,7 +17,6 @@ local utils = require 'utils' local Character = require 'Character' - -- required for formatting / localisation local ui = require 'pigui' local Lang = require 'Lang' @@ -25,66 +24,143 @@ local l = Lang.GetResource("ui-core") -- end of formating / localisation stuff -- default values (private) -local FlightLogSystemQueueLength = 1000 -local FlightLogStationQueueLength = 1000 +---@type integer +local MaxTotalDefaultElements = 3000 +-- how many default (so not custom) elements do we have +---@type integer +local TotalDefaultElements = 0 -- private data - the log itself -local FlightLogSystem = {} -local FlightLogStation = {} -local FlightLogCustom = {} - --- a generic log entry: +---@type LogEntry[] +local FlightLogData = {} + +---@class LogEntry +---A generic log entry: +---@field protected sort_date number Seconds since the epoch in game time, used to sort when loading old style +---@field protected entry string User entered text associated with the entry +---@field protected always_custom boolean|nil Is this always treated as a custom entry (so not auto deleted) LogEntry = utils.class("FlightLog.LogEntry.Base") +---@return string Description of this type function LogEntry:GetType() local full_type = self.Class().meta.class return string.sub( full_type, #"FlightLog.LogEntry."+1, #full_type ) end --- return true if this has a modifiable entry field -function LogEntry:HasEntry() +---@return boolean true if this is considered to have an entry +function LogEntry:CanHaveEntry() return true end +---@return boolean true if this has an entry +function LogEntry:HasEntry() + return self.entry and #self.entry > 0 +end --- return true if this has a Delete() method +---@return string The user provided entry or an empty string if there isn't one. +function LogEntry:GetEntry() + if not self.entry then return "" end + return self.entry +end + +---@return boolean true if this has a Delete() method function LogEntry:SupportsDelete() return false end -function LogEntry:Constructor( index, sort_date, entry ) - -- the index of the entry from the list which it came - self.index = index - -- a date that can be used to sort entries on TODO: remove this - self.sort_date = sort_date - -- the entry text associated with this log entry - self.entry = entry - self.type = self:GetType() + +---@return boolean True if this log entry should be considered custom and so not auto deleted +function LogEntry:IsCustom() + if self.always_custom then return true end + + if not self.entry then return false end + if self.entry == "" then return false end + return true +end + +-- The serialization table is an array +-- the elements of the array are a key-value pair +-- the key is the serialization index (so the type this log entry is) +-- the value is an array of data, to construct the table from. +---@param table table +---@return nil +function LogEntry:AddToSerializationTable( table ) + local v = {} + v[self.GetSerializationIndex()] = self:GetSerializationElements() + table[#table+1] = v +end + +---@return integer A unique integer for this specific type +function LogEntry.GetSerializationIndex() + return -1 +end + +---@return any[] An array of the elements as they will be serialized +function LogEntry:GetSerializationElements() + return {} end --- return the name for this log entry type + +---@return string The name for this log entry type function LogEntry:GetLocalizedName() return "Error" end --- return an array of key value pairs, the key being localized and the --- value being formatted appropriately. -function LogEntry:GetDataPairs() - return { "ERROR", "This should never be seen" } +---@param earliest_first boolean set to true if your sort order is to show the earlist first dates +---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. +function LogEntry:GetDataPairs( earliest_first ) + return { { "ERROR", "This should never be seen" } } end +---@param entry string A user provided description of the event. +---If non nil/empty this will cause the entry to be considered custom and not automatically deleted +---@return nil function LogEntry:UpdateEntry( entry ) + + if self:IsCustom() then + TotalDefaultElements = TotalDefaultElements-1 + end + + if entry and #entry == 0 then entry = nil end self.entry = entry + + if self:IsCustom() then + TotalDefaultElements = TotalDefaultElements+1 + end + +end + +---@param sort_date number The date to sort by (from epoch) +---@param entry string|nil The user entered custom test for this entry +---@param always_custom boolean Is this always treated as a custom entry +function LogEntry:Constructor( sort_date, entry, always_custom ) + -- a date that can be used to sort entries on TODO: remove this + self.sort_date = sort_date + -- the entry text associated with this log entry + if entry and #entry == 0 then entry = nil end + self.entry = entry + self.always_custom = always_custom + if self:IsCustom() then + TotalDefaultElements = TotalDefaultElements+1 + end end --- convenience helper function --- Sometimes date is empty, e.g. departure date prior to departure --- TODO: maybe not return this at all then! + +--- convenience helper function +--- Sometimes date is empty, e.g. departure date prior to departure +--- TODO: maybe not return this at all then! +--- +---@param date number The date since the epoch +--- +---@return string The date formatted function LogEntry.formatDate(date) return date and Format.Date(date) or nil end --- Based on flight state, compose a reasonable string for location +--- Based on flight state, compose a reasonable string for location +--- TODO: consider a class to represent, construct, store and format this +---@param location string[] Array of string info, the first one is the +---@return string The formatted composite location. function LogEntry.composeLocationString(location) return string.interp(l["FLIGHTLOG_"..location[1]], { primary_info = location[2], @@ -92,25 +168,27 @@ function LogEntry.composeLocationString(location) tertiary_info = location[4] or "",}) end +---@class CurrentStatusLogEngtry : LogEntry +--- Does not have any members, it grabs the current status live whenever requested CurrentStatusLogEntry = utils.class("FilghtLog.LogEntry.CurrentStatus", LogEntry ) -function CurrentStatusLogEntry:HasEntry() +---@return boolean true if this is considered to have an entry +function CurrentStatusLogEntry:CanHaveEntry() return false end - function CurrentStatusLogEntry:Constructor() - LogEntry.Constructor( self, 0, Game.time, "" ) + LogEntry.Constructor( self, Game.time, nil, true ) end +---@return string The name for this log entry type function CurrentStatusLogEntry:GetLocalizedName() return l.PERSONAL_INFORMATION; end --- return an array of key value pairs, the key being localized and the --- value being formatted appropriately. -function CurrentStatusLogEntry:GetDataPairs() - +---@param earliest_first boolean set to true if your sort order is to show the earlist first dates +---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. +function CurrentStatusLogEntry:GetDataPairs( earliest_first ) local player = Character.persistent.player return { @@ -122,17 +200,27 @@ function CurrentStatusLogEntry:GetDataPairs() } end +---@class SystemLogEntry : LogEntry +---@field systemp SystemPath The system in question +---@field arrtime number|nil The time of arrival in the system, nil if this is an exit log +---@field depime number|nil The time of leaving the system, nil if this is an entry log SystemLogEntry = utils.class("FlightLog.LogEntry.System", LogEntry) -function SystemLogEntry:Constructor( index, systemp, arrtime, deptime, entry ) +---@param systemp SystemPath The system in question +---@param arrtime number|nil The time of arrival in the system, nil if this is an exit log +---@param depime number|nil The time of leaving the system, nil if this is an entry log +---@param entry string The user entered custom test for this entry +function SystemLogEntry:Constructor( systemp, arrtime, deptime, entry ) + + local sort_date if nil == arrtime then sort_date = deptime else sort_date = arrtime end - LogEntry.Constructor( self, index, sort_date, entry ) + LogEntry.Constructor( self, sort_date, entry ) self.systemp = systemp self.arrtime = arrtime @@ -140,31 +228,71 @@ function SystemLogEntry:Constructor( index, systemp, arrtime, deptime, entry ) end +---@return integer A unique integer for this specific type +function SystemLogEntry.GetSerializationIndex() + return 0 +end + +---@return any[] An array of the elements as they will be serialized +function SystemLogEntry:GetSerializationElements() + return { self.systemp, self.arrtime, self.deptime, self.entry } +end + +--- A static function to create an entry from the elements that have been serialized +--- For the latest version will be the opposite of GetSerializationElements() +---@param elem any[] An array of elements used to construct +---@param version integer The version to read +---@return SystemLogEntry The newly created entry +function SystemLogEntry.CreateFromSerializationElements( elem, version ) + return SystemLogEntry.New( elem[1], elem[2], elem[3], elem[4] ) +end + +---@return string The name for this log entry type function SystemLogEntry:GetLocalizedName() return l.LOG_SYSTEM; end --- return an array of key value pairs, the key being localized and the --- value being formatted appropriately. -function SystemLogEntry:GetDataPairs() - return { - { l.ARRIVAL_DATE, self.formatDate(self.arrtime) }, - { l.DEPARTURE_DATE, self.formatDate(self.deptime) }, - { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, - { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name } - } -end +---@param earliest_first boolean set to true if your sort order is to show the earlist first dates +---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. +function SystemLogEntry:GetDataPairs( earliest_first ) + local o = {} ---@type table[] -function SystemLogEntry:UpdateEntry( entry ) - LogEntry.UpdateEntry( self, entry ) - FlightLogSystem[self.index][4] = entry + if ( earliest_first ) then + if self.arrtime then + o[#o+1] = { l.ARRIVAL_DATE, self.formatDate(self.arrtime) } + end + if self.deptime then + o[#o+1] = { l.DEPARTURE_DATE, self.formatDate(self.deptime) } + end + else + if self.deptime then + o[#o+1] = { l.DEPARTURE_DATE, self.formatDate(self.deptime) } + end + if self.arrtime then + o[#o+1] = { l.ARRIVAL_DATE, self.formatDate(self.arrtime) } + end + end + o[#o+1] = { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) } + o[#o+1] = { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name } + + return o end --- a custom log entry: +---@class CustomLogEntry : LogEntry +---@field systemp SystemPath The system the player is in when the log was written +---@field time number The game time the log was made, relative to the epoch +---@field money integer The amount of money the player has +---@field location string[] A number of string elements that can be compsed to create a localized description of the location. See composeLocationString CustomLogEntry = utils.class("FlightLog.LogEntry.Custom", LogEntry) -function CustomLogEntry:Constructor( index, systemp, time, money, location, entry ) - LogEntry.Constructor( self, index, time, entry ) + +---@param systemp SystemPath The system in question +---@param time number The game time the log was made, relative to the epoch +---@param money integer The amount of money the player has +---@param location string[] A number of string elements that can be compsed to create a localized description of the location. See composeLocationString +---@param entry string The user entered custom test for this entry +function CustomLogEntry:Constructor( systemp, time, money, location, entry ) + LogEntry.Constructor( self, time, entry, true ) self.systemp = systemp self.time = time @@ -172,13 +300,32 @@ function CustomLogEntry:Constructor( index, systemp, time, money, location, entr self.location = location end +---@return integer A unique integer for this specific type +function CustomLogEntry.GetSerializationIndex() + return 1 +end + +---@return any[] An array of the elements as they will be serialized +function CustomLogEntry:GetSerializationElements() + return { self.systemp, self.time, self.money, self.location, self.entry } +end + +--- A static function to create an entry from the elements that have been serialized +--- For the latest version will be the opposite of GetSerializationElements() +---@param elem any[] An array of elements used to construct +---@param version integer The version to read +function CustomLogEntry.CreateFromSerializationElements( elem, version ) + return CustomLogEntry.New( elem[1], elem[2], elem[3], elem[4], elem[5] ) +end + +---@return string The name for this log entry type function CustomLogEntry:GetLocalizedName() return l.LOG_CUSTOM; end --- return an array of key value pairs, the key being localized and the --- value being formatted appropriately. -function CustomLogEntry:GetDataPairs() +---@param earliest_first boolean set to true if your sort order is to show the earlist first dates +---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. +function CustomLogEntry:GetDataPairs( earliest_first ) return { { l.DATE, self.formatDate(self.time) }, { l.LOCATION, self.composeLocationString(self.location) }, @@ -188,53 +335,70 @@ function CustomLogEntry:GetDataPairs() } end - -function CustomLogEntry:UpdateEntry( entry ) - LogEntry.UpdateEntry( self, entry ) - FlightLogCustom[self.index][5] = entry -end - --- return true if this has a Delete() method +---@return boolean true if this has a Delete() method function CustomLogEntry:SupportsDelete() return true end +---Delete this entry +---@return nil function CustomLogEntry:Delete() - table.remove(FlightLogCustom, index) - - -- TODO: now all the indexes are wrong. - -- we should do away with them all! + TotalDefaultElements = TotalDefaultElements - 1 + utils.remove_elem( FlightLogData, self ) end - --- a station log entry: +---@class StationLogEntry : LogEntry +---@field systemp SystemPath The system the player is in when the log was written +---@field time deptime The game time the log was made, on departure from teh system, relative to the epoch +---@field money integer The amount of money the player has StationLogEntry = utils.class("FlightLog.LogEntry.Station", LogEntry) -function StationLogEntry:Constructor( index, systemp, deptime, money, entry ) - LogEntry.Constructor( self, index, deptime, entry ) +---@param systemp SystemPath The system the player is in when the log was written +---@param time deptime The game time the log was made, on departure from teh system, relative to the epoch +---@param money integer The amount of money the player has +---@param entry string The user entered custom test for this entry +function StationLogEntry:Constructor( systemp, deptime, money, entry ) + LogEntry.Constructor( self, deptime, entry ) self.systemp = systemp self.deptime = deptime self.money = money end +---@return integer A unique integer for this specific type +function StationLogEntry.GetSerializationIndex() + return 2 +end + +---@return any[] An array of the elements as they will be serialized +function StationLogEntry:GetSerializationElements() + return { self.systemp, self.deptime, self.money, self.entry } +end + +--- A static function to create an entry from the elements that have been serialized +--- For the latest version will be the opposite of GetSerializationElements() +---@param elem any[] An array of elements used to construct +---@param version integer The version to read +function StationLogEntry.CreateFromSerializationElements( elem, version ) + return StationLogEntry.New( elem[1], elem[2], elem[3], elem[4] ) +end + +---@return string The name for this log entry type function StationLogEntry:GetLocalizedName() return l.LOG_STATION; end --- return an array of key value pairs, the key being localized and the --- value being formatted appropriately. -function StationLogEntry:GetDataPairs() +---@param earliest_first boolean set to true if your sort order is to show the earlist first dates +---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. +function StationLogEntry:GetDataPairs( earliest_first ) local station_type = "FLIGHTLOG_" .. self.systemp:GetSystemBody().type - return { { l.DATE, self.formatDate(self.deptime) }, { l.STATION, string.interp(l[station_type], { primary_info = self.systemp:GetSystemBody().name, secondary_info = self.systemp:GetSystemBody().parent.name }) }, --- { l.LOCATION, self.systemp:GetSystemBody().parent.name }, { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name }, { l.CASH, Format.Money(self.money) }, @@ -242,11 +406,6 @@ function StationLogEntry:GetDataPairs() end -function StationLogEntry:UpdateEntry( entry ) - LogEntry.UpdateEntry( self, entry ) - FlightLogStation[self.index][4] = entry -end - local FlightLog FlightLog = { @@ -254,273 +413,6 @@ FlightLog = { -- Group: Methods -- -- --- Method: GetSystemPaths --- --- Returns an iterator returning a SystemLogEntry object for each system visited --- by the player, backwards in turn, starting with the most recent. If count --- is specified, returns no more than that many systems. --- --- > iterator = FlightLog.GetSystemPaths(count) --- --- Parameters: --- --- count - Optional. The maximum number of systems to return. --- --- Return: --- --- iterator - A function which will generate the paths from the log, returning --- one each time it is called until it runs out, after which it --- returns nil. It also returns, as secondary and tertiary values, --- the game times at shich the system was entered and left. --- --- Example: --- --- Print the names and departure times of the last five systems visited by --- the player --- --- > for entry in FlightLog.GetSystemPaths(5) do --- > print(entry.systemp:GetStarSystem().name, Format.Date(entry.deptime)) --- > end - - GetSystemPaths = function (maximum) - local counter = 0 - maximum = maximum or FlightLogSystemQueueLength - return function () - if counter < maximum then - counter = counter + 1 - if FlightLogSystem[counter] then - - local entry = SystemLogEntry.New( - counter, - FlightLogSystem[counter][1], - FlightLogSystem[counter][2], - FlightLogSystem[counter][3], - FlightLogSystem[counter][4] ) - - return entry - - end - end - return nil - end - end, - --- --- Method: GetStationPaths --- --- Returns an iterator returning a StationLogEntry object for each station visited --- by the player, backwards in turn, starting with the most recent. If count --- is specified, returns no more than that many stations. --- --- > iterator = FlightLog.GetStationPaths(count) --- --- Parameters: --- --- count - Optional. The maximum number of systems to return. --- --- Return: --- --- iterator - A function which will generate the StationLogEntry from the log, --- returning one each time it is called until it runs out, after --- which it returns nil. --- --- Example: --- --- Print the names and arrival times of the last five stations visited by --- the player --- --- > for entry in FlightLog.GetStationPaths(5) do --- > print(entry.systemp:GetSystemBody().name, Format.Date(entry.deptime)) --- > end - - GetStationPaths = function (maximum) - local counter = 0 - maximum = maximum or FlightLogStationQueueLength - return function () - if counter < maximum then - counter = counter + 1 - if FlightLogStation[counter] then - return StationLogEntry.New( - counter, - FlightLogStation[counter][1], - FlightLogStation[counter][2], - FlightLogStation[counter][3], - FlightLogStation[counter][4] ) - end - end - return nil, nil, nil, nil - end - end, - --- --- Method: UpdateStationEntry --- --- Update the free text field in station log. --- --- > UpdateStationEntry(index, entry) --- --- Parameters: --- --- index - Index in log, 1 being most recent station docked with --- entry - New text string to insert instead --- --- Example: --- --- Replace note for the second most recent station docked with --- --- > UpdateStationEntry(2, "This was a smelly station") --- - - UpdateStationEntry = function (index, entry) - FlightLogStation[index][4] = entry - end, - --- --- Method: GetPreviousSystemPath --- --- Returns a SystemPath object that points to the star system where the --- player was before jumping to this one. If none is on record (such as --- before any hyperjumps have been made) it returns nil. --- --- > path = FlightLog.GetPreviousSystemPath() --- --- Return: --- --- path - a SystemPath object --- --- Availability: --- --- alpha 20 --- --- Status: --- --- experimental --- - - GetPreviousSystemPath = function () - if FlightLogSystem[2] then - return FlightLogSystem[2][1] - else return nil end - end, - --- --- Method: GetPreviousStationPath --- --- Returns a SystemPath object that points to the starport most recently --- visited. If the player is currently docked, then the starport prior to --- the present one (which might be the same one, if the player launches --- and lands in the same port). If none is on record (such as before the --- player has ever launched) it returns nil. --- --- > path = FlightLog.GetPreviousStationPath() --- --- Return: --- --- path - a SystemPath object --- --- Availability: --- --- alpha 20 --- --- Status: --- --- experimental --- - - GetPreviousStationPath = function () - if FlightLogStation[1] then - return FlightLogStation[1][1] - else return nil end - end, - - - --- --- Method: GetCustomEntry --- --- Returns an iterator returning a CustomLogEntry, backwards in turn, --- starting with the most recent for each log entry the player has made. --- If count is specified, returns no more than that many entries. --- --- > iterator = FlightLog.GetCustomEntry(count) --- --- Parameters: --- --- count - Optional. The maximum number of entries to return. --- --- Return: --- --- iterator - A function which will generate the entries from the --- log, returning one each time it is called until it --- runs out, after which it returns nil. Each entry is --- a CustomLogEntry --- --- Example: --- --- > for entry in FlightLog.GetCustomEntry(5) do --- > print(entry.location[1], entry.location[2], Format.Date(entry.deptime)) --- > end --- - - GetCustomEntry = function (maximum) - local counter = 0 - maximum = maximum or #FlightLogCustom - return function () - if counter < maximum then - counter = counter + 1 - if FlightLogCustom[counter] then - return CustomLogEntry.New( - counter, - FlightLogCustom[counter][1], --path - FlightLogCustom[counter][2], --time - FlightLogCustom[counter][3], --money - FlightLogCustom[counter][4], --location - FlightLogCustom[counter][5] --manual entry - ) - end - end - return - end - end, - --- --- Method: UpdateCustomEntry --- --- Update the free text field with new entry. Allows the player to --- change the original text entry. --- --- > FlightLog.GetCustomEntry(index, entry) --- --- Parameters: --- --- index - Position in log, 1 being most recent --- entry - String of new text to replace the original with --- --- Example: --- --- > FlightLog.UpdateCustomEntry(2, "Earth is an overrated spot") --- - - UpdateCustomEntry = function (index, entry) - FlightLogCustom[index][5] = entry - end, - --- --- Method: DeleteCustomEntry --- --- Remove an entry. --- --- > FlightLog.DeleteCustomEntry(index) --- --- Parameters: --- --- index - Position in log to remove, 1 being most recent --- - - DeleteCustomEntry = function (index) - table.remove(FlightLogCustom, index) - end, - -- -- Method: MakeCustomEntry -- @@ -577,8 +469,7 @@ FlightLog = { location = {state, sysname} end - table.insert(FlightLogCustom,1, - {path, Game.time, Game.player:GetMoney(), location, text}) + table.insert(FlightLogData,1, CustomLogEntry.New( path, Game.time, Game.player:GetMoney(), location, text ) ) end, } @@ -602,60 +493,102 @@ FlightLog = { -- > for entry in FlightLog.GetLogEntries( { "Custom", "System", "Station" ) do -- > print( entry.GetType(), entry.entry ) -- > end -function FlightLog:GetLogEntries(types, maximum) +function FlightLog:GetLogEntries(types, maximum, earliest_first) -- TODO: actually just store a list of all of them as they are at startup local type_set = utils.set.New(types) - local all_entries = {} - - if nil == types or type_set:contains( 'Custom' ) then - for entry in self.GetCustomEntry() do - table.insert( all_entries, entry ) - end + -- note regardless of sort order, current status always comes first. + local currentStatus = nil + if nil == types or type_set:contains( "CurrentStatus" ) then + currentStatus = CurrentStatusLogEntry.New() end - if nil == types or type_set:contains( 'Station' ) then - for entry in self.GetStationPaths() do - table.insert( all_entries, entry ) + local counter = 0 + maximum = maximum or #FlightLogData + return function () + if currentStatus then + local t = currentStatus + currentStatus = nil + return t end - end + while counter < maximum do + counter = counter + 1 - if nil == types or type_set:contains( 'System' ) then - for entry in self.GetSystemPaths() do - table.insert( all_entries, entry ) + local v + if earliest_first then + v = FlightLogData[(#FlightLogData+1) - counter] + else + v = FlightLogData[counter] + end + -- TODO: Can we map the types to serialization indexes and check these + -- as they may be faster than the string manipulation comapare stuff. + if nil == types or type_set:contains( v:GetType() ) then + return v + end end + return nil end +end +--- If there are two system eventsm back to back, starting at first_index +--- entering and leaving the same system, it will put them together +--- as a single system event +--- +---@param first_index integer The index of the first element in the array (so the latest event) to collapse +local function ConsiderCollapseSystemEventPair( first_index ) + -- TODO: make this global (ideally const, but our Lua version doesn't support that) + local system_idx = SystemLogEntry.GetSerializationIndex(); ---@type integer + local second = FlightLogData[first_index] + local first = FlightLogData[first_index+1] - local function sortf( a, b ) - return a.sort_date > b.sort_date - end + if ( second:IsCustom() ) then return end + if ( second.GetSerializationIndex() ~= system_idx ) then return end + ---@cast second SystemLogEntry + -- is the latest one actually an arrival event, or already collapsed. + if ( second.arrtime ~= nil ) then return end +-- local first = FlightLogData[first_index+1] + if ( first:IsCustom() ) then return end + if ( first.GetSerializationIndex() ~= system_idx ) then return end + ---@cast first SystemLogEntry - table.sort( all_entries, sortf ) + -- is the first one actually a departure event or already collapsed + if ( first.deptime ~= nil ) then return end - -- note regardless of sort order, current status always comes first. - local currentStatus = nil - if nil == types or type_set:contains( "CurrentStatus" ) then - currentStatus = CurrentStatusLogEntry.New() + if ( first.systemp ~= second.systemp ) then return end + + second.arrtime = first.arrtime + table.remove( FlightLogData, first_index+1 ) + +end + +-- This will run through the array of events and if there are two system events +-- back to back, entering and leaving the same system, it will put them together +-- as a single system event +local function CollapseSystemEvents() + for i = #FlightLogData-1, 1, -1 do + ConsiderCollapseSystemEventPair( i ) end +end - local counter = 0 - maximum = maximum or #all_entries - return function () - if currentStatus then - t = currentStatus - currentStatus = nil - return t - end - if counter < maximum then - counter = counter + 1 - return all_entries[counter] +-- This will run through the array of events and remove any non custom ones +-- if we have exceeded our maximum size, until that maximum size is reattained. +local function TrimLogSize() + if TotalDefaultElements > MaxTotalDefaultElements then + CollapseSystemEvents() + while TotalDefaultElements > MaxTotalDefaultElements do + for i = #FlightLogData, 1, -1 do + local v = FlightLogData[i] + if not v:IsCustom() then + table.remove( FlightLogData, i ) + TotalDefaultElements = TotalDefaultElements-1 + end + end end - return nil + CollapseSystemEvents() end end @@ -665,28 +598,26 @@ end -- onLeaveSystem local AddSystemDepartureToLog = function (ship) if not ship:IsPlayer() then return end - FlightLogSystem[1][3] = Game.time - while #FlightLogSystem > FlightLogSystemQueueLength do - table.remove(FlightLogSystem,FlightLogSystemQueueLength + 1) - end + + table.insert( FlightLogData, 1, SystemLogEntry.New( Game.system.path, nil, Game.time, nil ) ); + ConsiderCollapseSystemEventPair( 1 ) + TrimLogSize() end -- onEnterSystem local AddSystemArrivalToLog = function (ship) if not ship:IsPlayer() then return end - table.insert(FlightLogSystem,1,{Game.system.path,Game.time,nil,""}) - while #FlightLogSystem > FlightLogSystemQueueLength do - table.remove(FlightLogSystem,FlightLogSystemQueueLength + 1) - end + + table.insert( FlightLogData, 1, SystemLogEntry.New( Game.system.path, Game.time, nil, nil ) ); + TrimLogSize() end -- onShipDocked local AddStationToLog = function (ship, station) if not ship:IsPlayer() then return end - table.insert(FlightLogStation,1,{station.path, Game.time, Game.player:GetMoney(), ""}) - while #FlightLogStation > FlightLogStationQueueLength do - table.remove(FlightLogStation,FlightLogStationQueueLength + 1) - end + + table.insert( FlightLogData, 1, StationLogEntry.New( station.path, Game.time, Game.player:GetMoney(), nil ) ); + TrimLogSize() end -- LOADING AND SAVING @@ -694,27 +625,72 @@ end local loaded_data local onGameStart = function () - if loaded_data and loaded_data.Version >= 1 then - FlightLogSystem = loaded_data.System - FlightLogStation = loaded_data.Station - FlightLogCustom = loaded_data.Custom - else - table.insert(FlightLogSystem,1,{Game.system.path,nil,nil,""}) + + if loaded_data and loaded_data.Version == 1 then + + for _, v in pairs( loaded_data.System ) do + local entryLog = SystemLogEntry.CreateFromSerializationElements( { v[1], v[2], nil, v[4] }, 1 ) + local exitLog = SystemLogEntry.CreateFromSerializationElements( { v[1], nil, v[3], v[4] }, 1 ) + + if (exitLog.deptime ~= nil) then + FlightLogData[#FlightLogData+1] = exitLog + end + if (entryLog.arrtime ~= nil) then + FlightLogData[#FlightLogData+1] = entryLog + end + end + + for _, v in pairs( loaded_data.Station ) do + FlightLogData[#FlightLogData+1] = StationLogEntry.CreateFromSerializationElements( v, 1 ) + end + + for _, v in pairs( loaded_data.Custom ) do + FlightLogData[#FlightLogData+1] = CustomLogEntry.CreateFromSerializationElements( v, 1 ) + end + + local function sortf( a, b ) + return a.sort_date > b.sort_date + end + + table.sort( FlightLogData, sortf ) + + CollapseSystemEvents() + + elseif loaded_data and loaded_data.Version > 1 then + + local loader_funcs = {} + loader_funcs[SystemLogEntry.GetSerializationIndex()] = SystemLogEntry.CreateFromSerializationElements + loader_funcs[StationLogEntry.GetSerializationIndex()] = StationLogEntry.CreateFromSerializationElements + loader_funcs[CustomLogEntry.GetSerializationIndex()] = CustomLogEntry.CreateFromSerializationElements + + for _, p in pairs( loaded_data.Data ) do + for type, v in pairs(p) do + local lf = loader_funcs[type] + local val = lf(v, loaded_data.Version); + FlightLogData[#FlightLogData+1] = val + end + end end loaded_data = nil end local onGameEnd = function () - FlightLogSystem = {} - FlightLogStation = {} - FlightLogCustom = {} + FlightLogData = {} + TotalDefaultElements = 0 end local serialize = function () - return { System = FlightLogSystem, - Station = FlightLogStation, - Custom = FlightLogCustom, - Version = 1 -- version for backwards compatibility + + local source = FlightLogData + local SaveData = {} + + for _, v in pairs( source ) do + v:AddToSerializationTable( SaveData ) + end + + return { + Data = SaveData, + Version = 2 -- version for backwards compatibility } end diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index fb185d99199..9ef5db3b32c 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -26,6 +26,7 @@ local include_player_info = true local include_custom_log = true local include_station_log = true local include_system_log = true +local earliest_first = false local export_html = true local function getIncludedSet() @@ -38,13 +39,12 @@ local function getIncludedSet() return o; end - function writeLogEntry( entry, formatter, write_header ) if write_header then formatter:write( entry:GetLocalizedName() ):newline() end - for _, pair in pairs( entry:GetDataPairs() ) do + for _, pair in pairs( entry:GetDataPairs( earliest_first ) ) do formatter:headerText( pair[1], pair[2] ) end end @@ -150,14 +150,14 @@ entering_text = false -- Display Entry text, and Edit button, to update flightlog function inputText(entry, counter, entering_text, str, clicked) - if #entry.entry > 0 then - ui_formatter:headerText(l.ENTRY, entry.entry, true) + if entry:HasEntry() then + ui_formatter:headerText(l.ENTRY, entry:GetEntry(), true) end if clicked or entering_text == counter then ui.spacing() ui.pushItemWidth(-1.0) - local updated_entry, return_pressed = ui.inputText("##" ..str..counter, entry.entry, {"EnterReturnsTrue"}) + local updated_entry, return_pressed = ui.inputText("##" ..str..counter, entry:GetEntry(), {"EnterReturnsTrue"}) ui.popItemWidth() entering_text = counter if return_pressed then @@ -184,12 +184,12 @@ local function renderLog( formatter ) local counter = 0 local was_clicked = false - for entry in FlightLog:GetLogEntries(getIncludedSet()) do + for entry in FlightLog:GetLogEntries(getIncludedSet(), nil, earliest_first ) do counter = counter + 1 writeLogEntry( entry, formatter, true ) - if entry:HasEntry() then + if entry:CanHaveEntry() then ::input:: entering_text = inputText(entry, counter, entering_text, "custom", was_clicked) @@ -258,12 +258,12 @@ local function exportLogs() formatter:open( io.open( log_filename, "w" ) ) - for entry in FlightLog:GetLogEntries(getIncludedSet()) do + for entry in FlightLog:GetLogEntries(getIncludedSet(),nil, earliest_first) do writeLogEntry(entry, formatter, true) - if #entry.entry > 0 then - formatter:headerText(l.ENTRY, entry.entry, true) + if (entry:HasEntry()) then + formatter:headerText(l.ENTRY, entry:GetEntry(), true) end formatter:separator() end @@ -281,6 +281,7 @@ local function displayFilterOptions() c,include_custom_log = checkbox(l.LOG_CUSTOM, include_custom_log) c,include_station_log = checkbox(l.LOG_STATION, include_station_log) c,include_system_log = checkbox(l.LOG_SYSTEM, include_system_log) + c,earliest_first = checkbox("Reverse Order", earliest_first) ui.spacing() ui.separator() ui.spacing() From ae8986e788e11975867408d055166f7bd4be41be Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Thu, 5 Oct 2023 21:41:13 +1300 Subject: [PATCH 10/19] Don't add a docking flight log when the player starts the game in a station. --- data/libs/FlightLog.lua | 16 ++++++++++++++++ data/pigui/modules/new-game-window/class.lua | 1 + 2 files changed, 17 insertions(+) diff --git a/data/libs/FlightLog.lua b/data/libs/FlightLog.lua index b981ec7e61d..bb994155179 100644 --- a/data/libs/FlightLog.lua +++ b/data/libs/FlightLog.lua @@ -23,6 +23,10 @@ local Lang = require 'Lang' local l = Lang.GetResource("ui-core") -- end of formating / localisation stuff +---@type boolean|nil If true then we've just started a game so don't record the first docking callback +local skip_first_docking = nil + + -- default values (private) ---@type integer local MaxTotalDefaultElements = 3000 @@ -474,6 +478,11 @@ FlightLog = { } + + +function FlightLog.SkipFirstDocking() + skip_first_docking = true +end -- -- Method: GetLogEntries -- @@ -616,6 +625,13 @@ end local AddStationToLog = function (ship, station) if not ship:IsPlayer() then return end + -- could check the game time and see if it's the same as the last custom event + -- and there is nothing else in the list and avoud the phantom 'first docking' + -- that way too. + if skip_first_docking then + skip_first_docking = nil + return + end table.insert( FlightLogData, 1, StationLogEntry.New( station.path, Game.time, Game.player:GetMoney(), nil ) ); TrimLogSize() end diff --git a/data/pigui/modules/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index efdcbe31889..01d65a2be3c 100644 --- a/data/pigui/modules/new-game-window/class.lua +++ b/data/pigui/modules/new-game-window/class.lua @@ -170,6 +170,7 @@ local function startGame(gameParams) -- XXX horrible hack here to avoid paying a spawn-in docking fee player:setprop("is_first_spawn", true) + FlightLog.SkipFirstDocking() FlightLog.MakeCustomEntry(gameParams.player.log) if gameParams.autoExec then From 5827a708517f9378539024388676fba9c5c1f8ad Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Fri, 6 Oct 2023 20:56:24 +1300 Subject: [PATCH 11/19] Move FlightLog.lua to moduels/FlightLog/FlightLog Do move first then we'll start splitting it up to keep git history nice. --- data/{libs => modules/FlightLog}/FlightLog.lua | 0 data/pigui/modules/info-view/06-flightlog.lua | 2 +- data/pigui/modules/new-game-window/class.lua | 2 +- data/pigui/views/mainmenu.lua | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename data/{libs => modules/FlightLog}/FlightLog.lua (100%) diff --git a/data/libs/FlightLog.lua b/data/modules/FlightLog/FlightLog.lua similarity index 100% rename from data/libs/FlightLog.lua rename to data/modules/FlightLog/FlightLog.lua diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index 9ef5db3b32c..1cf627f8e50 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -4,7 +4,7 @@ local ui = require 'pigui' local InfoView = require 'pigui.views.info-view' local Lang = require 'Lang' -local FlightLog = require 'FlightLog' +local FlightLog = require 'modules.FlightLog.FlightLog' local Format = require 'Format' local Color = _G.Color local Vector2 = _G.Vector2 diff --git a/data/pigui/modules/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index 01d65a2be3c..009d2978c99 100644 --- a/data/pigui/modules/new-game-window/class.lua +++ b/data/pigui/modules/new-game-window/class.lua @@ -8,7 +8,7 @@ local msgbox = require 'pigui.libs.message-box' local Character = require 'Character' local Commodities = require 'Commodities' local Equipment = require 'Equipment' -local FlightLog = require 'FlightLog' +local FlightLog = require 'modules.FlightLog.FlightLog' local ModalWindow = require 'pigui.libs.modal-win' local ModelSkin = require 'SceneGraph.ModelSkin' local ShipDef = require "ShipDef" diff --git a/data/pigui/views/mainmenu.lua b/data/pigui/views/mainmenu.lua index 1ae3cd50e3e..41e43e359b4 100644 --- a/data/pigui/views/mainmenu.lua +++ b/data/pigui/views/mainmenu.lua @@ -8,7 +8,7 @@ local ShipDef = require 'ShipDef' local Equipment = require 'Equipment' local MusicPlayer = require 'modules.MusicPlayer' local Lang = require 'Lang' -local FlightLog = require 'FlightLog' +local FlightLog = require 'modules.FlightLog.FlightLog' local Character = require 'Character' local Vector2 = _G.Vector2 local NewGameWindow = require("pigui.modules.new-game-window.class") From b7d6fd4c1bf3591a0b1af5d0dddb60cc62d960fb Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Fri, 6 Oct 2023 21:30:36 +1300 Subject: [PATCH 12/19] Start split --- data/modules/FlightLog/{FlightLog.lua => FlightLogEntries.lua} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename data/modules/FlightLog/{FlightLog.lua => FlightLogEntries.lua} (100%) diff --git a/data/modules/FlightLog/FlightLog.lua b/data/modules/FlightLog/FlightLogEntries.lua similarity index 100% rename from data/modules/FlightLog/FlightLog.lua rename to data/modules/FlightLog/FlightLogEntries.lua From 52d9c9f6f43f8ecd72aefd16b52a0b35488ac0a6 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Fri, 6 Oct 2023 21:32:50 +1300 Subject: [PATCH 13/19] temp move --- data/modules/FlightLog/{FlightLog.lua => copy} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename data/modules/FlightLog/{FlightLog.lua => copy} (100%) diff --git a/data/modules/FlightLog/FlightLog.lua b/data/modules/FlightLog/copy similarity index 100% rename from data/modules/FlightLog/FlightLog.lua rename to data/modules/FlightLog/copy From 43a12a6280e0b6489e1125953f6368a46b679d2f Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Fri, 6 Oct 2023 21:34:32 +1300 Subject: [PATCH 14/19] Move the temp back, preserving history on both --- data/modules/FlightLog/{copy => FlightLog.lua} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename data/modules/FlightLog/{copy => FlightLog.lua} (100%) diff --git a/data/modules/FlightLog/copy b/data/modules/FlightLog/FlightLog.lua similarity index 100% rename from data/modules/FlightLog/copy rename to data/modules/FlightLog/FlightLog.lua From 5782fb40dcf0b1fe7776428efb20a5855243c738 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Fri, 6 Oct 2023 22:22:32 +1300 Subject: [PATCH 15/19] Split FlightLog into FlightLog and FlightLogEntries --- data/modules/FlightLog/FlightLog.lua | 420 +----------------- data/modules/FlightLog/FlightLogEntries.lua | 459 +++----------------- 2 files changed, 87 insertions(+), 792 deletions(-) diff --git a/data/modules/FlightLog/FlightLog.lua b/data/modules/FlightLog/FlightLog.lua index bb994155179..545fc2564b6 100644 --- a/data/modules/FlightLog/FlightLog.lua +++ b/data/modules/FlightLog/FlightLog.lua @@ -17,399 +17,20 @@ local utils = require 'utils' local Character = require 'Character' --- required for formatting / localisation -local ui = require 'pigui' -local Lang = require 'Lang' -local l = Lang.GetResource("ui-core") --- end of formating / localisation stuff +local FlightLogEntry = require 'modules.FlightLog.FlightLogEntries' + ---@type boolean|nil If true then we've just started a game so don't record the first docking callback local skip_first_docking = nil - -- default values (private) ---@type integer local MaxTotalDefaultElements = 3000 --- how many default (so not custom) elements do we have ----@type integer -local TotalDefaultElements = 0 -- private data - the log itself ----@type LogEntry[] +---@type FlightLogEntry.Base[] local FlightLogData = {} ----@class LogEntry ----A generic log entry: ----@field protected sort_date number Seconds since the epoch in game time, used to sort when loading old style ----@field protected entry string User entered text associated with the entry ----@field protected always_custom boolean|nil Is this always treated as a custom entry (so not auto deleted) -LogEntry = utils.class("FlightLog.LogEntry.Base") - ----@return string Description of this type -function LogEntry:GetType() - local full_type = self.Class().meta.class - return string.sub( full_type, #"FlightLog.LogEntry."+1, #full_type ) -end - ----@return boolean true if this is considered to have an entry -function LogEntry:CanHaveEntry() - return true -end - ----@return boolean true if this has an entry -function LogEntry:HasEntry() - return self.entry and #self.entry > 0 -end - ----@return string The user provided entry or an empty string if there isn't one. -function LogEntry:GetEntry() - if not self.entry then return "" end - return self.entry -end - ----@return boolean true if this has a Delete() method -function LogEntry:SupportsDelete() - return false -end - - ----@return boolean True if this log entry should be considered custom and so not auto deleted -function LogEntry:IsCustom() - if self.always_custom then return true end - - if not self.entry then return false end - if self.entry == "" then return false end - return true -end - --- The serialization table is an array --- the elements of the array are a key-value pair --- the key is the serialization index (so the type this log entry is) --- the value is an array of data, to construct the table from. ----@param table table ----@return nil -function LogEntry:AddToSerializationTable( table ) - local v = {} - v[self.GetSerializationIndex()] = self:GetSerializationElements() - table[#table+1] = v -end - ----@return integer A unique integer for this specific type -function LogEntry.GetSerializationIndex() - return -1 -end - ----@return any[] An array of the elements as they will be serialized -function LogEntry:GetSerializationElements() - return {} -end - - ----@return string The name for this log entry type -function LogEntry:GetLocalizedName() - return "Error" -end - ----@param earliest_first boolean set to true if your sort order is to show the earlist first dates ----@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function LogEntry:GetDataPairs( earliest_first ) - return { { "ERROR", "This should never be seen" } } -end - ----@param entry string A user provided description of the event. ----If non nil/empty this will cause the entry to be considered custom and not automatically deleted ----@return nil -function LogEntry:UpdateEntry( entry ) - - if self:IsCustom() then - TotalDefaultElements = TotalDefaultElements-1 - end - - if entry and #entry == 0 then entry = nil end - self.entry = entry - - if self:IsCustom() then - TotalDefaultElements = TotalDefaultElements+1 - end - -end - ----@param sort_date number The date to sort by (from epoch) ----@param entry string|nil The user entered custom test for this entry ----@param always_custom boolean Is this always treated as a custom entry -function LogEntry:Constructor( sort_date, entry, always_custom ) - -- a date that can be used to sort entries on TODO: remove this - self.sort_date = sort_date - -- the entry text associated with this log entry - if entry and #entry == 0 then entry = nil end - self.entry = entry - self.always_custom = always_custom - if self:IsCustom() then - TotalDefaultElements = TotalDefaultElements+1 - end -end - - ---- convenience helper function ---- Sometimes date is empty, e.g. departure date prior to departure ---- TODO: maybe not return this at all then! ---- ----@param date number The date since the epoch ---- ----@return string The date formatted -function LogEntry.formatDate(date) - return date and Format.Date(date) or nil -end - ---- Based on flight state, compose a reasonable string for location ---- TODO: consider a class to represent, construct, store and format this ----@param location string[] Array of string info, the first one is the ----@return string The formatted composite location. -function LogEntry.composeLocationString(location) - return string.interp(l["FLIGHTLOG_"..location[1]], - { primary_info = location[2], - secondary_info = location[3] or "", - tertiary_info = location[4] or "",}) -end - ----@class CurrentStatusLogEngtry : LogEntry ---- Does not have any members, it grabs the current status live whenever requested -CurrentStatusLogEntry = utils.class("FilghtLog.LogEntry.CurrentStatus", LogEntry ) - ----@return boolean true if this is considered to have an entry -function CurrentStatusLogEntry:CanHaveEntry() - return false -end - -function CurrentStatusLogEntry:Constructor() - LogEntry.Constructor( self, Game.time, nil, true ) -end - ----@return string The name for this log entry type -function CurrentStatusLogEntry:GetLocalizedName() - return l.PERSONAL_INFORMATION; -end - ----@param earliest_first boolean set to true if your sort order is to show the earlist first dates ----@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function CurrentStatusLogEntry:GetDataPairs( earliest_first ) - local player = Character.persistent.player - - return { - { l.NAME_PERSON, player.name }, - -- TODO: localize - { "Title", player.title }, - { l.RATING, l[player:GetCombatRating()] }, - { l.KILLS, string.format('%d',player.killcount) } - } -end - ----@class SystemLogEntry : LogEntry ----@field systemp SystemPath The system in question ----@field arrtime number|nil The time of arrival in the system, nil if this is an exit log ----@field depime number|nil The time of leaving the system, nil if this is an entry log -SystemLogEntry = utils.class("FlightLog.LogEntry.System", LogEntry) - - ----@param systemp SystemPath The system in question ----@param arrtime number|nil The time of arrival in the system, nil if this is an exit log ----@param depime number|nil The time of leaving the system, nil if this is an entry log ----@param entry string The user entered custom test for this entry -function SystemLogEntry:Constructor( systemp, arrtime, deptime, entry ) - - local sort_date - if nil == arrtime then - sort_date = deptime - else - sort_date = arrtime - end - - LogEntry.Constructor( self, sort_date, entry ) - - self.systemp = systemp - self.arrtime = arrtime - self.deptime = deptime - -end - ----@return integer A unique integer for this specific type -function SystemLogEntry.GetSerializationIndex() - return 0 -end - ----@return any[] An array of the elements as they will be serialized -function SystemLogEntry:GetSerializationElements() - return { self.systemp, self.arrtime, self.deptime, self.entry } -end - ---- A static function to create an entry from the elements that have been serialized ---- For the latest version will be the opposite of GetSerializationElements() ----@param elem any[] An array of elements used to construct ----@param version integer The version to read ----@return SystemLogEntry The newly created entry -function SystemLogEntry.CreateFromSerializationElements( elem, version ) - return SystemLogEntry.New( elem[1], elem[2], elem[3], elem[4] ) -end - ----@return string The name for this log entry type -function SystemLogEntry:GetLocalizedName() - return l.LOG_SYSTEM; -end - ----@param earliest_first boolean set to true if your sort order is to show the earlist first dates ----@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function SystemLogEntry:GetDataPairs( earliest_first ) - local o = {} ---@type table[] - - if ( earliest_first ) then - if self.arrtime then - o[#o+1] = { l.ARRIVAL_DATE, self.formatDate(self.arrtime) } - end - if self.deptime then - o[#o+1] = { l.DEPARTURE_DATE, self.formatDate(self.deptime) } - end - else - if self.deptime then - o[#o+1] = { l.DEPARTURE_DATE, self.formatDate(self.deptime) } - end - if self.arrtime then - o[#o+1] = { l.ARRIVAL_DATE, self.formatDate(self.arrtime) } - end - end - o[#o+1] = { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) } - o[#o+1] = { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name } - - return o -end - ----@class CustomLogEntry : LogEntry ----@field systemp SystemPath The system the player is in when the log was written ----@field time number The game time the log was made, relative to the epoch ----@field money integer The amount of money the player has ----@field location string[] A number of string elements that can be compsed to create a localized description of the location. See composeLocationString -CustomLogEntry = utils.class("FlightLog.LogEntry.Custom", LogEntry) - - ----@param systemp SystemPath The system in question ----@param time number The game time the log was made, relative to the epoch ----@param money integer The amount of money the player has ----@param location string[] A number of string elements that can be compsed to create a localized description of the location. See composeLocationString ----@param entry string The user entered custom test for this entry -function CustomLogEntry:Constructor( systemp, time, money, location, entry ) - LogEntry.Constructor( self, time, entry, true ) - - self.systemp = systemp - self.time = time - self.money = money - self.location = location -end - ----@return integer A unique integer for this specific type -function CustomLogEntry.GetSerializationIndex() - return 1 -end - ----@return any[] An array of the elements as they will be serialized -function CustomLogEntry:GetSerializationElements() - return { self.systemp, self.time, self.money, self.location, self.entry } -end - ---- A static function to create an entry from the elements that have been serialized ---- For the latest version will be the opposite of GetSerializationElements() ----@param elem any[] An array of elements used to construct ----@param version integer The version to read -function CustomLogEntry.CreateFromSerializationElements( elem, version ) - return CustomLogEntry.New( elem[1], elem[2], elem[3], elem[4], elem[5] ) -end - ----@return string The name for this log entry type -function CustomLogEntry:GetLocalizedName() - return l.LOG_CUSTOM; -end - ----@param earliest_first boolean set to true if your sort order is to show the earlist first dates ----@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function CustomLogEntry:GetDataPairs( earliest_first ) - return { - { l.DATE, self.formatDate(self.time) }, - { l.LOCATION, self.composeLocationString(self.location) }, - { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, - { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name }, - { l.CASH, Format.Money(self.money) } - } -end - ----@return boolean true if this has a Delete() method -function CustomLogEntry:SupportsDelete() - return true -end - ----Delete this entry ----@return nil -function CustomLogEntry:Delete() - TotalDefaultElements = TotalDefaultElements - 1 - utils.remove_elem( FlightLogData, self ) -end - ----@class StationLogEntry : LogEntry ----@field systemp SystemPath The system the player is in when the log was written ----@field time deptime The game time the log was made, on departure from teh system, relative to the epoch ----@field money integer The amount of money the player has -StationLogEntry = utils.class("FlightLog.LogEntry.Station", LogEntry) - ----@param systemp SystemPath The system the player is in when the log was written ----@param time deptime The game time the log was made, on departure from teh system, relative to the epoch ----@param money integer The amount of money the player has ----@param entry string The user entered custom test for this entry -function StationLogEntry:Constructor( systemp, deptime, money, entry ) - LogEntry.Constructor( self, deptime, entry ) - - self.systemp = systemp - self.deptime = deptime - self.money = money -end - ----@return integer A unique integer for this specific type -function StationLogEntry.GetSerializationIndex() - return 2 -end - ----@return any[] An array of the elements as they will be serialized -function StationLogEntry:GetSerializationElements() - return { self.systemp, self.deptime, self.money, self.entry } -end - ---- A static function to create an entry from the elements that have been serialized ---- For the latest version will be the opposite of GetSerializationElements() ----@param elem any[] An array of elements used to construct ----@param version integer The version to read -function StationLogEntry.CreateFromSerializationElements( elem, version ) - return StationLogEntry.New( elem[1], elem[2], elem[3], elem[4] ) -end - ----@return string The name for this log entry type -function StationLogEntry:GetLocalizedName() - return l.LOG_STATION; -end - ----@param earliest_first boolean set to true if your sort order is to show the earlist first dates ----@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function StationLogEntry:GetDataPairs( earliest_first ) - - local station_type = "FLIGHTLOG_" .. self.systemp:GetSystemBody().type - - return { - { l.DATE, self.formatDate(self.deptime) }, - { l.STATION, string.interp(l[station_type], - { primary_info = self.systemp:GetSystemBody().name, - secondary_info = self.systemp:GetSystemBody().parent.name }) }, - { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, - { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name }, - { l.CASH, Format.Money(self.money) }, - } -end - - local FlightLog FlightLog = { @@ -473,7 +94,7 @@ FlightLog = { location = {state, sysname} end - table.insert(FlightLogData,1, CustomLogEntry.New( path, Game.time, Game.player:GetMoney(), location, text ) ) + table.insert(FlightLogData,1, FlightLogEntry.Custom.New( path, Game.time, Game.player:GetMoney(), location, text ) ) end, } @@ -510,7 +131,7 @@ function FlightLog:GetLogEntries(types, maximum, earliest_first) -- note regardless of sort order, current status always comes first. local currentStatus = nil if nil == types or type_set:contains( "CurrentStatus" ) then - currentStatus = CurrentStatusLogEntry.New() + currentStatus = FlightLogEntry.CurrentStatus.New() end local counter = 0 @@ -547,7 +168,7 @@ end ---@param first_index integer The index of the first element in the array (so the latest event) to collapse local function ConsiderCollapseSystemEventPair( first_index ) -- TODO: make this global (ideally const, but our Lua version doesn't support that) - local system_idx = SystemLogEntry.GetSerializationIndex(); ---@type integer + local system_idx = FlightLogEntry.System.GetSerializationIndex(); ---@type integer local second = FlightLogData[first_index] local first = FlightLogData[first_index+1] @@ -586,14 +207,14 @@ end -- This will run through the array of events and remove any non custom ones -- if we have exceeded our maximum size, until that maximum size is reattained. local function TrimLogSize() - if TotalDefaultElements > MaxTotalDefaultElements then + if FlightLogEntry.TotalDefaultElements > MaxTotalDefaultElements then CollapseSystemEvents() - while TotalDefaultElements > MaxTotalDefaultElements do + while FlightLogEntry.TotalDefaultElements > MaxTotalDefaultElements do for i = #FlightLogData, 1, -1 do local v = FlightLogData[i] if not v:IsCustom() then table.remove( FlightLogData, i ) - TotalDefaultElements = TotalDefaultElements-1 + FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements-1 end end end @@ -608,7 +229,7 @@ end local AddSystemDepartureToLog = function (ship) if not ship:IsPlayer() then return end - table.insert( FlightLogData, 1, SystemLogEntry.New( Game.system.path, nil, Game.time, nil ) ); + table.insert( FlightLogData, 1, FlightLogEntry.System.New( Game.system.path, nil, Game.time, nil ) ); ConsiderCollapseSystemEventPair( 1 ) TrimLogSize() end @@ -617,7 +238,7 @@ end local AddSystemArrivalToLog = function (ship) if not ship:IsPlayer() then return end - table.insert( FlightLogData, 1, SystemLogEntry.New( Game.system.path, Game.time, nil, nil ) ); + table.insert( FlightLogData, 1, FlightLogEntry.System.New( Game.system.path, Game.time, nil, nil ) ); TrimLogSize() end @@ -632,7 +253,7 @@ local AddStationToLog = function (ship, station) skip_first_docking = nil return end - table.insert( FlightLogData, 1, StationLogEntry.New( station.path, Game.time, Game.player:GetMoney(), nil ) ); + table.insert( FlightLogData, 1, FlightLogEntry.Station.New( station.path, Game.time, Game.player:GetMoney(), nil ) ); TrimLogSize() end @@ -645,8 +266,8 @@ local onGameStart = function () if loaded_data and loaded_data.Version == 1 then for _, v in pairs( loaded_data.System ) do - local entryLog = SystemLogEntry.CreateFromSerializationElements( { v[1], v[2], nil, v[4] }, 1 ) - local exitLog = SystemLogEntry.CreateFromSerializationElements( { v[1], nil, v[3], v[4] }, 1 ) + local entryLog = FlightLogEntry.System.CreateFromSerializationElements( { v[1], v[2], nil, v[4] }, 1 ) + local exitLog = FlightLogEntry.System.CreateFromSerializationElements( { v[1], nil, v[3], v[4] }, 1 ) if (exitLog.deptime ~= nil) then FlightLogData[#FlightLogData+1] = exitLog @@ -657,11 +278,11 @@ local onGameStart = function () end for _, v in pairs( loaded_data.Station ) do - FlightLogData[#FlightLogData+1] = StationLogEntry.CreateFromSerializationElements( v, 1 ) + FlightLogData[#FlightLogData+1] = FlightLogEntry.Station.CreateFromSerializationElements( v, 1 ) end for _, v in pairs( loaded_data.Custom ) do - FlightLogData[#FlightLogData+1] = CustomLogEntry.CreateFromSerializationElements( v, 1 ) + FlightLogData[#FlightLogData+1] = FlightLogEntry.Custom.CreateFromSerializationElements( v, 1 ) end local function sortf( a, b ) @@ -675,9 +296,9 @@ local onGameStart = function () elseif loaded_data and loaded_data.Version > 1 then local loader_funcs = {} - loader_funcs[SystemLogEntry.GetSerializationIndex()] = SystemLogEntry.CreateFromSerializationElements - loader_funcs[StationLogEntry.GetSerializationIndex()] = StationLogEntry.CreateFromSerializationElements - loader_funcs[CustomLogEntry.GetSerializationIndex()] = CustomLogEntry.CreateFromSerializationElements + loader_funcs[FlightLogEntry.System.GetSerializationIndex()] = FlightLogEntry.System.CreateFromSerializationElements + loader_funcs[FlightLogEntry.Station.GetSerializationIndex()] = FlightLogEntry.Station.CreateFromSerializationElements + loader_funcs[FlightLogEntry.Custom.GetSerializationIndex()] = FlightLogEntry.Custom.CreateFromSerializationElements for _, p in pairs( loaded_data.Data ) do for type, v in pairs(p) do @@ -687,12 +308,13 @@ local onGameStart = function () end end end + loaded_data = nil end local onGameEnd = function () FlightLogData = {} - TotalDefaultElements = 0 + FlightLogEntry.TotalDefaultElements = 0 end local serialize = function () diff --git a/data/modules/FlightLog/FlightLogEntries.lua b/data/modules/FlightLog/FlightLogEntries.lua index bb994155179..ae9938fbfab 100644 --- a/data/modules/FlightLog/FlightLogEntries.lua +++ b/data/modules/FlightLog/FlightLogEntries.lua @@ -1,17 +1,10 @@ -- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt --- --- Class: FlightLog --- --- A flight log, containing the last systems and stations visited by the --- player. Can be used by scripts to find out where the player has been --- recently. +-- Entries for the FlightLog local Game = require 'Game' -local Event = require 'Event' local Format = require 'Format' -local Serializer = require 'Serializer' local utils = require 'utils' @@ -23,58 +16,50 @@ local Lang = require 'Lang' local l = Lang.GetResource("ui-core") -- end of formating / localisation stuff ----@type boolean|nil If true then we've just started a game so don't record the first docking callback -local skip_first_docking = nil +FlightLogEntry = {} - --- default values (private) ----@type integer -local MaxTotalDefaultElements = 3000 -- how many default (so not custom) elements do we have ---@type integer -local TotalDefaultElements = 0 +FlightLogEntry.TotalDefaultElements = 0 --- private data - the log itself ----@type LogEntry[] -local FlightLogData = {} ----@class LogEntry +---@class FlightLogEntry.Base ---A generic log entry: ---@field protected sort_date number Seconds since the epoch in game time, used to sort when loading old style ---@field protected entry string User entered text associated with the entry ---@field protected always_custom boolean|nil Is this always treated as a custom entry (so not auto deleted) -LogEntry = utils.class("FlightLog.LogEntry.Base") +FlightLogEntry.Base = utils.class("FlightLogEntry") ---@return string Description of this type -function LogEntry:GetType() +function FlightLogEntry.Base:GetType() local full_type = self.Class().meta.class - return string.sub( full_type, #"FlightLog.LogEntry."+1, #full_type ) + return string.sub( full_type, #"FlightLogEntry."+1, #full_type ) end ---@return boolean true if this is considered to have an entry -function LogEntry:CanHaveEntry() +function FlightLogEntry.Base:CanHaveEntry() return true end ---@return boolean true if this has an entry -function LogEntry:HasEntry() +function FlightLogEntry.Base:HasEntry() return self.entry and #self.entry > 0 end ---@return string The user provided entry or an empty string if there isn't one. -function LogEntry:GetEntry() +function FlightLogEntry.Base:GetEntry() if not self.entry then return "" end return self.entry end ---@return boolean true if this has a Delete() method -function LogEntry:SupportsDelete() +function FlightLogEntry.Base:SupportsDelete() return false end ---@return boolean True if this log entry should be considered custom and so not auto deleted -function LogEntry:IsCustom() +function FlightLogEntry.Base:IsCustom() if self.always_custom then return true end if not self.entry then return false end @@ -88,48 +73,48 @@ end -- the value is an array of data, to construct the table from. ---@param table table ---@return nil -function LogEntry:AddToSerializationTable( table ) +function FlightLogEntry.Base:AddToSerializationTable( table ) local v = {} v[self.GetSerializationIndex()] = self:GetSerializationElements() table[#table+1] = v end ---@return integer A unique integer for this specific type -function LogEntry.GetSerializationIndex() +function FlightLogEntry.Base.GetSerializationIndex() return -1 end ---@return any[] An array of the elements as they will be serialized -function LogEntry:GetSerializationElements() +function FlightLogEntry.Base:GetSerializationElements() return {} end ---@return string The name for this log entry type -function LogEntry:GetLocalizedName() +function FlightLogEntry.Base:GetLocalizedName() return "Error" end ---@param earliest_first boolean set to true if your sort order is to show the earlist first dates ---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function LogEntry:GetDataPairs( earliest_first ) +function FlightLogEntry.Base:GetDataPairs( earliest_first ) return { { "ERROR", "This should never be seen" } } end ---@param entry string A user provided description of the event. ---If non nil/empty this will cause the entry to be considered custom and not automatically deleted ---@return nil -function LogEntry:UpdateEntry( entry ) +function FlightLogEntry.Base:UpdateEntry( entry ) if self:IsCustom() then - TotalDefaultElements = TotalDefaultElements-1 + FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements-1 end if entry and #entry == 0 then entry = nil end self.entry = entry if self:IsCustom() then - TotalDefaultElements = TotalDefaultElements+1 + FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements+1 end end @@ -137,7 +122,7 @@ end ---@param sort_date number The date to sort by (from epoch) ---@param entry string|nil The user entered custom test for this entry ---@param always_custom boolean Is this always treated as a custom entry -function LogEntry:Constructor( sort_date, entry, always_custom ) +function FlightLogEntry.Base:Constructor( sort_date, entry, always_custom ) -- a date that can be used to sort entries on TODO: remove this self.sort_date = sort_date -- the entry text associated with this log entry @@ -145,7 +130,7 @@ function LogEntry:Constructor( sort_date, entry, always_custom ) self.entry = entry self.always_custom = always_custom if self:IsCustom() then - TotalDefaultElements = TotalDefaultElements+1 + FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements+1 end end @@ -157,7 +142,7 @@ end ---@param date number The date since the epoch --- ---@return string The date formatted -function LogEntry.formatDate(date) +function FlightLogEntry.Base.formatDate(date) return date and Format.Date(date) or nil end @@ -165,34 +150,34 @@ end --- TODO: consider a class to represent, construct, store and format this ---@param location string[] Array of string info, the first one is the ---@return string The formatted composite location. -function LogEntry.composeLocationString(location) +function FlightLogEntry.Base.composeLocationString(location) return string.interp(l["FLIGHTLOG_"..location[1]], { primary_info = location[2], secondary_info = location[3] or "", tertiary_info = location[4] or "",}) end ----@class CurrentStatusLogEngtry : LogEntry +---@class CurrentStatusLogEngtry : FlightLogEntry.Base --- Does not have any members, it grabs the current status live whenever requested -CurrentStatusLogEntry = utils.class("FilghtLog.LogEntry.CurrentStatus", LogEntry ) +FlightLogEntry.CurrentStatus = utils.class("FlightLogEntry.CurrentStatus", FlightLogEntry.Base ) ---@return boolean true if this is considered to have an entry -function CurrentStatusLogEntry:CanHaveEntry() +function FlightLogEntry.CurrentStatus:CanHaveEntry() return false end -function CurrentStatusLogEntry:Constructor() - LogEntry.Constructor( self, Game.time, nil, true ) +function FlightLogEntry.CurrentStatus:Constructor() + FlightLogEntry.Base.Constructor( self, Game.time, nil, true ) end ---@return string The name for this log entry type -function CurrentStatusLogEntry:GetLocalizedName() +function FlightLogEntry.CurrentStatus:GetLocalizedName() return l.PERSONAL_INFORMATION; end ---@param earliest_first boolean set to true if your sort order is to show the earlist first dates ---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function CurrentStatusLogEntry:GetDataPairs( earliest_first ) +function FlightLogEntry.CurrentStatus:GetDataPairs( earliest_first ) local player = Character.persistent.player return { @@ -204,18 +189,18 @@ function CurrentStatusLogEntry:GetDataPairs( earliest_first ) } end ----@class SystemLogEntry : LogEntry +---@class FlightLogEntry.System : FlightLogEntry.Base ---@field systemp SystemPath The system in question ---@field arrtime number|nil The time of arrival in the system, nil if this is an exit log ---@field depime number|nil The time of leaving the system, nil if this is an entry log -SystemLogEntry = utils.class("FlightLog.LogEntry.System", LogEntry) +FlightLogEntry.System = utils.class("FlightLogEntry.System", FlightLogEntry.Base) ---@param systemp SystemPath The system in question ---@param arrtime number|nil The time of arrival in the system, nil if this is an exit log ---@param depime number|nil The time of leaving the system, nil if this is an entry log ---@param entry string The user entered custom test for this entry -function SystemLogEntry:Constructor( systemp, arrtime, deptime, entry ) +function FlightLogEntry.System:Constructor( systemp, arrtime, deptime, entry ) local sort_date if nil == arrtime then @@ -224,7 +209,7 @@ function SystemLogEntry:Constructor( systemp, arrtime, deptime, entry ) sort_date = arrtime end - LogEntry.Constructor( self, sort_date, entry ) + FlightLogEntry.Base.Constructor( self, sort_date, entry ) self.systemp = systemp self.arrtime = arrtime @@ -233,12 +218,12 @@ function SystemLogEntry:Constructor( systemp, arrtime, deptime, entry ) end ---@return integer A unique integer for this specific type -function SystemLogEntry.GetSerializationIndex() +function FlightLogEntry.System.GetSerializationIndex() return 0 end ---@return any[] An array of the elements as they will be serialized -function SystemLogEntry:GetSerializationElements() +function FlightLogEntry.System:GetSerializationElements() return { self.systemp, self.arrtime, self.deptime, self.entry } end @@ -246,19 +231,19 @@ end --- For the latest version will be the opposite of GetSerializationElements() ---@param elem any[] An array of elements used to construct ---@param version integer The version to read ----@return SystemLogEntry The newly created entry -function SystemLogEntry.CreateFromSerializationElements( elem, version ) - return SystemLogEntry.New( elem[1], elem[2], elem[3], elem[4] ) +---@return FlightLogEntry.System The newly created entry +function FlightLogEntry.System.CreateFromSerializationElements( elem, version ) + return FlightLogEntry.System.New( elem[1], elem[2], elem[3], elem[4] ) end ---@return string The name for this log entry type -function SystemLogEntry:GetLocalizedName() +function FlightLogEntry.System:GetLocalizedName() return l.LOG_SYSTEM; end ---@param earliest_first boolean set to true if your sort order is to show the earlist first dates ---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function SystemLogEntry:GetDataPairs( earliest_first ) +function FlightLogEntry.System:GetDataPairs( earliest_first ) local o = {} ---@type table[] if ( earliest_first ) then @@ -282,12 +267,12 @@ function SystemLogEntry:GetDataPairs( earliest_first ) return o end ----@class CustomLogEntry : LogEntry +---@class FlightLogEntry.Custom : FlightLogEntry.Base ---@field systemp SystemPath The system the player is in when the log was written ---@field time number The game time the log was made, relative to the epoch ---@field money integer The amount of money the player has ---@field location string[] A number of string elements that can be compsed to create a localized description of the location. See composeLocationString -CustomLogEntry = utils.class("FlightLog.LogEntry.Custom", LogEntry) +FlightLogEntry.Custom = utils.class("FlightLogEntry.Custom", FlightLogEntry.Base) ---@param systemp SystemPath The system in question @@ -295,8 +280,8 @@ CustomLogEntry = utils.class("FlightLog.LogEntry.Custom", LogEntry) ---@param money integer The amount of money the player has ---@param location string[] A number of string elements that can be compsed to create a localized description of the location. See composeLocationString ---@param entry string The user entered custom test for this entry -function CustomLogEntry:Constructor( systemp, time, money, location, entry ) - LogEntry.Constructor( self, time, entry, true ) +function FlightLogEntry.Custom:Constructor( systemp, time, money, location, entry ) + FlightLogEntry.Base.Constructor( self, time, entry, true ) self.systemp = systemp self.time = time @@ -305,12 +290,12 @@ function CustomLogEntry:Constructor( systemp, time, money, location, entry ) end ---@return integer A unique integer for this specific type -function CustomLogEntry.GetSerializationIndex() +function FlightLogEntry.Custom.GetSerializationIndex() return 1 end ---@return any[] An array of the elements as they will be serialized -function CustomLogEntry:GetSerializationElements() +function FlightLogEntry.Custom:GetSerializationElements() return { self.systemp, self.time, self.money, self.location, self.entry } end @@ -318,18 +303,18 @@ end --- For the latest version will be the opposite of GetSerializationElements() ---@param elem any[] An array of elements used to construct ---@param version integer The version to read -function CustomLogEntry.CreateFromSerializationElements( elem, version ) - return CustomLogEntry.New( elem[1], elem[2], elem[3], elem[4], elem[5] ) +function FlightLogEntry.Custom.CreateFromSerializationElements( elem, version ) + return FlightLogEntry.Custom.New( elem[1], elem[2], elem[3], elem[4], elem[5] ) end ---@return string The name for this log entry type -function CustomLogEntry:GetLocalizedName() +function FlightLogEntry.Custom:GetLocalizedName() return l.LOG_CUSTOM; end ---@param earliest_first boolean set to true if your sort order is to show the earlist first dates ---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function CustomLogEntry:GetDataPairs( earliest_first ) +function FlightLogEntry.Custom:GetDataPairs( earliest_first ) return { { l.DATE, self.formatDate(self.time) }, { l.LOCATION, self.composeLocationString(self.location) }, @@ -340,29 +325,29 @@ function CustomLogEntry:GetDataPairs( earliest_first ) end ---@return boolean true if this has a Delete() method -function CustomLogEntry:SupportsDelete() +function FlightLogEntry.Custom:SupportsDelete() return true end ---Delete this entry ---@return nil -function CustomLogEntry:Delete() - TotalDefaultElements = TotalDefaultElements - 1 +function FlightLogEntry.Custom:Delete() + FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements - 1 utils.remove_elem( FlightLogData, self ) end ----@class StationLogEntry : LogEntry +---@class FlightLogEntry.Station : FlightLogEntry.Base ---@field systemp SystemPath The system the player is in when the log was written ---@field time deptime The game time the log was made, on departure from teh system, relative to the epoch ---@field money integer The amount of money the player has -StationLogEntry = utils.class("FlightLog.LogEntry.Station", LogEntry) +FlightLogEntry.Station = utils.class("FlightLogEntry.Station", FlightLogEntry.Base) ---@param systemp SystemPath The system the player is in when the log was written ---@param time deptime The game time the log was made, on departure from teh system, relative to the epoch ---@param money integer The amount of money the player has ---@param entry string The user entered custom test for this entry -function StationLogEntry:Constructor( systemp, deptime, money, entry ) - LogEntry.Constructor( self, deptime, entry ) +function FlightLogEntry.Station:Constructor( systemp, deptime, money, entry ) + FlightLogEntry.Base.Constructor( self, deptime, entry ) self.systemp = systemp self.deptime = deptime @@ -370,12 +355,12 @@ function StationLogEntry:Constructor( systemp, deptime, money, entry ) end ---@return integer A unique integer for this specific type -function StationLogEntry.GetSerializationIndex() +function FlightLogEntry.Station.GetSerializationIndex() return 2 end ---@return any[] An array of the elements as they will be serialized -function StationLogEntry:GetSerializationElements() +function FlightLogEntry.Station:GetSerializationElements() return { self.systemp, self.deptime, self.money, self.entry } end @@ -383,18 +368,18 @@ end --- For the latest version will be the opposite of GetSerializationElements() ---@param elem any[] An array of elements used to construct ---@param version integer The version to read -function StationLogEntry.CreateFromSerializationElements( elem, version ) - return StationLogEntry.New( elem[1], elem[2], elem[3], elem[4] ) +function FlightLogEntry.Station.CreateFromSerializationElements( elem, version ) + return FlightLogEntry.Station.New( elem[1], elem[2], elem[3], elem[4] ) end ---@return string The name for this log entry type -function StationLogEntry:GetLocalizedName() +function FlightLogEntry.Station:GetLocalizedName() return l.LOG_STATION; end ---@param earliest_first boolean set to true if your sort order is to show the earlist first dates ---@return table[] An array of key value pairs, the key being localized and the value being formatted appropriately. -function StationLogEntry:GetDataPairs( earliest_first ) +function FlightLogEntry.Station:GetDataPairs( earliest_first ) local station_type = "FLIGHTLOG_" .. self.systemp:GetSystemBody().type @@ -409,316 +394,4 @@ function StationLogEntry:GetDataPairs( earliest_first ) } end - -local FlightLog -FlightLog = { - --- --- Group: Methods --- --- --- --- Method: MakeCustomEntry --- --- Create a custom entry. A set of information is automatically --- compiled, in a header. --- --- > FlightLog.MakeCustomEntry(text) --- --- Header: --- --- path - System path, pointing to player's current sytem --- time - Game date --- money - Financial balance at time of record creation --- location - Array, with two strings: flight state, and relevant additional string --- manual entry - Free text string --- --- Parameters: --- --- text - Text to accompany the log --- - - MakeCustomEntry = function (text) - text = text or "" - local location = "" - local state = Game.player:GetFlightState() - local path = "" - - if state == "DOCKED" then - local station = Game.player:GetDockedWith() - local parent_body = station.path:GetSystemBody().parent.name - location = {station.type, station.label, parent_body} - path = Game.system.path - elseif state == "DOCKING" or state == "UNDOCKING" then - location = {state, Game.player:FindNearestTo("SPACESTATION").label} - path = Game.system.path - elseif state == "FLYING" then - if Game.player.frameBody then - location = {state, Game.player.frameBody.label} - else - location = {state, Game.system.name} -- if orbiting a system barycenter, there will be no frame object - end - path = Game.system.path - elseif state == "LANDED" then - path = Game.system.path - local alt, vspd, lat, long = Game.player:GetGPS() - if not (lat and long) then - lat, long = "nil", "nil" - end - location = {state, Game.player:FindNearestTo("PLANET").label, lat, long} - elseif state == "JUMPING" or state == "HYPERSPACE" then - --if in hyperspace, there's no Game.system - local spath, sysname = Game.player:GetHyperspaceDestination() - path = spath - location = {state, sysname} - end - - table.insert(FlightLogData,1, CustomLogEntry.New( path, Game.time, Game.player:GetMoney(), location, text ) ) - end, - -} - - - -function FlightLog.SkipFirstDocking() - skip_first_docking = true -end --- --- Method: GetLogEntries --- --- Parameters: --- --- types - An array of the types we want to fetch, nil if all of them --- maximum - the maximum number of results to return --- Return: --- --- iterator - A function which will generate the entries from the --- log, returning one each time it is called until it --- runs out, after which it returns nil. Each entry is --- a child class of LogEntry --- --- Example: --- --- > for entry in FlightLog.GetLogEntries( { "Custom", "System", "Station" ) do --- > print( entry.GetType(), entry.entry ) --- > end -function FlightLog:GetLogEntries(types, maximum, earliest_first) - - -- TODO: actually just store a list of all of them as they are at startup - local type_set = utils.set.New(types) - - -- note regardless of sort order, current status always comes first. - local currentStatus = nil - if nil == types or type_set:contains( "CurrentStatus" ) then - currentStatus = CurrentStatusLogEntry.New() - end - - local counter = 0 - maximum = maximum or #FlightLogData - return function () - if currentStatus then - local t = currentStatus - currentStatus = nil - return t - end - while counter < maximum do - counter = counter + 1 - - local v - if earliest_first then - v = FlightLogData[(#FlightLogData+1) - counter] - else - v = FlightLogData[counter] - end - -- TODO: Can we map the types to serialization indexes and check these - -- as they may be faster than the string manipulation comapare stuff. - if nil == types or type_set:contains( v:GetType() ) then - return v - end - end - return nil - end -end - ---- If there are two system eventsm back to back, starting at first_index ---- entering and leaving the same system, it will put them together ---- as a single system event ---- ----@param first_index integer The index of the first element in the array (so the latest event) to collapse -local function ConsiderCollapseSystemEventPair( first_index ) - -- TODO: make this global (ideally const, but our Lua version doesn't support that) - local system_idx = SystemLogEntry.GetSerializationIndex(); ---@type integer - - local second = FlightLogData[first_index] - local first = FlightLogData[first_index+1] - - if ( second:IsCustom() ) then return end - if ( second.GetSerializationIndex() ~= system_idx ) then return end - ---@cast second SystemLogEntry - - -- is the latest one actually an arrival event, or already collapsed. - if ( second.arrtime ~= nil ) then return end - --- local first = FlightLogData[first_index+1] - if ( first:IsCustom() ) then return end - if ( first.GetSerializationIndex() ~= system_idx ) then return end - ---@cast first SystemLogEntry - - -- is the first one actually a departure event or already collapsed - if ( first.deptime ~= nil ) then return end - - if ( first.systemp ~= second.systemp ) then return end - - second.arrtime = first.arrtime - table.remove( FlightLogData, first_index+1 ) - -end - --- This will run through the array of events and if there are two system events --- back to back, entering and leaving the same system, it will put them together --- as a single system event -local function CollapseSystemEvents() - for i = #FlightLogData-1, 1, -1 do - ConsiderCollapseSystemEventPair( i ) - end -end - --- This will run through the array of events and remove any non custom ones --- if we have exceeded our maximum size, until that maximum size is reattained. -local function TrimLogSize() - if TotalDefaultElements > MaxTotalDefaultElements then - CollapseSystemEvents() - while TotalDefaultElements > MaxTotalDefaultElements do - for i = #FlightLogData, 1, -1 do - local v = FlightLogData[i] - if not v:IsCustom() then - table.remove( FlightLogData, i ) - TotalDefaultElements = TotalDefaultElements-1 - end - end - end - CollapseSystemEvents() - end -end - - --- LOGGING - --- onLeaveSystem -local AddSystemDepartureToLog = function (ship) - if not ship:IsPlayer() then return end - - table.insert( FlightLogData, 1, SystemLogEntry.New( Game.system.path, nil, Game.time, nil ) ); - ConsiderCollapseSystemEventPair( 1 ) - TrimLogSize() -end - --- onEnterSystem -local AddSystemArrivalToLog = function (ship) - if not ship:IsPlayer() then return end - - table.insert( FlightLogData, 1, SystemLogEntry.New( Game.system.path, Game.time, nil, nil ) ); - TrimLogSize() -end - --- onShipDocked -local AddStationToLog = function (ship, station) - if not ship:IsPlayer() then return end - - -- could check the game time and see if it's the same as the last custom event - -- and there is nothing else in the list and avoud the phantom 'first docking' - -- that way too. - if skip_first_docking then - skip_first_docking = nil - return - end - table.insert( FlightLogData, 1, StationLogEntry.New( station.path, Game.time, Game.player:GetMoney(), nil ) ); - TrimLogSize() -end - --- LOADING AND SAVING - -local loaded_data - -local onGameStart = function () - - if loaded_data and loaded_data.Version == 1 then - - for _, v in pairs( loaded_data.System ) do - local entryLog = SystemLogEntry.CreateFromSerializationElements( { v[1], v[2], nil, v[4] }, 1 ) - local exitLog = SystemLogEntry.CreateFromSerializationElements( { v[1], nil, v[3], v[4] }, 1 ) - - if (exitLog.deptime ~= nil) then - FlightLogData[#FlightLogData+1] = exitLog - end - if (entryLog.arrtime ~= nil) then - FlightLogData[#FlightLogData+1] = entryLog - end - end - - for _, v in pairs( loaded_data.Station ) do - FlightLogData[#FlightLogData+1] = StationLogEntry.CreateFromSerializationElements( v, 1 ) - end - - for _, v in pairs( loaded_data.Custom ) do - FlightLogData[#FlightLogData+1] = CustomLogEntry.CreateFromSerializationElements( v, 1 ) - end - - local function sortf( a, b ) - return a.sort_date > b.sort_date - end - - table.sort( FlightLogData, sortf ) - - CollapseSystemEvents() - - elseif loaded_data and loaded_data.Version > 1 then - - local loader_funcs = {} - loader_funcs[SystemLogEntry.GetSerializationIndex()] = SystemLogEntry.CreateFromSerializationElements - loader_funcs[StationLogEntry.GetSerializationIndex()] = StationLogEntry.CreateFromSerializationElements - loader_funcs[CustomLogEntry.GetSerializationIndex()] = CustomLogEntry.CreateFromSerializationElements - - for _, p in pairs( loaded_data.Data ) do - for type, v in pairs(p) do - local lf = loader_funcs[type] - local val = lf(v, loaded_data.Version); - FlightLogData[#FlightLogData+1] = val - end - end - end - loaded_data = nil -end - -local onGameEnd = function () - FlightLogData = {} - TotalDefaultElements = 0 -end - -local serialize = function () - - local source = FlightLogData - local SaveData = {} - - for _, v in pairs( source ) do - v:AddToSerializationTable( SaveData ) - end - - return { - Data = SaveData, - Version = 2 -- version for backwards compatibility - } -end - -local unserialize = function (data) - loaded_data = data -end - -Event.Register("onEnterSystem", AddSystemArrivalToLog) -Event.Register("onLeaveSystem", AddSystemDepartureToLog) -Event.Register("onShipDocked", AddStationToLog) -Event.Register("onGameStart", onGameStart) -Event.Register("onGameEnd", onGameEnd) -Serializer:Register("FlightLog", serialize, unserialize) - -return FlightLog +return FlightLogEntry \ No newline at end of file From 786928ad7c28bb8a1a067caf28500f24d2856111 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Fri, 6 Oct 2023 22:33:06 +1300 Subject: [PATCH 16/19] use table.insert to add to the end of a table --- data/modules/FlightLog/FlightLog.lua | 10 +++++----- data/modules/FlightLog/FlightLogEntries.lua | 18 +++++++++--------- data/pigui/modules/info-view/06-flightlog.lua | 8 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/data/modules/FlightLog/FlightLog.lua b/data/modules/FlightLog/FlightLog.lua index 545fc2564b6..936b114bb0b 100644 --- a/data/modules/FlightLog/FlightLog.lua +++ b/data/modules/FlightLog/FlightLog.lua @@ -270,19 +270,19 @@ local onGameStart = function () local exitLog = FlightLogEntry.System.CreateFromSerializationElements( { v[1], nil, v[3], v[4] }, 1 ) if (exitLog.deptime ~= nil) then - FlightLogData[#FlightLogData+1] = exitLog + table.insert(FlightLogData, exitLog) end if (entryLog.arrtime ~= nil) then - FlightLogData[#FlightLogData+1] = entryLog + table.insert(FlightLogData, entryLog) end end for _, v in pairs( loaded_data.Station ) do - FlightLogData[#FlightLogData+1] = FlightLogEntry.Station.CreateFromSerializationElements( v, 1 ) + table.insert(FlightLogData, FlightLogEntry.Station.CreateFromSerializationElements( v, 1 )) end for _, v in pairs( loaded_data.Custom ) do - FlightLogData[#FlightLogData+1] = FlightLogEntry.Custom.CreateFromSerializationElements( v, 1 ) + table.insert(FlightLogData, FlightLogEntry.Custom.CreateFromSerializationElements( v, 1 )) end local function sortf( a, b ) @@ -304,7 +304,7 @@ local onGameStart = function () for type, v in pairs(p) do local lf = loader_funcs[type] local val = lf(v, loaded_data.Version); - FlightLogData[#FlightLogData+1] = val + table.insert(FlightLogData, val) end end end diff --git a/data/modules/FlightLog/FlightLogEntries.lua b/data/modules/FlightLog/FlightLogEntries.lua index ae9938fbfab..904fe2ba7a7 100644 --- a/data/modules/FlightLog/FlightLogEntries.lua +++ b/data/modules/FlightLog/FlightLogEntries.lua @@ -71,12 +71,12 @@ end -- the elements of the array are a key-value pair -- the key is the serialization index (so the type this log entry is) -- the value is an array of data, to construct the table from. ----@param table table +---@param out table ---@return nil -function FlightLogEntry.Base:AddToSerializationTable( table ) +function FlightLogEntry.Base:AddToSerializationTable( out ) local v = {} v[self.GetSerializationIndex()] = self:GetSerializationElements() - table[#table+1] = v + table.insert(out, v) end ---@return integer A unique integer for this specific type @@ -248,21 +248,21 @@ function FlightLogEntry.System:GetDataPairs( earliest_first ) if ( earliest_first ) then if self.arrtime then - o[#o+1] = { l.ARRIVAL_DATE, self.formatDate(self.arrtime) } + table.insert(o, { l.ARRIVAL_DATE, self.formatDate(self.arrtime) }) end if self.deptime then - o[#o+1] = { l.DEPARTURE_DATE, self.formatDate(self.deptime) } + table.insert(o, { l.DEPARTURE_DATE, self.formatDate(self.deptime) }) end else if self.deptime then - o[#o+1] = { l.DEPARTURE_DATE, self.formatDate(self.deptime) } + table.insert(o, { l.DEPARTURE_DATE, self.formatDate(self.deptime) }) end if self.arrtime then - o[#o+1] = { l.ARRIVAL_DATE, self.formatDate(self.arrtime) } + table.insert(o, { l.ARRIVAL_DATE, self.formatDate(self.arrtime) }) end end - o[#o+1] = { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) } - o[#o+1] = { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name } + table.insert(o, { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }) + table.insert(o, { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name }) return o end diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index 1cf627f8e50..985a047377e 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -31,10 +31,10 @@ local export_html = true local function getIncludedSet() o = {} - if include_player_info then o[#o+1] = "CurrentStatus" end - if include_custom_log then o[#o+1] = "Custom" end - if include_station_log then o[#o+1] = "Station" end - if include_system_log then o[#o+1] = "System" end + if include_player_info then table.insert(o, "CurrentStatus") end + if include_custom_log then table.insert(o, "Custom") end + if include_station_log then table.insert(o, "Station") end + if include_system_log then table.insert(o, "System") end return o; end From 334718b766e4744c2949a3871723863b4ae50ee5 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sat, 7 Oct 2023 09:06:02 +1300 Subject: [PATCH 17/19] Split flightlog export functionality from the gui code and move it to the module Only export personal information when writing a log, don't show it on the gui ever. Remove the utils set class, it didn't add value --- data/libs/utils.lua | 38 ------ data/modules/FlightLog/FlightLog.lua | 48 ++----- data/pigui/modules/info-view/06-flightlog.lua | 126 +----------------- 3 files changed, 20 insertions(+), 192 deletions(-) diff --git a/data/libs/utils.lua b/data/libs/utils.lua index 7d2015bbfd9..a2cd34602c0 100644 --- a/data/libs/utils.lua +++ b/data/libs/utils.lua @@ -780,44 +780,6 @@ utils.getFromIntervals = function(array, value) return array[utils.getIndexFromIntervals(array, value)][1] end --- a simple set class -utils.set = utils.class( "utils.set" ) - --- Constructor --- Build a set --- Parameters: --- --- array - an array, every element of which will be added to the set. If not provided the set starts empty --- number - some value in intervals -function utils.set:Constructor( array ) - self.set = {} - if nil ~= array then - for _, value in pairs( array ) do - self.set[value] = true - end - end -end - -function utils.set:add( value ) - self.set[value] = true -end - -function utils.set:remove( value ) - self.set[value] = nil -end - -function utils.set:contains( value ) - return self.set[value] ~= nil -end - -function utils.set:to_array() - local array = {} - for v, _ in pairs( self.set ) do - array[#array+1] = v - end - return array -end - return utils diff --git a/data/modules/FlightLog/FlightLog.lua b/data/modules/FlightLog/FlightLog.lua index 936b114bb0b..03bda15fc67 100644 --- a/data/modules/FlightLog/FlightLog.lua +++ b/data/modules/FlightLog/FlightLog.lua @@ -104,44 +104,24 @@ FlightLog = { function FlightLog.SkipFirstDocking() skip_first_docking = true end --- --- Method: GetLogEntries --- --- Parameters: --- --- types - An array of the types we want to fetch, nil if all of them --- maximum - the maximum number of results to return --- Return: --- --- iterator - A function which will generate the entries from the --- log, returning one each time it is called until it --- runs out, after which it returns nil. Each entry is --- a child class of LogEntry --- --- Example: --- --- > for entry in FlightLog.GetLogEntries( { "Custom", "System", "Station" ) do --- > print( entry.GetType(), entry.entry ) --- > end -function FlightLog:GetLogEntries(types, maximum, earliest_first) - - -- TODO: actually just store a list of all of them as they are at startup - local type_set = utils.set.New(types) - -- note regardless of sort order, current status always comes first. - local currentStatus = nil - if nil == types or type_set:contains( "CurrentStatus" ) then - currentStatus = FlightLogEntry.CurrentStatus.New() - end +--- Method: GetLogEntries +---@param types table[string,boolean]|nil Keys are log types to include, set the boolean to true for the ones you want +---@param maximum integer|nil Maximum number of entries to include +---@param earliest_first boolean|nil Should the log start with the oldest entry or the most recent +--- +---@return function():FlightLogEntry.Base An iterator function that when called repeatedly returns the next entry or nil when complete +--- +--- Example: +--- +--- > for entry in FlightLog.GetLogEntries( { "Custom", "System", "Station" ) do +--- > print( entry.GetType(), entry.entry ) +--- > end +function FlightLog:GetLogEntries(types, maximum, earliest_first) local counter = 0 maximum = maximum or #FlightLogData return function () - if currentStatus then - local t = currentStatus - currentStatus = nil - return t - end while counter < maximum do counter = counter + 1 @@ -153,7 +133,7 @@ function FlightLog:GetLogEntries(types, maximum, earliest_first) end -- TODO: Can we map the types to serialization indexes and check these -- as they may be faster than the string manipulation comapare stuff. - if nil == types or type_set:contains( v:GetType() ) then + if nil == types or types[ v:GetType() ] then return v end end diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index 985a047377e..bc7c83d19e9 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -5,11 +5,10 @@ local ui = require 'pigui' local InfoView = require 'pigui.views.info-view' local Lang = require 'Lang' local FlightLog = require 'modules.FlightLog.FlightLog' +local FlightLogExporter = require 'modules.FlightLog.FlightLogExporter' local Format = require 'Format' local Color = _G.Color local Vector2 = _G.Vector2 -local FileSystem = require 'FileSystem' -local Character = require 'Character' local pionillium = ui.fonts.pionillium local icons = ui.theme.icons @@ -21,8 +20,6 @@ local l = Lang.GetResource("ui-core") local iconSize = ui.rescaleUI(Vector2(28, 28)) local buttonSpaceSize = iconSize - -local include_player_info = true local include_custom_log = true local include_station_log = true local include_system_log = true @@ -30,12 +27,10 @@ local earliest_first = false local export_html = true local function getIncludedSet() - o = {} - if include_player_info then table.insert(o, "CurrentStatus") end - if include_custom_log then table.insert(o, "Custom") end - if include_station_log then table.insert(o, "Station") end - if include_system_log then table.insert(o, "System") end - + local o = {} + if include_custom_log then o["Custom"] = true end + if include_station_log then o["Station"] = true end + if include_system_log then o["System"] = true end return o; end @@ -74,78 +69,6 @@ function ui_formatter:newline() return self end -local text_formatter = {} - -function text_formatter:open( file ) - self.file = file - return self -end - -function text_formatter:write( string ) - self.file:write( string ) - return self -end - -function text_formatter:newline() - self.file:write( "\n" ) - return self -end - -function text_formatter:close() - self.file:close() -end - -function text_formatter:headerText(title, text, wrap) - -- Title text is gray, followed by the variable text: - if not text then return end - self:write( string.gsub(title, ":", "") ):write( ": " ) - - -- TODO wrap? - self:write( text ):newline() - return self -end - -function text_formatter:separator() - self.file:write( "\n----------------------------------------------\n\n" ) -end - -local html_formatter = {} - -function html_formatter:write( string ) - self.file:write( string ) - return self -end - -function html_formatter:open( file ) - self.file = file - self.file:write( "\n" ) - return self -end - -function html_formatter:newline() - self.file:write( "
\n" ) - return self -end - -function html_formatter:close() - self.file:write( "" ) - self.file:close() -end - -function html_formatter:headerText(title, text, wrap) - -- Title text is gray, followed by the variable text: - if not text then return end - self:write( "" ):write( string.gsub(title, ":", "") ):write( ": " ) - - -- TODO wrap? - self:write( text ):newline() - return self -end - -function html_formatter:separator() - self.file:write( "\n
\n" ) -end - entering_text = false -- Display Entry text, and Edit button, to update flightlog @@ -236,48 +159,11 @@ local function checkbox(label, checked, tooltip) return changed, ret end -local function exportLogs() - -- TODO localize? - local foldername = FileSystem.MakeUserDataDirectory( "player_logs" ) - - local player = Character.persistent.player - - local base_save_name = player.name - - local formatter - local extension - if export_html then - formatter = html_formatter - extension = '.html' - else - formatter = text_formatter - extension = '.log' - end - - local log_filename = FileSystem.JoinPath( foldername, base_save_name .. extension ) - - formatter:open( io.open( log_filename, "w" ) ) - - for entry in FlightLog:GetLogEntries(getIncludedSet(),nil, earliest_first) do - - writeLogEntry(entry, formatter, true) - - if (entry:HasEntry()) then - formatter:headerText(l.ENTRY, entry:GetEntry(), true) - end - formatter:separator() - end - - formatter:close() - -end - local function displayFilterOptions() ui.spacing() local c local flight_log = true; - c,include_player_info = checkbox(l.PERSONAL_INFORMATION, include_player_info) c,include_custom_log = checkbox(l.LOG_CUSTOM, include_custom_log) c,include_station_log = checkbox(l.LOG_STATION, include_station_log) c,include_system_log = checkbox(l.LOG_SYSTEM, include_system_log) @@ -290,7 +176,7 @@ local function displayFilterOptions() ui.spacing() if ui.button(l.SAVE) then - exportLogs() + FlightLogExporter.Export( getIncludedSet(), earliest_first, true, export_html) end end From fd4917d2fd47106c6fd219eb86739a1ce888ceec Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sun, 8 Oct 2023 10:59:13 +1300 Subject: [PATCH 18/19] Missing file from last commit. --- data/modules/FlightLog/FlightLogExporter.lua | 137 +++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 data/modules/FlightLog/FlightLogExporter.lua diff --git a/data/modules/FlightLog/FlightLogExporter.lua b/data/modules/FlightLog/FlightLogExporter.lua new file mode 100644 index 00000000000..491f12f4e30 --- /dev/null +++ b/data/modules/FlightLog/FlightLogExporter.lua @@ -0,0 +1,137 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local FlightLog = require 'modules.FlightLog.FlightLog' +local FileSystem = require 'FileSystem' +local Character = require 'Character' +local Lang = require 'Lang' +local l = Lang.GetResource("ui-core") + +local text_formatter = {} + +function text_formatter:open( file ) + self.file = file + return self +end + +function text_formatter:write( string ) + self.file:write( string ) + return self +end + +function text_formatter:newline() + self.file:write( "\n" ) + return self +end + +function text_formatter:close() + self.file:close() +end + +function text_formatter:headerText(title, text, wrap) + -- Title text is gray, followed by the variable text: + if not text then return end + self:write( string.gsub(title, ":", "") ):write( ": " ) + + -- TODO wrap? + self:write( text ):newline() + return self +end + +function text_formatter:separator() + self.file:write( "\n----------------------------------------------\n\n" ) +end + +local html_formatter = {} + +function html_formatter:write( string ) + self.file:write( string ) + return self +end + +function html_formatter:open( file ) + self.file = file + self.file:write( "\n" ) + return self +end + +function html_formatter:newline() + self.file:write( "
\n" ) + return self +end + +function html_formatter:close() + self.file:write( "" ) + self.file:close() +end + +function html_formatter:headerText(title, text, wrap) + -- Title text is gray, followed by the variable text: + if not text then return end + self:write( "" ):write( string.gsub(title, ":", "") ):write( ": " ) + + -- TODO wrap? + self:write( text ):newline() + return self +end + +function html_formatter:separator() + self.file:write( "\n
\n" ) +end + +local Exporter = {} + +---@param included_types table[string,boolean] Keys are log types to include, set the boolean to true for the ones you want +---@param earliest_first boolean Should the log start with the oldest entry or the most recent +---@param player_info boolean Should the log include the current player status at the top? +---@param export_html boolean true for HTML, false for plain text +function Exporter.Export( included_types, earliest_first, player_info, export_html ) + + local foldername = FileSystem.MakeUserDataDirectory( "player_logs" ) + + local player = Character.persistent.player + + local base_save_name = player.name + + local formatter + local extension + if export_html then + formatter = html_formatter + extension = '.html' + else + formatter = text_formatter + extension = '.log' + end + + local log_filename = FileSystem.JoinPath( foldername, base_save_name .. extension ) + + formatter:open( io.open( log_filename, "w" ) ) + + if player_info then + formatter:headerText( l.NAME_PERSON, player.name ) + -- TODO: localize + formatter:headerText( "Title", player.title ) + formatter:headerText( l.RATING, l[player:GetCombatRating()] ) + formatter:headerText( l.KILLS, string.format('%d',player.killcount) ) + formatter:separator() + formatter:newline() + end + + for entry in FlightLog:GetLogEntries(included_types,nil, earliest_first) do + + formatter:write( entry:GetLocalizedName() ):newline() + for _, pair in pairs( entry:GetDataPairs( earliest_first ) ) do + formatter:headerText( pair[1], pair[2] ) + end + + if (entry:HasEntry()) then + formatter:headerText(l.ENTRY, entry:GetEntry(), true) + end + formatter:separator() + end + + formatter:close() + +end + +return Exporter \ No newline at end of file From 3046469e84422ec9cd16ebc112c27ac0072f6261 Mon Sep 17 00:00:00 2001 From: Jon Booth Date: Sun, 8 Oct 2023 11:11:22 +1300 Subject: [PATCH 19/19] Creation of a FileSystem.Open() method that takes the root filesystem as an argument for the location of the file to open This ensures that files are opened within our sandbox and are read only if in the data folder. Addition of lua type annotations for the FileSystem functionality. --- data/libs/FileSystem.lua | 27 ++++++ data/meta/FileSystemBase.lua | 31 +++++++ data/modules/FlightLog/FlightLogExporter.lua | 8 +- src/lua/LuaFileSystem.cpp | 89 ++++++++++++++------ src/lua/LuaFileSystem.h | 8 ++ src/lua/core/Sandbox.cpp | 31 ++++--- 6 files changed, 152 insertions(+), 42 deletions(-) create mode 100644 data/libs/FileSystem.lua create mode 100644 data/meta/FileSystemBase.lua diff --git a/data/libs/FileSystem.lua b/data/libs/FileSystem.lua new file mode 100644 index 00000000000..cd9912757b1 --- /dev/null +++ b/data/libs/FileSystem.lua @@ -0,0 +1,27 @@ +---@class FileSystem : FileSystemBase +local FileSystem = package.core["FileSystem"] + +--- Wrapper for our patched io.open that ensures files are opened inside the sandbox. +--- Prefer using this to io.open +--- +--- Files in the user folder can be read or written to +--- Files in the data folder are read only. +--- +--- +--- +--- Example: +--- > f = FileSystem.Open( "USER", "my_file.txt", "w" ) +--- > f:write( "file contents" ) +--- > f:close() +--- +---@param root string A FileSystemRoot constant. Can be either "DATA" or "USER" +---@param filename string The name of the file to open, relative to the root +---@param mode string|nil The mode to open the file in, defaults to read only +---@return file A lua io file +function FileSystem.Open( root, filename, mode ) + if not mode then mode = "r" end + + return io.open( filename, mode, root ) +end + +return FileSystem diff --git a/data/meta/FileSystemBase.lua b/data/meta/FileSystemBase.lua new file mode 100644 index 00000000000..20418a4145b --- /dev/null +++ b/data/meta/FileSystemBase.lua @@ -0,0 +1,31 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +-- This file implements type information about C++ classes for Lua static analysis +-- This is used in FileSyestem.lua whcih then extends that. + +---@meta + +---@class FileSystemBase +local FileSystemBase = {} + +---@param root string A FileSystemRoot constant. Can be either "DATA" or "USER" +---@return string[] files A list of files as full paths from the root +---@return string[] dirs A list of dirs as full paths from the root +--- +--- Example: +--- > local files, dirs = FileSystem.ReadDirectory(root, path) +function FileSystemBase.ReadDirectory(root, path) end + +--- Join the passed arguments into a path, correctly handling separators and . +--- and .. special dirs. +--- +---@param arg string[] A list of path elements to be joined +---@return string The joined path elements +function FileSystemBase.JoinPath( ... ) end + +---@param dir_name string The name of the folder to create in the user directory +---@return boolean Success +function FileSystemBase.MakeUserDataDirectory( dir_name ) end + +return FileSystemBase diff --git a/data/modules/FlightLog/FlightLogExporter.lua b/data/modules/FlightLog/FlightLogExporter.lua index 491f12f4e30..c6270dffc64 100644 --- a/data/modules/FlightLog/FlightLogExporter.lua +++ b/data/modules/FlightLog/FlightLogExporter.lua @@ -86,8 +86,8 @@ local Exporter = {} ---@param player_info boolean Should the log include the current player status at the top? ---@param export_html boolean true for HTML, false for plain text function Exporter.Export( included_types, earliest_first, player_info, export_html ) - - local foldername = FileSystem.MakeUserDataDirectory( "player_logs" ) + + FileSystem.MakeUserDataDirectory( "player_logs" ) local player = Character.persistent.player @@ -103,9 +103,9 @@ function Exporter.Export( included_types, earliest_first, player_info, export_ht extension = '.log' end - local log_filename = FileSystem.JoinPath( foldername, base_save_name .. extension ) + local log_filename = FileSystem.JoinPath( "player_logs", base_save_name .. extension ) - formatter:open( io.open( log_filename, "w" ) ) + formatter:open( FileSystem.Open( "USER", log_filename, "w" ) ) if player_info then formatter:headerText( l.NAME_PERSON, player.name ) diff --git a/src/lua/LuaFileSystem.cpp b/src/lua/LuaFileSystem.cpp index 3178ea3f062..c1ecf79ea9e 100644 --- a/src/lua/LuaFileSystem.cpp +++ b/src/lua/LuaFileSystem.cpp @@ -17,6 +17,59 @@ * will get a Lua error. */ +FileSystem::FileSource* get_filesytem_for_root(LuaFileSystem::Root root) +{ + FileSystem::FileSource* fs = nullptr; + switch (root) { + case LuaFileSystem::ROOT_USER: + fs = &FileSystem::userFiles; + break; + + case LuaFileSystem::ROOT_DATA: + fs = &FileSystem::gameDataFiles; + break; + + default: + assert(0); // can't happen + } + return fs; +} + +std::string LuaFileSystem::lua_path_to_fs_path(lua_State* l, const char* root_name, const char* path, const char* access) +{ + const LuaFileSystem::Root root = static_cast(LuaConstants::GetConstant(l, "FileSystemRoot", root_name)); + + if (root == LuaFileSystem::ROOT_DATA) + { + // check the acces mode is allowed: + + // default mode is read only + if (access[0] != 0) + { + if (access[0] != 'r' || access[1] != 0) + { + // we are requesting an access mode not allowed for the user folder + // as you can't write there. + luaL_error(l, "'%s' is not valid for opening a file in root '%s'", access, root_name); + return ""; + } + } + } + + FileSystem::FileSource* fs = get_filesytem_for_root(root); + assert(fs); + + try + { + return std::move(fs->Lookup(path).GetAbsolutePath()); + } + catch (std::invalid_argument e) + { + luaL_error(l, "'%s' is not valid for opening a file in root '%s' - Is the file location within the root?", path, root_name); + return ""; + } +} + static void push_date_time(lua_State *l, const Time::DateTime &dt) { int year, month, day, hour, minute, second; @@ -65,20 +118,7 @@ static int l_filesystem_read_dir(lua_State *l) if (lua_gettop(l) > 1) path = luaL_checkstring(l, 2); - FileSystem::FileSource *fs = nullptr; - switch (root) { - case LuaFileSystem::ROOT_USER: - fs = &FileSystem::userFiles; - break; - - case LuaFileSystem::ROOT_DATA: - fs = &FileSystem::gameDataFiles; - break; - - default: - assert(0); // can't happen - return 0; - } + FileSystem::FileSource* fs = get_filesytem_for_root(root); assert(fs); @@ -157,8 +197,8 @@ static int l_filesystem_join_path(lua_State *l) * * > local path = FileSystem.MakeUserDataDirectory( dir_name ) * - * Creating the given directory if it's missing, returning the name - * full name of the directory + * Creating the given directory if it's missing, returning a boolean + * indicating success * * Availability: * @@ -177,20 +217,13 @@ static int l_filesystem_make_user_directory(lua_State* l) FileSystem::FileInfo f = FileSystem::userFiles.Lookup(dir); - if (f.IsDir()) - { - std::string fullDirName = f.GetAbsolutePath(); - lua_pushlstring(l, fullDirName.c_str(), fullDirName.size()); - return 1; - } else - { - lua_pushlstring(l, "", 1 ); - return 0; - } + lua_pushboolean(l, f.IsDir()); + return 1; } catch (const std::invalid_argument&) { - luaL_error(l, "unable to create directory"); - return 0; + luaL_error(l, "unable to create directory the argument is invalid"); + lua_pushboolean(l, 0); + return 1; } } diff --git a/src/lua/LuaFileSystem.h b/src/lua/LuaFileSystem.h index eaa55b9a3c0..1cfdb571782 100644 --- a/src/lua/LuaFileSystem.h +++ b/src/lua/LuaFileSystem.h @@ -4,6 +4,10 @@ #ifndef _LUAFILESYSTEM_H #define _LUAFILESYSTEM_H +#include + +struct lua_State; + namespace LuaFileSystem { void Register(); @@ -11,6 +15,10 @@ namespace LuaFileSystem { ROOT_USER, ROOT_DATA }; + + // will throw lua errors if not allowed + // and return an empty string + std::string lua_path_to_fs_path(lua_State* l, const char* root, const char* path, const char* access); } // namespace LuaFileSystem #endif diff --git a/src/lua/core/Sandbox.cpp b/src/lua/core/Sandbox.cpp index 3d879f687ec..950e11aa433 100644 --- a/src/lua/core/Sandbox.cpp +++ b/src/lua/core/Sandbox.cpp @@ -7,6 +7,7 @@ #include "core/Log.h" #include "utils.h" #include "FileSystem.h" +#include "LuaFileSystem.h" static int l_d_null_userdata(lua_State *L) { @@ -109,19 +110,29 @@ static lua_CFunction l_original_io_open = nullptr; static int l_patched_io_open(lua_State* L) { - std::string path = lua_tostring(L, 1); - path = FileSystem::NormalisePath(path); + if (lua_gettop(L) != 3) + { + luaL_error(L, "Wrong number of arguments for io.open(). You should be using FileSystem.Open() instead"); + return 0; + } + const char* path_arg = lua_tostring(L, 1); + const char* access_arg = lua_tostring(L, 2); + const char* root_arg = lua_tostring(L, 3); - std::string userDir = FileSystem::GetUserDir(); - // TODO: should we add a file separator here? - - if (path.rfind(userDir, 0) == 0) + std::string path = LuaFileSystem::lua_path_to_fs_path(L, root_arg, path_arg, access_arg); + + if (path.length() == 0) { - // path starts with the userDir, we're good to go - return l_original_io_open(L); + luaL_error(L, "attempt to access filesystem in an invalid user folder location"); + return 0; } - luaL_error(L, "attempt to access filesystem outside of the user data folder"); - return 0; + + lua_pushstring(L, path.c_str()); + lua_replace(L, 1); + + const int rv = l_original_io_open(L); + + return rv; } static const luaL_Reg STANDARD_LIBS[] = {