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/libs/FlightLog.lua b/data/libs/FlightLog.lua deleted file mode 100644 index c03088e34be..00000000000 --- a/data/libs/FlightLog.lua +++ /dev/null @@ -1,450 +0,0 @@ --- 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. - -local Game = require 'Game' -local Event = require 'Event' -local Format = require 'Format' -local Serializer = require 'Serializer' - --- default values (private) -local FlightLogSystemQueueLength = 1000 -local FlightLogStationQueueLength = 1000 - --- private data - the log itself -local FlightLogSystem = {} -local FlightLogStation = {} -local FlightLogCustom = {} - -local FlightLog -FlightLog = { - --- --- Group: Methods --- - --- --- Method: GetSystemPaths --- --- Returns an iterator returning a SystemPath 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 systemp,arrtime,deptime,entry in FlightLog.GetSystemPaths(5) do --- > print(systemp:GetStarSystem().name, Format.Date(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 - return FlightLogSystem[counter][1], - FlightLogSystem[counter][2], - FlightLogSystem[counter][3], - FlightLogSystem[counter][4] - end - end - return nil, nil, nil, nil - end - end, - - --- --- 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") --- - - UpdateSystemEntry = function (index, entry) - FlightLogSystem[index][4] = entry - end, - --- --- Method: GetStationPaths --- --- Returns an iterator returning a SystemPath 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 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. --- --- 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)) --- > 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 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 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. --- --- > 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 --- consists of the system's path, date, money, location, --- text; 'location' being an text array with flight state --- and appropriate additional information. - --- --- Example: --- --- > for systemp, date, money, location, entry in FlightLog.GetCustomEntry(5) do --- > print(location[1], location[2], Format.Date(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 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 - 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 --- --- 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(FlightLogCustom,1, - {path, Game.time, Game.player:GetMoney(), location, text}) - end, - -} - --- LOGGING - --- 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 -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 -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 -end - --- LOADING AND SAVING - -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,""}) - end - loaded_data = nil -end - -local onGameEnd = function () - FlightLogSystem = {} - FlightLogStation = {} - FlightLogCustom = {} -end - -local serialize = function () - return { System = FlightLogSystem, - Station = FlightLogStation, - Custom = FlightLogCustom, - Version = 1 -- 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 diff --git a/data/libs/utils.lua b/data/libs/utils.lua index 63d0eb6f27b..a2cd34602c0 100644 --- a/data/libs/utils.lua +++ b/data/libs/utils.lua @@ -781,3 +781,5 @@ utils.getFromIntervals = function(array, value) end return utils + + 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/FlightLog.lua b/data/modules/FlightLog/FlightLog.lua new file mode 100644 index 00000000000..03bda15fc67 --- /dev/null +++ b/data/modules/FlightLog/FlightLog.lua @@ -0,0 +1,326 @@ +-- 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. + +local Game = require 'Game' +local Event = require 'Event' +local Format = require 'Format' +local Serializer = require 'Serializer' + +local utils = require 'utils' + +local Character = require 'Character' + +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 + +-- private data - the log itself +---@type FlightLogEntry.Base[] +local FlightLogData = {} + +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, FlightLogEntry.Custom.New( path, Game.time, Game.player:GetMoney(), location, text ) ) + end, + +} + + + +function FlightLog.SkipFirstDocking() + skip_first_docking = true +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 () + 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 types[ 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 = FlightLogEntry.System.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 FlightLogEntry.TotalDefaultElements > MaxTotalDefaultElements then + CollapseSystemEvents() + while FlightLogEntry.TotalDefaultElements > MaxTotalDefaultElements do + for i = #FlightLogData, 1, -1 do + local v = FlightLogData[i] + if not v:IsCustom() then + table.remove( FlightLogData, i ) + FlightLogEntry.TotalDefaultElements = FlightLogEntry.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, FlightLogEntry.System.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, FlightLogEntry.System.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, FlightLogEntry.Station.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 = 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 + table.insert(FlightLogData, exitLog) + end + if (entryLog.arrtime ~= nil) then + table.insert(FlightLogData, entryLog) + end + end + + for _, v in pairs( loaded_data.Station ) do + table.insert(FlightLogData, FlightLogEntry.Station.CreateFromSerializationElements( v, 1 )) + end + + for _, v in pairs( loaded_data.Custom ) do + table.insert(FlightLogData, FlightLogEntry.Custom.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[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 + local lf = loader_funcs[type] + local val = lf(v, loaded_data.Version); + table.insert(FlightLogData, val) + end + end + end + + loaded_data = nil +end + +local onGameEnd = function () + FlightLogData = {} + FlightLogEntry.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 diff --git a/data/modules/FlightLog/FlightLogEntries.lua b/data/modules/FlightLog/FlightLogEntries.lua new file mode 100644 index 00000000000..904fe2ba7a7 --- /dev/null +++ b/data/modules/FlightLog/FlightLogEntries.lua @@ -0,0 +1,397 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +-- Entries for the FlightLog + +local Game = require 'Game' +local Format = require 'Format' + +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 + +FlightLogEntry = {} + +-- how many default (so not custom) elements do we have +---@type integer +FlightLogEntry.TotalDefaultElements = 0 + + +---@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) +FlightLogEntry.Base = utils.class("FlightLogEntry") + +---@return string Description of this type +function FlightLogEntry.Base:GetType() + local full_type = self.Class().meta.class + return string.sub( full_type, #"FlightLogEntry."+1, #full_type ) +end + +---@return boolean true if this is considered to have an entry +function FlightLogEntry.Base:CanHaveEntry() + return true +end + +---@return boolean true if this has an entry +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 FlightLogEntry.Base:GetEntry() + if not self.entry then return "" end + return self.entry +end + +---@return boolean true if this has a Delete() method +function FlightLogEntry.Base:SupportsDelete() + return false +end + + +---@return boolean True if this log entry should be considered custom and so not auto deleted +function FlightLogEntry.Base: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 out table +---@return nil +function FlightLogEntry.Base:AddToSerializationTable( out ) + local v = {} + v[self.GetSerializationIndex()] = self:GetSerializationElements() + table.insert(out, v) +end + +---@return integer A unique integer for this specific type +function FlightLogEntry.Base.GetSerializationIndex() + return -1 +end + +---@return any[] An array of the elements as they will be serialized +function FlightLogEntry.Base:GetSerializationElements() + return {} +end + + +---@return string The name for this log entry type +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 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 FlightLogEntry.Base:UpdateEntry( entry ) + + if self:IsCustom() then + FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements-1 + end + + if entry and #entry == 0 then entry = nil end + self.entry = entry + + if self:IsCustom() then + FlightLogEntry.TotalDefaultElements = FlightLogEntry.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 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 + if entry and #entry == 0 then entry = nil end + self.entry = entry + self.always_custom = always_custom + if self:IsCustom() then + FlightLogEntry.TotalDefaultElements = FlightLogEntry.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 FlightLogEntry.Base.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 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 : FlightLogEntry.Base +--- Does not have any members, it grabs the current status live whenever requested +FlightLogEntry.CurrentStatus = utils.class("FlightLogEntry.CurrentStatus", FlightLogEntry.Base ) + +---@return boolean true if this is considered to have an entry +function FlightLogEntry.CurrentStatus:CanHaveEntry() + return false +end + +function FlightLogEntry.CurrentStatus:Constructor() + FlightLogEntry.Base.Constructor( self, Game.time, nil, true ) +end + +---@return string The name for this log entry type +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 FlightLogEntry.CurrentStatus: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 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 +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 FlightLogEntry.System:Constructor( systemp, arrtime, deptime, entry ) + + local sort_date + if nil == arrtime then + sort_date = deptime + else + sort_date = arrtime + end + + FlightLogEntry.Base.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 FlightLogEntry.System.GetSerializationIndex() + return 0 +end + +---@return any[] An array of the elements as they will be serialized +function FlightLogEntry.System: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 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 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 FlightLogEntry.System:GetDataPairs( earliest_first ) + local o = {} ---@type table[] + + if ( earliest_first ) then + if self.arrtime then + table.insert(o, { l.ARRIVAL_DATE, self.formatDate(self.arrtime) }) + end + if self.deptime then + table.insert(o, { l.DEPARTURE_DATE, self.formatDate(self.deptime) }) + end + else + if self.deptime then + table.insert(o, { l.DEPARTURE_DATE, self.formatDate(self.deptime) }) + end + if self.arrtime then + table.insert(o, { l.ARRIVAL_DATE, self.formatDate(self.arrtime) }) + end + end + table.insert(o, { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }) + table.insert(o, { l.ALLEGIANCE, self.systemp:GetStarSystem().faction.name }) + + return o +end + +---@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 +FlightLogEntry.Custom = utils.class("FlightLogEntry.Custom", FlightLogEntry.Base) + + +---@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 FlightLogEntry.Custom:Constructor( systemp, time, money, location, entry ) + FlightLogEntry.Base.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 FlightLogEntry.Custom.GetSerializationIndex() + return 1 +end + +---@return any[] An array of the elements as they will be serialized +function FlightLogEntry.Custom: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 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 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 FlightLogEntry.Custom: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 FlightLogEntry.Custom:SupportsDelete() + return true +end + +---Delete this entry +---@return nil +function FlightLogEntry.Custom:Delete() + FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements - 1 + utils.remove_elem( FlightLogData, self ) +end + +---@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 +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 FlightLogEntry.Station:Constructor( systemp, deptime, money, entry ) + FlightLogEntry.Base.Constructor( self, deptime, entry ) + + self.systemp = systemp + self.deptime = deptime + self.money = money +end + +---@return integer A unique integer for this specific type +function FlightLogEntry.Station.GetSerializationIndex() + return 2 +end + +---@return any[] An array of the elements as they will be serialized +function FlightLogEntry.Station: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 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 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 FlightLogEntry.Station: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 + +return FlightLogEntry \ No newline at end of file diff --git a/data/modules/FlightLog/FlightLogExporter.lua b/data/modules/FlightLog/FlightLogExporter.lua new file mode 100644 index 00000000000..c6270dffc64 --- /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 ) + + 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( "player_logs", base_save_name .. extension ) + + formatter:open( FileSystem.Open( "USER", 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 diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index 81e83c4acbc..bc7c83d19e9 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -4,7 +4,8 @@ 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 FlightLogExporter = require 'modules.FlightLog.FlightLogExporter' local Format = require 'Format' local Color = _G.Color local Vector2 = _G.Vector2 @@ -19,13 +20,33 @@ 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 +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() + 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 --- Title text is gray, followed by the variable text: -local function headerText(title, text, wrap) +function writeLogEntry( entry, formatter, write_header ) + if write_header then + formatter:write( entry:GetLocalizedName() ):newline() + end + + for _, pair in pairs( entry:GetDataPairs( earliest_first ) ) do + formatter:headerText( pair[1], pair[2] ) + end +end + +local ui_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,25 +58,33 @@ local function headerText(title, text, wrap) end end +function ui_formatter:write( string ) + ui.text( string ) + ui.sameLine() + return self +end -local entering_text_custom = false -local entering_text_system = false -local entering_text_station = false +function ui_formatter:newline() + ui.text( "" ) + return self +end + +entering_text = false -- Display Entry text, and Edit button, to update flightlog -local function inputText(entry, counter, entering_text, log, str, clicked) - if #entry > 0 then - headerText(l.ENTRY, entry, true) +function inputText(entry, counter, entering_text, str, clicked) + 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, {"EnterReturnsTrue"}) + local updated_entry, return_pressed = ui.inputText("##" ..str..counter, entry:GetEntry(), {"EnterReturnsTrue"}) ui.popItemWidth() entering_text = counter if return_pressed then - log(counter, updated_entry) + entry:UpdateEntry(updated_entry) entering_text = -1 end end @@ -64,155 +93,127 @@ 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 counter = 0 - local was_clicked = false - for systemp, time, money, location, entry in FlightLog.GetCustomEntry() 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)) - - ::input:: - entering_text_custom = inputText(entry, counter, - entering_text_custom, FlightLog.UpdateCustomEntry, "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 +local function renderLog( formatter ) - 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() local counter = 0 local was_clicked = false - for systemp, deptime, money, entry in FlightLog.GetStationPaths() 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)) - - ::input:: - entering_text_station = inputText(entry, counter, - entering_text_station, FlightLog.UpdateStationEntry, "station", was_clicked) - ui.nextColumn() + for entry in FlightLog:GetLogEntries(getIncludedSet(), nil, earliest_first ) do + counter = counter + 1 + + writeLogEntry( entry, formatter, true ) + + if entry:CanHaveEntry() 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 - was_clicked = false - if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##station"..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 --- See comments on previous function -local function renderSystemLog() - local counter = 0 - local was_clicked = false - for systemp, arrtime, deptime, entry in FlightLog.GetSystemPaths() do - counter = counter + 1 +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 - 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) +local function displayFilterOptions() + ui.spacing() + local c + local flight_log = true; - ::input:: - entering_text_system = inputText(entry, counter, - entering_text_system, FlightLog.UpdateSystemEntry, "sys", was_clicked) - ui.nextColumn() + 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() + c,export_html = checkbox("HTML", export_html) - was_clicked = false - if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##system"..counter) then - was_clicked = true - goto input - end + ui.spacing() - ui.nextColumn() - ui.separator() + if ui.button(l.SAVE) then + FlightLogExporter.Export( getIncludedSet(), earliest_first, true, export_html) end + end -local function displayLog(logFn) + +local function drawFlightHistory() + 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) + ui.setColumnWidth(0, width - (iconSize.x*3)/2) + ui.setColumnWidth(1, (iconSize.x*3)/2) - logFn() - ui.columns(1) + renderLog(ui_formatter) end -local function drawFlightHistory() - ui.tabBarFont("#flightlog", { - - { name = l.LOG_CUSTOM, - draw = function() - ui.spacing() - -- input field for custom log: - 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 } - - }, pionillium.heading) -end +local function drawScreen() -local function drawLog () - ui.withFont(pionillium.body, function() - drawFlightHistory() + 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 @@ -221,7 +222,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 }) diff --git a/data/pigui/modules/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index efdcbe31889..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" @@ -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 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") diff --git a/src/lua/LuaFileSystem.cpp b/src/lua/LuaFileSystem.cpp index af64ff9c178..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); @@ -151,6 +191,42 @@ 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 a boolean + * indicating success + * + * 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); + + lua_pushboolean(l, f.IsDir()); + return 1; + } + catch (const std::invalid_argument&) { + luaL_error(l, "unable to create directory the argument is invalid"); + lua_pushboolean(l, 0); + return 1; + } +} + void LuaFileSystem::Register() { lua_State *l = Lua::manager->GetLuaState(); @@ -160,6 +236,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/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 b84923a2b04..950e11aa433 100644 --- a/src/lua/core/Sandbox.cpp +++ b/src/lua/core/Sandbox.cpp @@ -6,6 +6,8 @@ #include "LuaUtils.h" #include "core/Log.h" #include "utils.h" +#include "FileSystem.h" +#include "LuaFileSystem.h" static int l_d_null_userdata(lua_State *L) { @@ -104,6 +106,35 @@ 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) +{ + 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 path = LuaFileSystem::lua_path_to_fs_path(L, root_arg, path_arg, access_arg); + + if (path.length() == 0) + { + luaL_error(L, "attempt to access filesystem in an invalid user folder location"); + 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[] = { { "_G", luaopen_base }, { LUA_COLIBNAME, luaopen_coroutine }, @@ -112,6 +143,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 +197,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); 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) {