diff --git a/data/lang/core/en.json b/data/lang/core/en.json index 957997e964..ad2b64753a 100644 --- a/data/lang/core/en.json +++ b/data/lang/core/en.json @@ -1843,6 +1843,10 @@ "description": "Duration unit: one 7-day week", "message": "w" }, + "UNIT_CUBIC_METERS": { + "description": "Volume unit: cubic meter", + "message": "m³" + }, "UNKNOWN": { "description": "", "message": "" diff --git a/data/libs/Event.lua b/data/libs/Event.lua index 2288b43c0c..ff744b8ee3 100644 --- a/data/libs/Event.lua +++ b/data/libs/Event.lua @@ -131,6 +131,8 @@ Event.New = function() -- -- stable -- + ---@param name string + ---@param cb function self.Register = function (name, cb) super.Register(self, name, package.modulename(2), cb) end diff --git a/data/libs/autoload.lua b/data/libs/autoload.lua index 17374981a6..c0c1aea18f 100644 --- a/data/libs/autoload.lua +++ b/data/libs/autoload.lua @@ -85,9 +85,15 @@ end ---@param predicate nil|fun(k: K, v: V): any, any ---@return table table.merge = function(a, b, predicate) - for k, v in pairs(b) do - if predicate then k, v = predicate(k, v) end - a[k] = v + if predicate then + for k, v in pairs(b) do + k, v = predicate(k, v) + a[k] = v + end + else + for k, v in pairs(b) do + a[k] = v + end end return a end @@ -102,9 +108,15 @@ end ---@param predicate nil|fun(v: T): any ---@return table table.append = function(a, b, predicate) - for _, v in ipairs(b) do - if predicate then v = predicate(v) end - table.insert(a, v) + if predicate then + for _, v in ipairs(b) do + v = predicate(v) + table.insert(a, v) + end + else + for _, v in ipairs(b) do + table.insert(a, v) + end end return a end diff --git a/data/libs/utils.lua b/data/libs/utils.lua index f969122274..2d9ea9d341 100644 --- a/data/libs/utils.lua +++ b/data/libs/utils.lua @@ -230,6 +230,58 @@ utils.find_if = function(t, predicate) return nil end +-- +-- Function: best_score +-- +-- Returns the value in the passed table with the highest score according to the passed scoring function +-- +-- Iteration order is undefined (uses pairs() internally). +-- +---@generic K, V +---@param t table +---@param score_fun fun(k: K, v: V): number? +---@return V?, number +utils.best_score = function(t, score_fun) + local score = 0.0 + local best_val = nil + + for k, v in pairs(t) do + local new_score = score_fun(k, v) + if new_score and new_score > score then + score = new_score + best_val = v + end + end + + return best_val, score +end + +-- +-- Function: least_score +-- +-- Returns the value in the passed table with the lowest score according to the passed scoring function +-- +-- Iteration order is undefined (uses pairs() internally). +-- +---@generic K, V +---@param t table +---@param score_fun fun(k: K, v: V): number? +---@return V?, number +utils.least_score = function(t, score_fun) + local score = math.huge + local best_val = nil + + for k, v in pairs(t) do + local new_score = score_fun(k, v) + if new_score and new_score < score then + score = new_score + best_val = v + end + end + + return best_val, score +end + -- -- Function: stable_sort -- @@ -326,7 +378,7 @@ end -- local object = {} -object.meta = { __index = object, class="object" } +object.meta = { __index = object, class="object", inherits = { ["object"] = true } } -- -- Function: New @@ -390,7 +442,10 @@ end utils.inherits = function (baseClass, name) local new_class = {} local base_class = baseClass or object - new_class.meta = { __index = new_class, class=name } + new_class.meta = { __index = new_class, class=name, inherits = { [name] = true } } + + -- Allow quick lookup by parent class + table.merge(new_class.meta.inherits, base_class.meta.inherits) -- generic constructor function new_class.New(...) @@ -406,6 +461,10 @@ utils.inherits = function (baseClass, name) return new_class end + function new_class:IsA(typename) + return new_class.meta.inherits[typename] + end + function new_class.Unserialize(data) local tmp = base_class.Unserialize(data) setmetatable(tmp, new_class.meta) @@ -494,6 +553,11 @@ utils.proto = function(classname) return newProto end +-- Return a copy of a with the values from b added to it +function utils.mixin(a, b) + return table.merge(table.copy(a), b) +end + -- -- Function: print_r -- diff --git a/data/meta/CoreObject/Ship.meta.lua b/data/meta/CoreObject/Ship.meta.lua index 044e620820..dc37389aad 100644 --- a/data/meta/CoreObject/Ship.meta.lua +++ b/data/meta/CoreObject/Ship.meta.lua @@ -115,3 +115,18 @@ function Ship:GetShieldsPercent() end -- Sets the thruster fuel tank of the ship to the given percentage of its maximum. ---@param percent number function Ship:SetFuelPercent(percent) end + +-- Update ship properties after changing ship equipment or cargo +function Ship:UpdateEquipStats() end + +-- Is this ship currently docked with anything? +---@return boolean +function Ship:IsDocked() end + +-- Is this ship currently landed on a planet? +---@return boolean +function Ship:IsLanded() end + +-- Get the starport this ship is docked with, if any +---@return SpaceStation? +function Ship:GetDockedWith() end diff --git a/data/meta/Engine.lua b/data/meta/Engine.lua index 0051d44fe3..458a2572df 100644 --- a/data/meta/Engine.lua +++ b/data/meta/Engine.lua @@ -20,4 +20,9 @@ local Engine = {} -- TODO: add information about Engine methods +-- Get a model file by name +---@param name string +---@return SceneGraph.Model model +function Engine.GetModel(name) end + return Engine diff --git a/data/meta/Model.lua b/data/meta/Model.lua new file mode 100644 index 0000000000..1207ef8d54 --- /dev/null +++ b/data/meta/Model.lua @@ -0,0 +1,23 @@ +-- Copyright © 2008-2024 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 + +---@meta + +---@class SceneGraph.Model +--- +---@field name string +---@field pattern integer Current pattern applied to the model +---@field numPatterns integer Number of patterns supported by the model + +---@class SceneGraph.Model +local Model = {} + +-- Set the pattern currently applied to the model +---@param idx integer +function Model:SetPattern(idx) end + +-- Set debug flags used when rendering this model +---@param flags ModelDebugFlags[] +function Model:SetDebugFlags(flags) end diff --git a/data/modules/MissionUtils.lua b/data/modules/MissionUtils.lua index 540ad8de7b..71df4fc8b0 100644 --- a/data/modules/MissionUtils.lua +++ b/data/modules/MissionUtils.lua @@ -8,11 +8,15 @@ local utils = require "utils" local AU = 149598000000 local AU_sqrt = math.sqrt(AU) -local Days = 24*60*60 +local Hours = 60*60 +local Days = 24*Hours +local Weeks = 7*Days local MissionUtils = { AU = AU, - Days = Days + Days = Days, + Hours = Hours, + Weeks = Weeks, } ---@class MissionUtils.Calculator diff --git a/data/pigui/libs/buttons.lua b/data/pigui/libs/buttons.lua index 08b167ca07..8b4992485c 100644 --- a/data/pigui/libs/buttons.lua +++ b/data/pigui/libs/buttons.lua @@ -60,7 +60,7 @@ end -- -- Function: ui.button -- --- > clicked = ui.button(label, button_size, variant, tooltip) +-- > clicked = ui.button(label, button_size, variant, tooltip, padding) -- -- Example: -- @@ -74,19 +74,20 @@ end -- variant - [optional] Table, color variants used for this button; -- contains Color fields 'normal', 'hovered', and 'active' -- tooltip - [optional] string, mouseover text +-- padding - [optional] Vector2, size of padding on each side of button -- -- Returns: -- -- clicked - true if button was clicked -- -function ui.button(label, button_size, variant, tooltip) +function ui.button(label, button_size, variant, tooltip, padding) if variant then pigui.PushStyleColor("Button", variant.normal) pigui.PushStyleColor("ButtonHovered", variant.hovered) pigui.PushStyleColor("ButtonActive", variant.active) end - pigui.PushStyleVar("FramePadding", ui.theme.styles.ButtonPadding) + pigui.PushStyleVar("FramePadding", padding or ui.theme.styles.ButtonPadding) local res = pigui.Button(label, button_size or Vector2(0, 0)) pigui.PopStyleVar(1) @@ -94,7 +95,10 @@ function ui.button(label, button_size, variant, tooltip) pigui.PopStyleColor(3) end - if pigui.IsItemHovered() and tooltip then pigui.SetTooltip(tooltip) end + if tooltip then + ui.setItemTooltip(tooltip) + end + return res end @@ -185,8 +189,8 @@ function ui.iconButton(id, icon, tooltip, variant, size, padding, flags) pigui:PopFont() end - if tooltip and pigui.IsItemHovered() then - ui.setTooltip(tooltip) + if tooltip then + ui.setItemTooltip(tooltip) end return ret diff --git a/data/pigui/libs/forwarded.lua b/data/pigui/libs/forwarded.lua index fd77c5d122..676ee08c67 100644 --- a/data/pigui/libs/forwarded.lua +++ b/data/pigui/libs/forwarded.lua @@ -50,8 +50,8 @@ ui.setScrollHereY = pigui.SetScrollHereY ui.selectable = pigui.Selectable ui.progressBar = pigui.ProgressBar ui.plotHistogram = pigui.PlotHistogram -ui.setTooltip = pigui.SetTooltip -ui.setItemTooltip = pigui.SetItemTooltip +-- ui.setTooltip = pigui.SetTooltip +-- ui.setItemTooltip = pigui.SetItemTooltip ui.addCircle = pigui.AddCircle ui.addCircleFilled = pigui.AddCircleFilled ui.addRect = pigui.AddRect ---@type fun(a: Vector2, b: Vector2, col: Color, rounding: number, edges: integer, thickness: number) @@ -106,6 +106,7 @@ ui.isMouseDoubleClicked = pigui.IsMouseDoubleClicked ui.isMouseHoveringRect = pigui.IsMouseHoveringRect ui.collapsingHeader = pigui.CollapsingHeader ui.treeNode = pigui.TreeNode +ui.treePush = pigui.TreePush ui.treePop = pigui.TreePop ui.beginPopupModal = pigui.BeginPopupModal ui.endPopup = pigui.EndPopup diff --git a/data/pigui/libs/icons.lua b/data/pigui/libs/icons.lua index b5a28d8468..c26d246800 100644 --- a/data/pigui/libs/icons.lua +++ b/data/pigui/libs/icons.lua @@ -100,7 +100,7 @@ function ui.addIcon(position, icon, color, size, anchor_horizontal, anchor_verti end if tooltip and (ui.isMouseHoveringWindow() or not ui.isAnyWindowHovered()) and tooltip ~= "" then if pigui.IsMouseHoveringRect(pos, pos + size, true) then - ui.maybeSetTooltip(tooltip) + ui.setTooltip(tooltip) end end return size @@ -155,7 +155,7 @@ function ui.addWideIcon(position, icon, color, size, anchor_horizontal, anchor_v end if tooltip and (ui.isMouseHoveringWindow() or not ui.isAnyWindowHovered()) and tooltip ~= "" then if pigui.IsMouseHoveringRect(pos, pos + size, true) then - ui.maybeSetTooltip(tooltip) + ui.setTooltip(tooltip) end end @@ -185,7 +185,7 @@ function ui.addIconSimple(pos, icon, size, color, tooltip) if tooltip and (ui.isMouseHoveringWindow() or not ui.isAnyWindowHovered()) then if pigui.IsMouseHoveringRect(pos, pos + size, true) then - ui.maybeSetTooltip(tooltip) + ui.setTooltip(tooltip) end end end diff --git a/data/pigui/libs/text.lua b/data/pigui/libs/text.lua index 10eed36a92..cc1bc7eb0f 100644 --- a/data/pigui/libs/text.lua +++ b/data/pigui/libs/text.lua @@ -429,7 +429,7 @@ ui.addStyledText = function(position, anchor_horizontal, anchor_vertical, text, if tooltip and (ui.isMouseHoveringWindow() or not ui.isAnyWindowHovered()) and tooltip ~= "" then if pigui.IsMouseHoveringRect(position, position + size, true) then - ui.maybeSetTooltip(tooltip) + ui.setTooltip(tooltip) end end @@ -446,7 +446,7 @@ end -- tooltip - string, tooltip text to display to the user -- font - optional font table, used to display the given tooltip -- -function ui.maybeSetTooltip(tooltip, font) +function ui.setTooltip(tooltip, font) if not Input.GetMouseCaptured() then ui.withFont(font or ui.fonts.pionillium.details, function() pigui.SetTooltip(tooltip) @@ -454,4 +454,20 @@ function ui.maybeSetTooltip(tooltip, font) end end -ui.setTooltip = ui.maybeSetTooltip +-- +-- Function: setItemTooltip +-- +-- Displays a tooltip in the UI if the last submitted "item" is hovered with a short delay. +-- The function does not display a tooltip if the mouse is currently captured by the game. +-- +-- Parameters: +-- tooltip - string, tooltip text to display to the user +-- font - optional font table, used to display the given tooltip +-- +function ui.setItemTooltip(tooltip, font) + if not Input.GetMouseCaptured() then + ui.withFont(font or ui.fonts.pionillium.details, function() + pigui.SetItemTooltip(tooltip) + end) + end +end diff --git a/data/pigui/libs/wrappers.lua b/data/pigui/libs/wrappers.lua index 1e5dd01cc3..bf4acc2c80 100644 --- a/data/pigui/libs/wrappers.lua +++ b/data/pigui/libs/wrappers.lua @@ -971,3 +971,32 @@ function ui.incrementDrag(...) return pigui.IncrementDrag(table.unpack(args)) end) end + +-- Function: comboBox +-- +-- Wrapper for BeginCombo() + EndCombo(). The given function is called while +-- the combo box is open. +-- +-- Parameters: +-- +-- label - string, label to display next to the combo box +-- preview - string, label of currently selected item +-- flags - optional ComboFlags, table controlling display of the combo box +-- fun - function, renders the contents of the combo box while open +-- +---@param label string +---@param preview string +---@param flags any +---@param fun fun() +---@overload fun(label: string, preview: string, fun: fun()) +function ui.comboBox(label, preview, flags, fun) + if not fun then + fun = flags + flags = nil + end + + if pigui.BeginCombo(label, preview) then + fun() + pigui.EndCombo() + end +end diff --git a/data/pigui/views/debug.lua b/data/pigui/views/debug.lua index 311792abd1..ec978af829 100644 --- a/data/pigui/views/debug.lua +++ b/data/pigui/views/debug.lua @@ -14,11 +14,17 @@ local function drawTab(tab, i, delta) local label = tab.label or "" local icon_str = "{}##{}" % { ui.get_icon_glyph(icon), i } + local active = false + ui.tabItem(icon_str, label, function() + active = true + ui.child(label, Vector2(0, 0), childWindowFlags, function() - tab.draw(delta) + tab.draw(tab, delta) end) end) + + return active end ---@class Debug.DebugTab @@ -51,14 +57,17 @@ end function debugView.drawTabs(delta) for i, tab in ipairs(debugView.tabs) do if not tab.show or tab.show() then - drawTab(tab, i, delta) + if drawTab(tab, i, delta) then + + if ui.ctrlHeld() and ui.isKeyReleased(string.byte 'r') then + print("Hot reloading module " .. tab.module) + package.reimport(tab.module) + end - if ui.ctrlHeld() and ui.isKeyReleased(string.byte 'r') then - print("Hot reloading module " .. tab.module) - package.reimport(tab.module) end end end + end ui.registerHandler('debug-tabs', debugView.drawTabs) diff --git a/src/lua/LuaJson.cpp b/src/lua/LuaJson.cpp index bfd984a96f..349c07faea 100644 --- a/src/lua/LuaJson.cpp +++ b/src/lua/LuaJson.cpp @@ -13,7 +13,7 @@ */ // Do a simple JSON->Lua translation. -static void _push_json_to_lua(lua_State *l, Json &obj) +void LuaJson::PushToLua(lua_State *l, const Json &obj) { lua_checkstack(l, 20); @@ -40,15 +40,15 @@ static void _push_json_to_lua(lua_State *l, Json &obj) size_t size = obj.size(); for (size_t idx = 0; idx < size; idx++) { lua_pushinteger(l, idx + 1); - _push_json_to_lua(l, obj[idx]); + PushToLua(l, obj[idx]); lua_settable(l, -3); } } break; case Json::value_t::object: { lua_newtable(l); - for (Json::iterator it = obj.begin(); it != obj.end(); it++) { - lua_pushstring(l, it.key().c_str()); - _push_json_to_lua(l, it.value()); + for (const auto &pair : obj.items()) { + lua_pushstring(l, pair.key().c_str()); + PushToLua(l, pair.value()); lua_settable(l, -3); } } break; @@ -79,7 +79,7 @@ static int l_load_json(lua_State *l) if (data.is_null()) return luaL_error(l, "Error loading JSON file %s.", filename.c_str()); - _push_json_to_lua(l, data); + LuaJson::PushToLua(l, data); return 1; } @@ -106,7 +106,7 @@ static int l_load_save_file(lua_State *l) return luaL_error(l, "Error loading JSON file %s.", filename.c_str()); } - _push_json_to_lua(l, data); + LuaJson::PushToLua(l, data); return 1; } diff --git a/src/lua/LuaJson.h b/src/lua/LuaJson.h index 53c20b6abf..6f6f87a7be 100644 --- a/src/lua/LuaJson.h +++ b/src/lua/LuaJson.h @@ -4,8 +4,15 @@ #ifndef PI_LUA_JSON_H #define PI_LUA_JSON_H +#include "JsonFwd.h" +#include "Lua.h" + namespace LuaJson { + void Register(); + + void PushToLua(lua_State *l, const Json &data); + } #endif diff --git a/src/lua/LuaPiGui.cpp b/src/lua/LuaPiGui.cpp index ca9dd5eee6..8e48233ba4 100644 --- a/src/lua/LuaPiGui.cpp +++ b/src/lua/LuaPiGui.cpp @@ -526,6 +526,14 @@ void pi_lua_generic_pull(lua_State *l, int index, ImGuiTableColumnFlags_ &thefla theflags = parse_imgui_flags(l, index, imguiTableColumnFlagsTable); } +int l_pigui_check_table_column_flags(lua_State *l) +{ + luaL_checktype(l, 1, LUA_TTABLE); + ImGuiTableColumnFlags_ fl = imguiTableColumnFlagsTable.LookupTable(l, 1); + LuaPush(l, fl); + return 1; +} + static LuaFlags imguiColorEditFlagsTable = { { "None", ImGuiColorEditFlags_None }, { "NoAlpha", ImGuiColorEditFlags_NoAlpha }, @@ -559,11 +567,27 @@ void pi_lua_generic_pull(lua_State *l, int index, ImGuiColorEditFlags_ &theflags theflags = parse_imgui_flags(l, index, imguiColorEditFlagsTable); } -int l_pigui_check_table_column_flags(lua_State *l) +static LuaFlags imguiComboFlagsTable = { + { "None", ImGuiComboFlags_None }, + { "PopupAlignLeft", ImGuiComboFlags_PopupAlignLeft }, + { "HeightSmall", ImGuiComboFlags_HeightSmall }, + { "HeightRegular", ImGuiComboFlags_HeightRegular }, + { "HeightLarge", ImGuiComboFlags_HeightLarge }, + { "HeightLargest", ImGuiComboFlags_HeightLargest }, + { "NoArrowButton", ImGuiComboFlags_NoArrowButton }, + { "NoPreview", ImGuiComboFlags_NoPreview }, +}; + +void pi_lua_generic_pull(lua_State *l, int index, ImGuiComboFlags_ &theflags) +{ + theflags = parse_imgui_flags(l, index, imguiComboFlagsTable); +} + +int l_pigui_check_combo_flags(lua_State *l) { luaL_checktype(l, 1, LUA_TTABLE); - ImGuiTableColumnFlags_ fl = imguiTableColumnFlagsTable.LookupTable(l, 1); - LuaPush(l, fl); + ImGuiComboFlags_ fl = imguiComboFlagsTable.LookupTable(l, 1); + LuaPush(l, fl); return 1; } @@ -1346,7 +1370,7 @@ static int l_pigui_invisible_button(lua_State *l) PROFILE_SCOPED() std::string id = LuaPull(l, 1); ImVec2 size = LuaPull(l, 2); - ImGuiButtonFlags flags = LuaPull(l, 3); + ImGuiButtonFlags flags = LuaPull(l, 3, ImGuiButtonFlags_None); bool ret = ImGui::InvisibleButton(id.c_str(), size, flags); LuaPush(l, ret); return 1; @@ -2636,6 +2660,27 @@ static int l_pigui_combo(lua_State *l) return 2; } +static int l_pigui_begin_combo(lua_State *l) +{ + PROFILE_SCOPED() + + std::string lbl = LuaPull(l, 1); + std::string preview = LuaPull(l, 2); + ImGuiComboFlags flags = LuaPull(l, 3, ImGuiComboFlags_None); + + bool open = ImGui::BeginCombo(lbl.c_str(), preview.c_str(), flags); + LuaPush(l, open); + return 1; +} + +static int l_pigui_end_combo(lua_State *l) +{ + PROFILE_SCOPED() + + ImGui::EndCombo(); + return 0; +} + static int l_pigui_listbox(lua_State *l) { PROFILE_SCOPED() @@ -3269,6 +3314,13 @@ static int l_pigui_treenode(lua_State *l) return 1; } +static int l_pigui_treepush(lua_State *l) +{ + PROFILE_SCOPED() + ImGui::TreePush(LuaPull(l, 1)); + return 0; +} + /* * Function: treePop * @@ -3403,6 +3455,30 @@ static int l_pigui_table_set_bg_color(lua_State *l) return 0; } +// ============================================================================ + +static int l_pigui_get_bool_state(lua_State *l) +{ + const char *id = LuaPull(l, 1); + bool default_val = LuaPull(l, 2, false); + + bool state = ImGui::GetStateStorage()->GetBool(ImGui::GetCurrentWindow()->GetID(id), default_val); + + LuaPush(l, state); + return 1; +} + +static int l_pigui_set_bool_state(lua_State *l) +{ + const char *id = LuaPull(l, 1); + bool val = LuaPull(l, 2); + + ImGui::GetStateStorage()->SetBool(ImGui::GetCurrentWindow()->GetID(id), val); + return 0; +} + +// ============================================================================ + static Color4ub to_Color4ub(ImVec4 c) { return Color4ub(uint8_t(c.x * 255), uint8_t(c.y * 255), uint8_t(c.z * 255), uint8_t(c.w * 255)); @@ -3596,9 +3672,12 @@ void LuaObject::RegisterClass() { "GetItemRect", l_pigui_get_item_rect }, { "InputText", l_pigui_input_text }, { "Combo", l_pigui_combo }, + { "BeginCombo", l_pigui_begin_combo }, + { "EndCombo", l_pigui_end_combo }, { "ListBox", l_pigui_listbox }, { "CollapsingHeader", l_pigui_collapsing_header }, { "TreeNode", l_pigui_treenode }, + { "TreePush", l_pigui_treepush }, { "TreePop", l_pigui_treepop }, { "CaptureMouseFromApp", l_pigui_capture_mouse_from_app }, { "PlotHistogram", l_pigui_plot_histogram }, @@ -3644,6 +3723,10 @@ void LuaObject::RegisterClass() { "TableSetBgColor", l_pigui_table_set_bg_color }, // TODO: finish exposing Tables API + // Simple ImGui state access + { "GetBoolState", l_pigui_get_bool_state }, + { "SetBoolState", l_pigui_set_bool_state }, + { "WantTextInput", l_pigui_want_text_input }, { "ClearMouse", l_pigui_clear_mouse }, { 0, 0 } @@ -3679,4 +3762,5 @@ void LuaObject::RegisterClass() imguiTableColumnFlagsTable.Register(l, "ImGuiTableColumnFlags"); imguiTableBgFlagsTable.Register(l, "ImGuiTableBgTargetFlags"); imguiColorEditFlagsTable.Register(l, "ImGuiTableColumnFlags"); + imguiComboFlagsTable.Register(l, "ImGuiComboFlags"); }