diff --git a/data/modules/FlightLog/FlightLog.lua b/data/modules/FlightLog/FlightLog.lua index e20b25c871a..889cc1e09b1 100644 --- a/data/modules/FlightLog/FlightLog.lua +++ b/data/modules/FlightLog/FlightLog.lua @@ -19,6 +19,10 @@ local Character = require 'Character' local FlightLogEntry = require 'modules.FlightLog.FlightLogEntries' +local function logInfo( msg ) + logWarning( "FlightLog: " .. msg ) +end + -- default values (private) ---@type integer local MaxTotalNonCustomElements = 3000 @@ -209,91 +213,6 @@ function FlightLog.MakeCustomEntry(text) FlightLog.AddEntry( FlightLogEntry.Custom.New( path, Game.time, Game.player:GetMoney(), location, text ) ) end --- --- Method: InsertCustomEntry --- --- Insert an element into custom flight log with all required fields --- --- > FlightLog.InsertCustomEntry(entry) --- --- Parameters: --- --- entry - table --- --- Entry: --- --- path - System path --- time - Game date --- money - Financial balance at time of record creation --- location - Array, with two strings: flight state, and relevant additional string --- text - Free text string --- ----@param entry table -function FlightLog.InsertCustomEntry(entry) - if entry.path and entry.time and entry.money and entry.location and entry.text then - FlightLog.AddEntry( FlightLogEntry.Custom.New( entry.path, entry.time, entry.money, entry.location, entry.text ) ) - return true - else - return false - end -end - --- --- Method: InsertSystemEntry --- --- Insert an element into system flight log with all required fields --- --- > FlightLog.InsertSystemEntry(entry) --- --- Parameters: --- --- entry - table --- --- Entry: --- --- path - System path, pointing to player's current system --- arrtime - Game date, arrival --- deptime - Game date, departure (optional) --- text - Free text string (optional) --- ----@param entry table -function FlightLog.InsertSystemEntry(entry) - if entry.path and (entry.arrtime or entry.deptime) then - FlightLog.AddEntry( FlightLogEntry.System.New( entry.path, entry.arrtime, entry.deptime, entry.text or "" ) ) - return true - else - return false - end -end - --- --- Method: InsertStationEntry --- --- Insert an element into station flight log with all required fields --- --- > FlightLog.InsertStationEntry(entry) --- --- Parameters: --- --- entry - table --- --- Entry: --- --- path - System path --- arrtime - Game date, arrival --- money - Financial balance at time of record creation --- text - Free text string (optional) --- ----@param entry table -function FlightLog.InsertStationEntry(entry) - if entry.path and entry.time and entry.money then - FlightLog.AddEntry( FlightLogEntry.Station.New( entry.path, entry.time, entry.money, entry.text or "" ) ) - return true - else - return false - end -end - -- Method: GetLogEntries -- -- Example: @@ -341,51 +260,43 @@ function FlightLog:GetLogEntries(types, maximum, earliest_first) end end --- LOGGING - --- onLeaveSystem -local AddSystemDepartureToLog = function (ship) - if not ship:IsPlayer() then return end - - FlightLog.AddEntry( FlightLogEntry.System.New( Game.system.path, nil, Game.time, nil ) ) -end - --- onEnterSystem -local AddSystemArrivalToLog = function (ship) - if not ship:IsPlayer() then return end - - FlightLog.AddEntry( FlightLogEntry.System.New( Game.system.path, Game.time, nil, nil ) ) -end - --- onShipDocked -local AddStationToLog = function (ship, station) - if not ship:IsPlayer() then return end - - FlightLog.AddEntry( FlightLogEntry.Station.New( station.path, Game.time, Game.player:GetMoney(), nil ) ) -end - -function FlightLog.OrganizeEntries() - - local function sortf( a, b ) - return a.sort_date > b.sort_date +local entryRecoveryLookup = { + ["FlightLogEntry.Custom"] = FlightLogEntry.Custom.Unserialize, + ["FlightLogEntry.System"] = FlightLogEntry.System.Unserialize, + ["FlightLogEntry.Station"] = FlightLogEntry.Station.Unserialize +} + +local function recoverEntry( entry, pathFixup, classLookup ) + local class = classLookup( entry ); + local unserializer = entryRecoveryLookup[class] + if unserializer == nil then + logWarning( "Flightlig unserialozer for " .. class .. " not found" ) + else + logInfo( "Found unserializer for " .. class ) end - - table.sort( FlightLogData, sortf ) - - CollapseSystemEvents() + if unserializer == nil then return nil end + return unserializer( entry, pathFixup ) end --- LOADING AND SAVING +--- @param loaded_data table The data to load +--- @param pathFixup func(table):SystemPath Optional function to fixup invalid system paths +--- @param classLookup func(table):string Lookup for classname. +--- @return boolean Did we succesfully read the data? +function FlightLog.ParseSavedData(loaded_data, pathFixup, classLookup) -local loaded_data -local onGameStart = function () + if loaded_data == nil then return false end + + if loaded_data.Version == 1 then - if loaded_data and loaded_data.Version == 1 then + logInfo( "Parsing Save Data v1") + FlightLogData = {} for _, v in pairs( loaded_data.System ) do - local data = { systemp = v[1], arrtime = v[2], deptime = nil, entry = v[4] } + local systempath = pathFixup ~= nil and pathFixup( v[1] ) or v[1] + + local data = { systemp = systempath, arrtime = v[2], deptime = nil, entry = v[4] } if ( data.arrtime ~= nil ) then -- entry table.insert(FlightLogData, FlightLogEntry.System.Unserialize( data )) @@ -399,21 +310,50 @@ local onGameStart = function () end for _, v in pairs( loaded_data.Station ) do - local data = { systemp = v[1], deptime = v[2], money = v[3], entry = v[4] } + + local systempath = pathFixup ~= nil and pathFixup( v[1] ) or v[1] + + local data = { systemp = systempath, deptime = v[2], money = v[3], entry = v[4] } table.insert(FlightLogData, FlightLogEntry.Station.Unserialize(data)) end for _, v in pairs( loaded_data.Custom ) do - local data = { systemp = v[1], time = v[2], money = v[3], location = v[4], entry = v[5] } + + local systempath = pathFixup ~= nil and pathFixup( v[1] ) or v[1] + + local data = { systemp = systempath, time = v[2], money = v[3], location = v[4], entry = v[5] } table.insert(FlightLogData, FlightLogEntry.Custom.Unserialize(data)) end - FlightLog.OrganizeEntries() - - elseif loaded_data and loaded_data.Version > 1 then - FlightLogData = loaded_data.Data + local function sortf( a, b ) + return a.sort_date > b.sort_date + end + + table.sort( FlightLogData, sortf ) + + CollapseSystemEvents() + + elseif loaded_data.Version > 1 then + if classLookup == nil then + logInfo( "Parsing Save Data v2 - no recovery with " .. #loaded_data.Data .. " elements" ) + + --- simple path, it's loaded, we don't need to lookup classes and unserialize. + FlightLogData = loaded_data.Data + else + logInfo( "Parsing Save Data v2 - recovery with " .. #loaded_data.Data .. " elements" ) + --- we're doing some recovery right now... + --- TODO: do we pass through the pathfixup here and do above too, right off the bat? + FlightLogData = {} + for _, e in pairs( loaded_data.Data ) do + local recovered = recoverEntry( e, pathFixup, classLookup ) + if recovered ~= nil then + table.insert( FlightLogData, recovered ) + end + end + end end + NonCustomElementCount = 0 for _, e in pairs(FlightLogData) do if not e:IsCustom() then @@ -421,10 +361,49 @@ local onGameStart = function () end end + logInfo( "Finished loading data with " .. #FlightLogData .. " elements and " .. NonCustomElementCount .. " non custom elements" ) + +end + +-- LOGGING + +-- onLeaveSystem +local AddSystemDepartureToLog = function (ship) + if not ship:IsPlayer() then return end + + FlightLog.AddEntry( FlightLogEntry.System.New( Game.system.path, nil, Game.time, nil ) ) +end + +-- onEnterSystem +local AddSystemArrivalToLog = function (ship) + if not ship:IsPlayer() then return end + + FlightLog.AddEntry( FlightLogEntry.System.New( Game.system.path, Game.time, nil, nil ) ) +end + +-- onShipDocked +local AddStationToLog = function (ship, station) + if not ship:IsPlayer() then return end + + FlightLog.AddEntry( FlightLogEntry.Station.New( station.path, Game.time, Game.player:GetMoney(), nil ) ) +end + + +-- LOADING AND SAVING + +local loaded_data = nil + +local onGameStart = function () + + if loaded_data then + FlightLog.ParseSavedData( loaded_data ) + end loaded_data = nil end local onGameEnd = function () + + logInfo( "Game end, clearing the logs" ) FlightLogData = {} NonCustomElementCount = 0 end diff --git a/data/modules/FlightLog/FlightLogEntries.lua b/data/modules/FlightLog/FlightLogEntries.lua index 57f59d15a70..e2511186188 100644 --- a/data/modules/FlightLog/FlightLogEntries.lua +++ b/data/modules/FlightLog/FlightLogEntries.lua @@ -11,17 +11,15 @@ local Serializer = require 'Serializer' local Character = require 'Character' +local Location = require 'pigui.modules.new-game-window.location' + -- 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 +local FlightLogEntry = {} ---@class FlightLogEntry.Base ---A generic log entry: @@ -30,8 +28,11 @@ FlightLogEntry.TotalDefaultElements = 0 ---@field protected always_custom boolean? Is this always treated as a custom entry (so not auto deleted) FlightLogEntry.Base = utils.class("FlightLogEntry") +---@type func(integer) +FlightLogEntry.Base.non_custom_count_change = nil; + ---@return string Description of this type -function FlightLogEntry.Base:GetType() end +function FlightLogEntry.Base:GetType() assert(false, tostring(self.class) .. ":GetType() should be overridden.") end ---@return boolean true if this is considered to have an entry function FlightLogEntry.Base:CanHaveEntry() @@ -61,15 +62,11 @@ function FlightLogEntry.Base:IsCustom() end ---@return string The name for this log entry type -function FlightLogEntry.Base:GetLocalizedName() - return "Error" -end +function FlightLogEntry.Base:GetLocalizedName() assert(false, tostring(self.class) .. ":GetLocalizedName() should be overridden.") 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 +function FlightLogEntry.Base:GetDataPairs( earliest_first ) assert(false, tostring(self.class) .. ":GetDataPairs() should be overridden.") 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 @@ -77,14 +74,14 @@ end function FlightLogEntry.Base:UpdateEntry( entry ) if self:IsCustom() then - FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements-1 + self.non_custom_count_change( -1 ) end if entry and #entry == 0 then entry = nil end self.entry = entry if self:IsCustom() then - FlightLogEntry.TotalDefaultElements = FlightLogEntry.TotalDefaultElements+1 + self.non_custom_count_change( 1 ) end end @@ -99,9 +96,6 @@ function FlightLogEntry.Base:Constructor( sort_date, entry, always_custom ) 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 @@ -170,7 +164,6 @@ function FlightLogEntry.System:GetType() return "System" end - ---@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 @@ -196,8 +189,10 @@ function FlightLogEntry.System:Serialize() return { systemp = self.systemp, arrtime = self.arrtime, deptime = self.deptime, entry = self.entry } end -function FlightLogEntry.System.Unserialize( data ) - return FlightLogEntry.System.New(data.systemp, data.arrtime, data.deptime, data.entry ) +function FlightLogEntry.System.Unserialize( data, pathFixup ) + local systemp = pathFixup ~= nil and pathFixup( data.systemp ) or data.systemp + + return FlightLogEntry.System.New(systemp, data.arrtime, data.deptime, data.entry ) end Serializer:RegisterClass("FlightLogEntry.System", FlightLogEntry.System) @@ -207,20 +202,49 @@ function FlightLogEntry.System:GetLocalizedName() return l.LOG_SYSTEM; end +-- local function asFaction(path) +-- if path:IsSectorPath() then return l.UNKNOWN_FACTION end +-- return path:GetStarSystem().faction.name +-- end + +-- local function asStation(path) +-- if not path:IsBodyPath() then return l.NO_AVAILABLE_DATA end +-- local system = path:GetStarSystem() +-- local systembody = system:GetBodyByPath(path) +-- local station_type = "FLIGHTLOG_" .. systembody.type +-- return string.interp(l[station_type], { +-- primary_info = systembody.name, +-- secondary_info = systembody.parent.name +-- }) +-- end + +-- local function asSystem(path) +-- return ui.Format.SystemPath(path) +-- end + local function asFaction(path) - if path:IsSectorPath() then return l.UNKNOWN_FACTION end - return path:GetStarSystem().faction.name + if path:IsSectorPath() then return l.UNKNOWN_FACTION end + return Location:getGalaxy():GetStarSystem(path).faction.name end local function asStation(path) - if not path:IsBodyPath() then return l.NO_AVAILABLE_DATA end - local system = path:GetStarSystem() - local systembody = system:GetBodyByPath(path) - local station_type = "FLIGHTLOG_" .. systembody.type - return string.interp(l[station_type], { - primary_info = systembody.name, - secondary_info = systembody.parent.name - }) + if not path:IsBodyPath() then return l.NO_AVAILABLE_DATA end + local system = Location:getGalaxy():GetStarSystem(path) + local systembody = system:GetBodyByPath(path) + local station_type = "FLIGHTLOG_" .. systembody.type + return string.interp(l[station_type], { + primary_info = systembody.name, + secondary_info = systembody.parent.name + }) +end + +local function asSystem(path) + local sectorString = "(" .. path.sectorX .. ", " .. path.sectorY .. ", " .. path.sectorZ .. ")" + if path:IsSectorPath() then + return l.UNKNOWN_LOCATION_IN_SECTOR_X:interp{ sector = sectorString } + end + local system = Location:getGalaxy():GetStarSystem(path) + return system.name .. " " .. sectorString end ---@param earliest_first boolean set to true if your sort order is to show the earlist first dates @@ -243,7 +267,7 @@ function FlightLogEntry.System:GetDataPairs( earliest_first ) 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.IN_SYSTEM, asSystem(self.systemp) }) table.insert(o, { l.ALLEGIANCE, asFaction(self.systemp) }) return o @@ -279,8 +303,9 @@ function FlightLogEntry.Custom:Serialize() return { systemp = self.systemp, time = self.time, money = self.money, location = self.location, entry = self.entry } end -function FlightLogEntry.Custom.Unserialize( data ) - return FlightLogEntry.Custom.New( data.systemp, data.time, data.money, data.location, data.entry ) +function FlightLogEntry.Custom.Unserialize( data, pathFixup ) + local systemp = pathFixup ~= nil and pathFixup( data.systemp ) or data.systemp + return FlightLogEntry.Custom.New( systemp, data.time, data.money, data.location, data.entry ) end Serializer:RegisterClass("FlightLogEntry.Custom", FlightLogEntry.Custom) @@ -296,7 +321,7 @@ 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.IN_SYSTEM, asSystem(self.systemp) }, { l.ALLEGIANCE, asFaction(self.systemp) }, { l.CASH, Format.Money(self.money) } } @@ -307,13 +332,6 @@ 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 @@ -325,6 +343,7 @@ function FlightLogEntry.Station:GetType() return "Station" end + ---@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 @@ -341,8 +360,9 @@ function FlightLogEntry.Station:Serialize() return { systemp = self.systemp, deptime = self.deptime, money = self.money, entry = self.entry } end -function FlightLogEntry.Station.Unserialize( data ) - return FlightLogEntry.Station.New( data.systemp, data.deptime, data.money, data.entry ) +function FlightLogEntry.Station.Unserialize( data, pathFixup ) + local systemp = pathFixup ~= nil and pathFixup( data.systemp ) or data.systemp + return FlightLogEntry.Station.New( systemp, data.deptime, data.money, data.entry ) end Serializer:RegisterClass("FlightLogEntry.Station", FlightLogEntry.Station) @@ -360,7 +380,7 @@ function FlightLogEntry.Station:GetDataPairs( earliest_first ) return { { l.DATE, self.formatDate(self.deptime) }, { l.STATION, asStation(self.systemp) }, - { l.IN_SYSTEM, ui.Format.SystemPath(self.systemp) }, + { l.IN_SYSTEM, asSystem(self.systemp) }, { l.ALLEGIANCE, asFaction(self.systemp) }, { l.CASH, Format.Money(self.money) }, } diff --git a/data/modules/FlightLog/FlightLogRenderer.lua b/data/modules/FlightLog/FlightLogRenderer.lua new file mode 100644 index 00000000000..1f61464a702 --- /dev/null +++ b/data/modules/FlightLog/FlightLogRenderer.lua @@ -0,0 +1,169 @@ + +local ui = require 'pigui' +local FlightLog = require 'modules.FlightLog.FlightLog' +local Vector2 = _G.Vector2 +local Color = _G.Color +local Lang = require 'Lang' +local l = Lang.GetResource("ui-core") + +local icons = ui.theme.icons + +local gray = Color(145, 145, 145) + + +local iconSize = ui.rescaleUI(Vector2(28, 28)) +local buttonSpaceSize = iconSize + +local FlightLogRenderer = {} + + +FlightLogRenderer.earliestFirst = true +FlightLogRenderer.includedSet = { System = true, Custom = true, Station = true } + +local function writeLogEntry( entry, formatter, write_header ) + if write_header then + formatter:write( entry:GetLocalizedName() ):newline() + end + + for _, pair in ipairs( entry:GetDataPairs( FlightLogRenderer.earliestFirst ) ) 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() + if wrap then + -- TODO: limit width of text box! + ui.textWrapped(text) + else + ui.text(text) + 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 entering_text = false + +-- Display Entry text, and Edit button, to update flightlog +local function inputText(entry, counter, entering_text, str) + + if entering_text == counter then + ui.spacing() + ui.pushItemWidth(-1.0) + local updated_entry, return_pressed = ui.inputText("##" ..str..counter, entry:GetEntry(), {"EnterReturnsTrue"}) + ui.popItemWidth() + if return_pressed then + entry:UpdateEntry(updated_entry) + entering_text = -1 + end + elseif entry:HasEntry() then + ui_formatter:headerText(l.ENTRY, entry:GetEntry(), true) + end + + ui.spacing() + return entering_text +end + +local function renderLog( formatter, interactive ) + + ui.spacing() + if interactive then + -- 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() + end + + local counter = 0 + for entry in FlightLog:GetLogEntries(FlightLogRenderer.includedSet, nil, FlightLogRenderer.earliestFirst ) do + counter = counter + 1 + + writeLogEntry( entry, formatter, true ) + + if interactive then + if entry:CanHaveEntry() then + entering_text = inputText(entry, counter, + entering_text, "custom") + ui.nextColumn() + + if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##custom"..counter) then + entering_text = counter + end + else + ui.nextColumn() + end + + 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() + else + if entry:HasEntry() then + ui_formatter:headerText(l.ENTRY, entry:GetEntry(), true) + end + end + + ui.separator() + ui.spacing() + end + if counter == 0 then + ui.text(l.NONE) + end +end + +function FlightLogRenderer.displayFilterOptions() + ui.spacing() + local c + local flight_log = true; + + c,FlightLogRenderer.includedSet.Custom = ui.checkbox(l.LOG_CUSTOM, FlightLogRenderer.includedSet.Custom) + c,FlightLogRenderer.includedSet.Station = ui.checkbox(l.LOG_STATION, FlightLogRenderer.includedSet.Station) + c,FlightLogRenderer.includedSet.System = ui.checkbox(l.LOG_SYSTEM, FlightLogRenderer.includedSet.System) + ui.separator() + c,FlightLogRenderer.earliestFirst = ui.checkbox(l.LOG_EARLIEST_FIRST, FlightLogRenderer.earliestFirst) +end + + +function FlightLogRenderer.drawFlightHistory( interactive ) + + ui.spacing() + + if interactive then + -- 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) + end + + renderLog(ui_formatter, interactive) +end + + +return FlightLogRenderer + + diff --git a/data/pigui/modules/info-view/06-flightlog.lua b/data/pigui/modules/info-view/06-flightlog.lua index bc156df4d99..c4ffac61671 100644 --- a/data/pigui/modules/info-view/06-flightlog.lua +++ b/data/pigui/modules/info-view/06-flightlog.lua @@ -7,125 +7,15 @@ 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 FlightLogRenderer = require 'modules.FlightLog.FlightLogRenderer' local pionillium = ui.fonts.pionillium local icons = ui.theme.icons -local gray = Color(145, 145, 145) - local l = Lang.GetResource("ui-core") -local iconSize = ui.rescaleUI(Vector2(28, 28)) -local buttonSpaceSize = iconSize - -local earliestFirst = true local exportHtml = true -local includedSet = { System = true, Custom = true, Station = true } - -local function writeLogEntry( entry, formatter, write_header ) - if write_header then - formatter:write( entry:GetLocalizedName() ):newline() - end - - for _, pair in ipairs( entry:GetDataPairs( earliestFirst ) ) 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() - if wrap then - -- TODO: limit width of text box! - ui.textWrapped(text) - else - ui.text(text) - 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 entering_text = false - --- Display Entry text, and Edit button, to update flightlog -local function inputText(entry, counter, entering_text, str) - - if entering_text == counter then - ui.spacing() - ui.pushItemWidth(-1.0) - local updated_entry, return_pressed = ui.inputText("##" ..str..counter, entry:GetEntry(), {"EnterReturnsTrue"}) - ui.popItemWidth() - if return_pressed then - entry:UpdateEntry(updated_entry) - entering_text = -1 - end - elseif entry:HasEntry() then - ui_formatter:headerText(l.ENTRY, entry:GetEntry(), true) - end - - ui.spacing() - return entering_text -end - -local function renderLog( formatter ) - - 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() - - local counter = 0 - for entry in FlightLog:GetLogEntries(includedSet, nil, earliestFirst ) do - counter = counter + 1 - - writeLogEntry( entry, formatter, true ) - - if entry:CanHaveEntry() then - entering_text = inputText(entry, counter, - entering_text, "custom") - ui.nextColumn() - - if ui.iconButton(icons.pencil, buttonSpaceSize, l.EDIT .. "##custom"..counter) then - entering_text = counter - end - else - ui.nextColumn() - end - - 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 Windows = { exportButtonWindow = layout.NewWindow(l.LOG_EXPORT, nil, ui.WindowFlags {"AlwaysAutoResize", "NoFocusOnAppearing", "NoBringToFrontOnFocus", "NoSavedSettings"}), @@ -148,38 +38,11 @@ function Windows.exportButtonWindow.Show() c,exportHtml = ui.checkbox(l.LOG_HTML, exportHtml) ui.sameLine() if ui.button(l.SAVE) then - FlightLogExporter.Export( includedSet, earliestFirst, true, exportHtml) + FlightLogExporter.Export( FlightLogRenderer.includedSet, FlightLogRenderer.earliestFirst, true, exportHtml) end end -local function displayFilterOptions() - ui.spacing() - local c - local flight_log = true; - - c,includedSet.Custom = ui.checkbox(l.LOG_CUSTOM, includedSet.Custom) - c,includedSet.Station = ui.checkbox(l.LOG_STATION, includedSet.Station) - c,includedSet.System = ui.checkbox(l.LOG_SYSTEM, includedSet.System) - ui.separator() - c,earliestFirst = ui.checkbox(l.LOG_EARLIEST_FIRST, earliestFirst) -end - - -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*3)/2) - ui.setColumnWidth(1, (iconSize.x*3)/2) - - renderLog(ui_formatter) -end - local function drawScreen() local width = ui.getContentRegion().x @@ -190,13 +53,13 @@ local function drawScreen() ui.child( "FlightLogList", function() ui.withFont(pionillium.body, function() - drawFlightHistory() + FlightLogRenderer.drawFlightHistory( true ) end) end) ui.nextColumn() ui.child( "FlightLogConfig", function() ui.withFont(pionillium.body, function() - displayFilterOptions() + FlightLogRenderer.displayFilterOptions() end) end) diff --git a/data/pigui/modules/new-game-window/class.lua b/data/pigui/modules/new-game-window/class.lua index 6e6e93054be..3f9a0611fdf 100644 --- a/data/pigui/modules/new-game-window/class.lua +++ b/data/pigui/modules/new-game-window/class.lua @@ -21,7 +21,7 @@ local Defs = require 'pigui.modules.new-game-window.defs' local Layout = require 'pigui.modules.new-game-window.layout' local Recovery = require 'pigui.modules.new-game-window.recovery' local StartVariants = require 'pigui.modules.new-game-window.start-variants' -local FlightLogParam = require 'pigui.modules.new-game-window.flight-log' +local FlightLogParam = require 'pigui.modules.new-game-window.flight-log-tab' local Helpers = require 'pigui.modules.new-game-window.helpers' local Game = require 'Game' @@ -96,9 +96,11 @@ local function updateParams() end end -local function setStartVariant(variant) +local function setStartVariant(variant, is_recovering) for _, param in ipairs(Layout.UpdateOrder) do - param:fromStartVariant(variant) + if not is_recovering or not param.exclude_on_recovery then + param:fromStartVariant(variant) + end end end @@ -170,31 +172,6 @@ local function startGame(gameParams) cargoMgr:AddCommodity(Commodities[id], amount) end - for _, entry in ipairs(gameParams.flightlog.Custom) do - if FlightLog.InsertCustomEntry(entry) then - -- ok then - elseif entry.text then - -- allow a custom log entry containing only text - -- because when starting a new game we only have text - FlightLog.MakeCustomEntry(entry.text) - else - logWarning("Wrong entry for the custom flight log") - end - end - - for _, entry in ipairs(gameParams.flightlog.System) do - if not FlightLog.InsertSystemEntry(entry) then - logWarning("Wrong entry for the system flight log") - end - end - - for _, entry in ipairs(gameParams.flightlog.Station) do - if not FlightLog.InsertStationEntry(entry) then - logWarning("Wrong entry for the station flight log") - end - end - FlightLog.OrganizeEntries() - if gameParams.autoExec then gameParams.autoExec() end @@ -269,7 +246,7 @@ NewGameWindow = ModalWindow.New("New Game", function() local action = profileCombo.actions[ret + 1] if action == 'DO_UNLOCK' then Layout.setLock(false) - FlightLogParam.value.Custom = {{ text = "Custom start of the game - for the purpose of debugging or cheat." }} + FlightLog.MakeCustomEntry( "Custom start of the game - for the purpose of debugging or cheat." ) else setStartVariant(StartVariants.item(ret + 1)) end @@ -316,9 +293,7 @@ function NewGameWindow.restoreSaveGame(selectedSave) local report = lui.ATTEMPTING_TO_RECOVER_GAME_PROGRESS_FROM_SAVE_X:interp{ save = selectedSave } -- first variant as default value - setStartVariant(StartVariants.item(1)) - -- except for clearing the log - FlightLogParam:fromStartVariant({}) + setStartVariant(StartVariants.item(1),true) report = report .. "\n\n" .. string.format(lui.FILE_SAVEGAME_VERSION, tostring(saveGame.version)) report = report .. "\n" .. string.format(lui.CURRENT_SAVEGAME_VERSION, tostring(Game.CurrentSaveVersion())) for _, param in ipairs(Layout.UpdateOrder) do diff --git a/data/pigui/modules/new-game-window/flight-log-tab.lua b/data/pigui/modules/new-game-window/flight-log-tab.lua new file mode 100644 index 00000000000..5b85acdfe56 --- /dev/null +++ b/data/pigui/modules/new-game-window/flight-log-tab.lua @@ -0,0 +1,142 @@ +-- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txtl + +local ui = require 'pigui' +local Lang = require 'Lang' +local lui = Lang.GetResource("ui-core") +local Format = require 'Format' +local SystemPath = require 'SystemPath' + +local Defs = require 'pigui.modules.new-game-window.defs' +local GameParam = require 'pigui.modules.new-game-window.game-param' +local Location = require 'pigui.modules.new-game-window.location' +local Helpers = require 'pigui.modules.new-game-window.helpers' + +local FlightLog = require 'modules.FlightLog.FlightLog' + +-- TODO: move this into the pygui space - probably! +local FlightLogRenderer = require 'modules.FlightLog.FlightLogRenderer' + + +local FlightLogTab = GameParam.New(lui.FLIGHT_LOG, "flightlog") + +FlightLogTab.version = 1 +FlightLogTab.exclude_on_recovery = true + +function FlightLogTab:fromStartVariant(variant) + if ( variant.logmsg ) then + FlightLog.MakeCustomEntry( variant.logmsg ) + end +end + +function FlightLogTab:draw() + local h = ui.getCursorPos().y + + --TODO WE MUST ADD THE FILTERING OPTIONS SOMEHOW? + + -- local changed, newValue = ui.combo("##select_log", cbCurrentValue, cbValues) + -- if changed then cbCurrentValue = newValue end + + -- local section = sections[cbCurrentValue] + -- local entries = self.value[section] + + -- ui.separator() + + h = ui.getCursorPos().y - h + ui.child("flightlog", Vector2(Defs.contentRegion.x, Defs.contentRegion.y - h), function() + FlightLogRenderer.drawFlightHistory( false ) + end) +end + +local function logPathWarning( path, msg ) + local si = path.systemIndex or "nil" + local bi = path.bodyIndex or "nil" + logWarning( msg .. " [" .. path.sectorX .. "," .. path.sectorY .. "," .. path.sectorZ .. ":" .. si .. ":" .. bi .. "]" ) +end + +-- an invalid body path will be read as a system path only +-- an invalid system path will be read as a sector path only +local function systemPathFromTable(t) + if not t then + logWarning( "No object found to fetch a path from" ) + return nil + end + t = t.inner + + -- something is completely wrong + if not t or #t ~= 5 then + logWarning( "Inner path object is missing or too small" ) + return nil + end + + local path = SystemPath.New(t[1], t[2], t[3], t[4], t[5]) + + if path:IsSectorPath() then + logPathWarning( path, "Returning sector path" ) + return path + end + + local system = Location:getGalaxy():GetStarSystem(path) + if not system then + logPathWarning( path, "Reducing system path to sector path" ) + return SystemPath:SectorOnly() + end + if path:IsSystemPath() then + logPathWarning( path, "Returning system path" ) + return path + end + + local paths = system:GetBodyPaths() + if t[5] >= #paths then + logPathWarning( path, "Returning body path as system path" ) + return path:SystemOnly() + end + + local systembody = system:GetBodyByPath(path) + if systembody.superType ~= 'STARPORT' then + logPathWarning( path, "Returning body path as system path as it's not a starport" ) + return path:SystemOnly() + end + + logPathWarning( path, "Returning path" ) + + return path +end + +FlightLogTab.reader = Helpers.versioned {{ + version = 89, + fnc = function(saveGame) + + local data, errorString = Helpers.getByPath(saveGame, "lua_modules_json/FlightLog") + if errorString then return nil, errorString end + + FlightLog.ParseSavedData( data, systemPathFromTable, Helpers.getLuaClass ) + + return {} + end + +}, { + + version = 90, + fnc = function(saveGame) + + local data, errorString = Helpers.getByPath(saveGame, "lua_modules_json/FlightLog") + if errorString then return nil, errorString end + + FlightLog.ParseSavedData( data, systemPathFromTable, Helpers.getLuaClass ) + + return {} + + end +}} + +FlightLogTab.updateLayout = false +FlightLogTab.updateParams = false + +function FlightLogTab:isValid() + return true +end + +FlightLogTab.TabName = lui.FLIGHT_LOG + +return FlightLogTab diff --git a/data/pigui/modules/new-game-window/flight-log.lua b/data/pigui/modules/new-game-window/flight-log.lua deleted file mode 100644 index cbff29238d9..00000000000 --- a/data/pigui/modules/new-game-window/flight-log.lua +++ /dev/null @@ -1,247 +0,0 @@ --- Copyright © 2008-2024 Pioneer Developers. See AUTHORS.txt for details --- Licensed under the terms of the GPL v3. See licenses/GPL-3.txtl - -local ui = require 'pigui' -local Lang = require 'Lang' -local lui = Lang.GetResource("ui-core") -local Format = require 'Format' -local SystemPath = require 'SystemPath' - -local Defs = require 'pigui.modules.new-game-window.defs' -local GameParam = require 'pigui.modules.new-game-window.game-param' -local Location = require 'pigui.modules.new-game-window.location' -local Helpers = require 'pigui.modules.new-game-window.helpers' - -local FlightLog = GameParam.New(lui.FLIGHT_LOG, "flightlog") - -FlightLog.value = { - Custom = {}, - System = {}, - Station = {} -} - -FlightLog.version = 1 - -function FlightLog:fromStartVariant(variant) - self.value.System = {} - self.value.Station = {} - self.value.Custom = { - variant.logmsg and { text = variant.logmsg } - } -end - --- Title text is gray, followed by the variable text: -local function headerText(title, text, wrap) - if not text then return end - ui.textColored(ui.theme.colors.grey, string.gsub(title, ":", "") .. ":") - ui.sameLine() - if wrap then - ui.textWrapped(text) - else - ui.text(text) - end -end - -local function asFaction(path) - if path:IsSectorPath() then return lui.UNKNOWN_FACTION end - return Location:getGalaxy():GetStarSystem(path).faction.name -end - -local function asStation(path) - if not path:IsBodyPath() then return lui.NO_AVAILABLE_DATA end - local system = Location:getGalaxy():GetStarSystem(path) - local systembody = system:GetBodyByPath(path) - local station_type = "FLIGHTLOG_" .. systembody.type - return string.interp(lui[station_type], { - primary_info = systembody.name, - secondary_info = systembody.parent.name - }) -end - -local function asPath(path) - local sectorString = "(" .. path.sectorX .. ", " .. path.sectorY .. ", " .. path.sectorZ .. ")" - if path:IsSectorPath() then - return lui.UNKNOWN_LOCATION_IN_SECTOR_X:interp{ sector = sectorString } - end - local system = Location:getGalaxy():GetStarSystem(path) - return system.name .. " " .. sectorString -end - --- Based on flight state, compose a reasonable string for location -local function composeLocationString(location) - return string.interp(lui["FLIGHTLOG_"..location[1]], { - primary_info = location[2], - secondary_info = location[3] or "", - tertiary_info = location[4] or "", - }) -end - -local cbCurrentValue = 0 -local cbValues = { lui.LOG_CUSTOM, lui.LOG_SYSTEM, lui.LOG_STATION } -local sections = { [0] = "Custom", [1] = "System", [2] = "Station" } -local entryTemplates = { - Custom = { - { id = "time", label = lui.DATE, render = Format.Date }, - { id = "location", label = lui.LOCATION, render = composeLocationString }, - { id = "path", label = lui.IN_SYSTEM, render = asPath }, - { id = "path", label = lui.ALLEGIANCE, render = asFaction }, - { id = "money", label = lui.CASH, render = Format.Money }, - { id = "text", label = lui.ENTRY, render = tostring, wrap = true }, - }, - System = { - { id = "arrtime", label = lui.ARRIVAL_DATE, render = Format.Date }, - { id = "deptime", label = lui.DEPARTURE_DATE, render = Format.Date }, - { id = "path", label = lui.IN_SYSTEM, render = asPath }, - { id = "path", label = lui.ALLEGIANCE, render = asFaction }, - { id = "text", label = lui.ENTRY, render = tostring, wrap = true }, - }, - Station = { - { id = "time", label = lui.DATE, render = Format.Date }, - { id = "path", label = lui.STATION, render = asStation }, - { id = "path", label = lui.IN_SYSTEM, render = asPath }, - { id = "path", label = lui.ALLEGIANCE, render = asFaction }, - { id = "money", label = lui.CASH, render = Format.Money }, - { id = "text", label = lui.ENTRY, render = tostring, wrap = true }, - }, -} - -local function renderEntry(template, entry) - for _, param in ipairs(template) do - local value = entry[param.id] - if value and value ~= "" then -- do not show an empty entry too - headerText(param.label, param.render(value), param.wrap) - end - end -end - -function FlightLog:draw() - local h = ui.getCursorPos().y - local changed, newValue = ui.combo("##select_log", cbCurrentValue, cbValues) - if changed then cbCurrentValue = newValue end - - local section = sections[cbCurrentValue] - local entries = self.value[section] - - ui.separator() - - h = ui.getCursorPos().y - h - ui.child("flightlog", Vector2(Defs.contentRegion.x, Defs.contentRegion.y - h), function() - if #entries == 0 then - ui.text(lui.NONE) - return - end - -- draw separators only between elements - local sep = function() end - for _, entry in ipairs(entries) do - sep() - renderEntry(entryTemplates[section], entry) - sep = ui.separator - end - end) -end - --- an invalid body path will be read as a system path only --- an invalid system path will be read as a sector path only -local function systemPathFromTable(t) - - -- something is completely wrong - if not t or #t ~= 5 then return nil end - - local path = SystemPath.New(t[1], t[2], t[3], t[4], t[5]) - - if path:IsSectorPath() then return path end - - local system = Location:getGalaxy():GetStarSystem(path) - if not system then - return SystemPath:SectorOnly() - end - if path:IsSystemPath() then return path end - - local paths = system:GetBodyPaths() - if t[5] >= #paths then - return path:SystemOnly() - end - - local systembody = system:GetBodyByPath(path) - if systembody.superType ~= 'STARPORT' then - return path:SystemOnly() - end - - return path -end - -FlightLog.reader = Helpers.versioned {{ - version = 89, - fnc = function(saveGame) - - local value = { Custom = {}, System = {}, Station = {} } - - local custom, errorString = Helpers.getByPath(saveGame, "lua_modules_json/FlightLog/Custom") - if errorString then return nil, errorString end - assert(custom) - for _, entry in ipairs(custom) do - local parsed = {} - if entry then - parsed.time = entry[2] - local loc = entry[4] - parsed.location = loc and { loc[1], loc[2], loc[3] } - parsed.path = systemPathFromTable(entry[1].inner) - parsed.money = entry[3] - parsed.text = entry[5] - end - if not entry or not parsed.time or not parsed.location or not parsed.path or not parsed.money or not parsed.text then - return nil, lui.UNKNOWN_CUSTOM_LOG_ENTRY_FORMAT - end - table.insert(value.Custom, parsed) - end - - local system - system, errorString = Helpers.getByPath(saveGame, "lua_modules_json/FlightLog/System") - if errorString then return nil, errorString end - assert(system) - for _, entry in ipairs(system) do - local parsed = {} - if entry then - parsed.arrtime = entry[2] - parsed.deptime = entry[3] - parsed.path = systemPathFromTable(entry[1].inner) - parsed.text = entry[4] - end - if not entry or not parsed.path then - return nil, lui.UNKNOWN_SYSTEM_LOG_ENTRY_FORMAT - end - table.insert(value.System, parsed) - end - - local station - station, errorString = Helpers.getByPath(saveGame, "lua_modules_json/FlightLog/Station") - if errorString then return nil, errorString end - assert(station) - for _, entry in ipairs(station) do - local parsed = {} - if entry then - parsed.time = entry[2] - parsed.path = systemPathFromTable(entry[1].inner) - parsed.money = entry[3] - parsed.text = entry[4] - end - if not entry or not parsed.time or not parsed.path or not parsed.money then - return nil, lui.UNKNOWN_STATION_LOG_ENTRY_FORMAT - end - table.insert(value.Station, parsed) - end - - return value - end -}} - -FlightLog.updateLayout = false -FlightLog.updateParams = false - -function FlightLog:isValid() - return true -end - -FlightLog.TabName = lui.FLIGHT_LOG - -return FlightLog diff --git a/data/pigui/modules/new-game-window/game-param.lua b/data/pigui/modules/new-game-window/game-param.lua index d6782a183d6..8083e4e3ddc 100644 --- a/data/pigui/modules/new-game-window/game-param.lua +++ b/data/pigui/modules/new-game-window/game-param.lua @@ -18,6 +18,7 @@ function GameParam:Constructor(name, path) self.value = nil self.hidden = false self.noName = false + self.exclude_on_recovery = false end function GameParam:isValid() assert(false, tostring(self.name) .. ":isValid() should be overridden.") end diff --git a/data/pigui/modules/new-game-window/helpers.lua b/data/pigui/modules/new-game-window/helpers.lua index 891e7ea8739..4f8b334ed4c 100644 --- a/data/pigui/modules/new-game-window/helpers.lua +++ b/data/pigui/modules/new-game-window/helpers.lua @@ -191,4 +191,29 @@ function Helpers.getPlayerShipParameter(saveGame, paramPath) return param end +-- +-- Function: Helpers.getLuaClass +-- +-- Retrieves the value of the 'class' field from the metatable. It should have +-- been there when the save was unpickled. +-- +-- Example: +-- +-- > local entryType = Helpers.getLuaClass(entry) +-- +-- Parameters: +-- +-- tbl - table +-- +-- Returns: +-- +-- typename - string? +-- +---@param tbl table +---@return string? typename +function Helpers.getLuaClass(tbl) + local typeTable = getmetatable(tbl) + if typeTable then return typeTable.class end +end + return Helpers diff --git a/data/pigui/modules/new-game-window/layout.lua b/data/pigui/modules/new-game-window/layout.lua index 6b1a4abb217..a0dec4ec998 100644 --- a/data/pigui/modules/new-game-window/layout.lua +++ b/data/pigui/modules/new-game-window/layout.lua @@ -6,7 +6,7 @@ local Crew = require 'pigui.modules.new-game-window.crew' local Ship = require 'pigui.modules.new-game-window.ship' local Location = require 'pigui.modules.new-game-window.location' local Summary = require 'pigui.modules.new-game-window.summary' -local FlightLog = require 'pigui.modules.new-game-window.flight-log' +local FlightLogTab = require 'pigui.modules.new-game-window.flight-log-tab' local Layout = {} @@ -26,11 +26,11 @@ Layout.UpdateOrder = { Ship.Fuel, Location, Location.Time, - FlightLog, + FlightLogTab, Summary.Description } -Layout.Tabs = { Summary, Crew, Ship, Location, FlightLog } +Layout.Tabs = { Summary, Crew, Ship, Location, FlightLogTab } function Layout.updateLayout(contentRegion) diff --git a/data/pigui/modules/new-game-window/recovery.lua b/data/pigui/modules/new-game-window/recovery.lua index 73c01222966..23a329d2fd8 100644 --- a/data/pigui/modules/new-game-window/recovery.lua +++ b/data/pigui/modules/new-game-window/recovery.lua @@ -30,6 +30,8 @@ local fixupDoc = Helpers.versioned {{ local refs = {} + local typeTables = {} + local function unpickle(tbl) local result = {} tbl = tbl.table @@ -59,6 +61,14 @@ local fixupDoc = Helpers.versioned {{ local result = {} if tbl.ref then + if tbl.lua_class then + local typeTable = typeTables[tbl.lua_class] + if not typeTable then + typeTable = { class = tbl.lua_class } + typeTables[tbl.lua_class] = typeTable + end + setmetatable(result, typeTable) + end cache[tbl.ref] = result tbl = refs[tbl.ref] else diff --git a/src/core/Log.cpp b/src/core/Log.cpp index ef5fef861ec..002b7440061 100644 --- a/src/core/Log.cpp +++ b/src/core/Log.cpp @@ -15,6 +15,11 @@ #include #include +#ifdef _MSC_VER +#define WIN32_LEAN_AND_MEAN +#include +#endif + namespace Log { Logger s_defaultLog; @@ -86,6 +91,20 @@ void Log::Logger::WriteLog(Time::DateTime time, Severity sv, std::string_view ms { std::string &svName = s_severityNames.at(sv); +#ifdef _MSC_VER + // TODO: unicode.., max_severity etc. + const static int BUFFER_SIZE = 1024; + char buffer[BUFFER_SIZE]; + const char* m = msg.data(); + if (!msg.empty() && msg.back() == '\n') + snprintf(buffer, BUFFER_SIZE - 1, "%s %.*s", svName.c_str(), msg.size(), msg.data()); + else + snprintf(buffer, BUFFER_SIZE - 1, "%s %.*s\n", svName.c_str(), msg.size(), msg.data()); + + OutputDebugStringA(buffer); +#endif + + /* Don't output to the console on Windows Builds on /subsystem:WINDOWS will not usually have a console and fmt::print will throw an exception in this case */