diff --git a/data/libs/SpaceStation.lua b/data/libs/SpaceStation.lua index c6db67fccab..4b821553330 100644 --- a/data/libs/SpaceStation.lua +++ b/data/libs/SpaceStation.lua @@ -941,7 +941,7 @@ function SpaceStation:UnlockAdvert (ref) end local function updateSystem () - local stations = Space.GetBodies(function (b) return b.superType == "STARPORT" end) + local stations = Space.GetBodies("SpaceStation") for i, station in ipairs(stations) do -- updateStationMarket(station) Economy.UpdateStationMarket(station:GetSystemBody()) diff --git a/data/meta/Space.lua b/data/meta/Space.lua new file mode 100644 index 00000000000..5d3cb8621d5 --- /dev/null +++ b/data/meta/Space.lua @@ -0,0 +1,28 @@ +-- 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 + +---@meta + +local Space = {} + +package.core["Space"] = Space + +-- Returns a list of bodies simulated in the current system, optionally filtered by type +---@generic T +---@param class `T` an optional body classname to filter the returned results by +---@return T[] +---@overload fun(): Body[] +function Space.GetBodies(class) end + +-- Returns a list of bodies at most dist away from the given body, optionally filtered by type +---@generic T +---@param body Body the reference body +---@param dist number the maximum distance from the reference body to search +---@param filter `T` an optional body classname to filter the returned results by +---@return T[] +---@overload fun(body: Body, dist: number): Body[] +function Space.GetBodiesNear(body, dist, filter) end + +return Space diff --git a/data/modules/Assassination/Assassination.lua b/data/modules/Assassination/Assassination.lua index c2da76544c3..837caedf4dd 100644 --- a/data/modules/Assassination/Assassination.lua +++ b/data/modules/Assassination/Assassination.lua @@ -388,7 +388,7 @@ local onShipUndocked = function (ship, station) for ref,mission in pairs(missions) do if mission.status == 'ACTIVE' and mission.ship == ship then - planets = Space.GetBodies(function (body) return body:isa("Planet") end) + planets = Space.GetBodies("Planet") if #planets == 0 then ship:AIFlyTo(station) mission.shipstate = 'outbound' diff --git a/data/modules/BulkShips.lua b/data/modules/BulkShips.lua index 910c392c420..b99101d093b 100644 --- a/data/modules/BulkShips.lua +++ b/data/modules/BulkShips.lua @@ -19,7 +19,7 @@ local spawnShips = function () return end - local stations = Space.GetBodies(function (body) return body:isa("SpaceStation") and not body.isGroundStation end) + local stations = utils.filter_array(Space.GetBodies("SpaceStation"), function (body) return not body.isGroundStation end) if #stations == 0 then return end diff --git a/data/modules/Common/ProximityQuery.lua b/data/modules/Common/ProximityQuery.lua new file mode 100644 index 00000000000..24cfcd249ed --- /dev/null +++ b/data/modules/Common/ProximityQuery.lua @@ -0,0 +1,139 @@ +-- Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +local Engine = require 'Engine' +local Space = require 'Space' +local Timer = require 'Timer' +local utils = require 'utils' +local Game = require 'Game' + +local ProximityQuery = {} + +-- ─── Context ───────────────────────────────────────────────────────────────── + +local onBodyEnter = function(ctx, enter) end +local onBodyLeave = function(ctx, leave) end + +-- List of bodies currently in proximity is stored with weak keys, so deleted +-- bodies are quietly cleaned up during a GC cycle +local bodyListMt = { + __mode = "k" +} + +---@class ProximityQuery.Context +---@field New fun(body, dist, interval, type): self +---@field body Body +---@field dist number +local Context = utils.class 'ProximityQuery.Context' + +function Context:Constructor(body, dist, type) + self.bodies = setmetatable({}, bodyListMt) + self.body = body + self.dist = dist + self.filter = type + self.iter = 0 + + self.onBodyEnter = onBodyEnter + self.onBodyLeave = onBodyLeave +end + +function Context:Cancel() + self.iter = nil +end + +---@param fn fun(ctx: ProximityQuery.Context, enter: Body) +function Context:SetBodyEnter(fn) + self.onBodyEnter = fn + return self +end + +---@param fn fun(ctx: ProximityQuery.Context, leave: Body) +function Context:SetBodyLeave(fn) + self.onBodyLeave = fn + return self +end + +-- Class: ProximityQuery +-- +-- This class provides a helper utility to allow using Space.GetBodiesNear() in +-- an efficient manner, providing an event-based API when bodies enter or leave +-- a user-specified relevancy range of the reference body. + +-- Function: CreateQuery +-- +-- Register a new periodic proximity test relative to the given body +-- +-- Parameters: +-- +-- body - the reference body to perform testing for +-- dist - the distance (in meters) of the proximity test to perform +-- interval - how often a proximity test should be performed in seconds. +-- Smaller values are more performance-hungry. +-- type - optional body classname filter, see Space.GetBodiesNear +-- overlap - if false, all bodies of type in the radius will generate +-- initial proximity events on the first proximity test +-- +-- Returns: +-- +-- context - the context object for the registered test +-- +function ProximityQuery.CreateQuery(body, dist, interval, type, overlap) + local context = Context.New(body, dist, type) + local cb = ProximityQuery.MakeCallback(context) + + -- Queue the start of the timer at a random timestamp inside the first interval period + -- This provides natural load balancing for large numbers of callbacks created on the same frame + -- (e.g. at game start / hyperspace entry) + Timer:CallAt(Game.time + Engine.rand:Number(interval), function() + + if overlap == false then + cb() + else + -- Pre-fill the list of nearby bodies (avoid spurious onBodyEnter() callbacks when creating) + for i, locBody in ipairs(Space.GetBodiesNear(body, dist, type)) do + context.bodies[locBody] = context.iter + end + end + + Timer:CallEvery(interval, cb) + end) + + return context +end + +---@private +---@param context ProximityQuery.Context +function ProximityQuery.MakeCallback(context) + return function() + -- Callback has been cancelled or query body no longer exists, + -- stop ticking this query + if not context.iter or not context.body:exists() then + return true + end + + local newIter = (context.iter + 1) % 2 + context.iter = newIter + + for i, locBody in ipairs(Space.GetBodiesNear(context.body, context.dist, context.filter)) do + if not context.bodies[locBody] then + context.onBodyEnter(context, locBody) + end + + context.bodies[locBody] = newIter + end + + local remove = {} + for locBody, ver in pairs(context.bodies) do + if ver ~= newIter then + context.onBodyLeave(context, locBody) + table.insert(remove, locBody) + end + end + + for _, v in ipairs(remove) do + context.bodies[v] = nil + end + end +end + +return ProximityQuery diff --git a/data/modules/SearchRescue/SearchRescue.lua b/data/modules/SearchRescue/SearchRescue.lua index b99f917683f..820fa6469ba 100644 --- a/data/modules/SearchRescue/SearchRescue.lua +++ b/data/modules/SearchRescue/SearchRescue.lua @@ -301,7 +301,7 @@ local triggerAdCreation = function () -- Return if ad should be created based on lawlessness and min/max frequency values. -- Ad number per system is based on how many stations a system has so a player will -- be met with a certain number of stations that have one or more ads. - local stations = Space.GetBodies(function (body) return body.superType == 'STARPORT' end) + local stations = Space.GetBodies("SpaceStation") local freq = Game.system.lawlessness * ad_freq_max if freq < ad_freq_min then freq = ad_freq_min end local ad_num_max = freq * #stations @@ -1001,10 +1001,11 @@ local findNearbyStations = function (vacuum, body) -- get station bodies within current system depending on vacuum variable local nearbystations_raw if vacuum == true then - nearbystations_raw = Space.GetBodies(function (body) - return body.superType == 'STARPORT' and (body.type == 'STARPORT_ORBITAL' or (not body.path:GetSystemBody().parent.hasAtmosphere)) end) + nearbystations_raw = utils.filter_array(Space.GetBodies("SpaceStation"), function (body) + return body.type == 'STARPORT_ORBITAL' or (not body.path:GetSystemBody().parent.hasAtmosphere) + end) else - nearbystations_raw = Space.GetBodies(function (body) return body.superType == 'STARPORT' end) + nearbystations_raw = Space.GetBodies("SpaceStation") end -- determine distance to body @@ -1040,7 +1041,7 @@ local findClosestPlanets = function () end -- get planets with stations and remove from planet list - local ground_stations = Space.GetBodies(function (body) return body.type == 'STARPORT_SURFACE' end) + local ground_stations = utils.filter_array(Space.GetBodies("SpaceStation"), function (body) return body.type == 'STARPORT_SURFACE' end) for _,ground_station in pairs(ground_stations) do for i=#rockyplanets, 1, -1 do if rockyplanets[i] == Space.GetBody(ground_station.path:GetSystemBody().parent.index) then @@ -1052,7 +1053,7 @@ local findClosestPlanets = function () -- create dictionary of stations local closestplanets = {} - local stations = Space.GetBodies(function (body) return body.superType == 'STARPORT' end) + local stations = Space.GetBodies("SpaceStation") for _,station in pairs(stations) do closestplanets[station] = {} end -- pick closest planets to stations @@ -1299,7 +1300,7 @@ local makeAdvert = function (station, manualFlavour, closestplanets) local needed_fuel if flavour.id == 2 or flavour.id == 5 then needed_fuel = math.max(math.floor(shipdef.fuelTankMass * 0.1), 1) - elseif flavour.id == 4 then -- different planet + elseif flavour.id == 4 then -- different planet needed_fuel = math.max(math.floor(shipdef.fuelTankMass * 0.5), 1) end deliver_comm[Commodities.hydrogen] = needed_fuel diff --git a/data/modules/System/Explore.lua b/data/modules/System/Explore.lua index d579afa0897..32c0ff85a56 100644 --- a/data/modules/System/Explore.lua +++ b/data/modules/System/Explore.lua @@ -7,13 +7,14 @@ local Event = require 'Event' local Comms = require 'Comms' local Timer = require 'Timer' local Lang = require 'Lang' +local utils = require 'utils' local l = Lang.GetResource("module-system") local exploreSystem = function (system) Comms.Message(l.GETTING_SENSOR_DATA) - local starports = #Space.GetBodies(function (body) return body.superType == 'STARPORT' end) - local major_bodies = #Space.GetBodies(function (body) return body.superType and body.superType ~= 'STARPORT' and body.superType ~= 'NONE' end) + local starports = #Space.GetBodies("SpaceStation") + local major_bodies = #Space.GetBodies("TerrainBody") local bodies if major_bodies == 1 then bodies = l.BODY diff --git a/data/modules/TradeShips/Debug.lua b/data/modules/TradeShips/Debug.lua index 53905045483..300a4465bc1 100644 --- a/data/modules/TradeShips/Debug.lua +++ b/data/modules/TradeShips/Debug.lua @@ -108,7 +108,7 @@ debugView.registerTab('debug-trade-ships', function() ui.sameLine() property("Lawlessness", string.format("%.4f", Game.system.lawlessness)) ui.sameLine() - property("Total bodies in space", #Space.GetBodies()) + property("Total bodies in space", Space.GetNumBodies()) end if ui.collapsingHeader("Stations") then diff --git a/data/modules/TradeShips/Flow.lua b/data/modules/TradeShips/Flow.lua index 353616800b7..8ff66460412 100644 --- a/data/modules/TradeShips/Flow.lua +++ b/data/modules/TradeShips/Flow.lua @@ -108,7 +108,7 @@ Flow.calculateSystemParams = function() if system.population == 0 then return nil end -- all ports in the system - local ports = Space.GetBodies(function (body) return body.superType == 'STARPORT' end) + local ports = Space.GetBodies("SpaceStation") -- check if the current system can be traded in if #ports == 0 then return nil end diff --git a/src/lua/LuaBody.cpp b/src/lua/LuaBody.cpp index 34b3a92e35d..fc3f12be438 100644 --- a/src/lua/LuaBody.cpp +++ b/src/lua/LuaBody.cpp @@ -27,12 +27,31 @@ #include "SpaceStation.h" #include "Star.h" +// For LuaFlags +#include "enum_table.h" +#include "pigui/LuaFlags.h" + namespace PiGui { // Declared in LuaPiGuiInternal.h extern bool first_body_is_more_important_than(Body *, Body *); extern int pushOnScreenPositionDirection(lua_State *l, vector3d position); } // namespace PiGui +static LuaFlags s_bodyFlags ({ + { "Body", ObjectType::BODY }, + { "ModelBody", ObjectType::MODELBODY }, + { "Ship", ObjectType::SHIP }, + { "Player", ObjectType::PLAYER }, + { "SpaceStation", ObjectType::SPACESTATION }, + { "TerrainBody", ObjectType::TERRAINBODY }, + { "Planet", ObjectType::PLANET }, + { "Star", ObjectType::STAR }, + { "CargoBody", ObjectType::CARGOBODY }, + { "Projectile", ObjectType::PROJECTILE }, + { "Missile", ObjectType::MISSILE }, + { "HyperspaceCloud", ObjectType::HYPERSPACECLOUD } +}); + /* * Class: Body * @@ -843,6 +862,16 @@ static int l_body_set_ang_velocity(lua_State *l) return 0; } +void pi_lua_generic_pull(lua_State *l, int index, ObjectType &objectType) +{ + objectType = s_bodyFlags.LookupEnum(l, index); +} + +void pi_lua_generic_push(lua_State *l, ObjectType bodyType) +{ + lua_pushstring(l, EnumStrings::GetString("PhysicsObjectType", static_cast(bodyType))); +} + template <> const char *LuaObject::s_type = "Body"; @@ -908,4 +937,6 @@ void LuaObject::RegisterClass() LuaObjectBase::RegisterSerializer("CargoBody", body_serializers); LuaObjectBase::RegisterSerializer("Missile", body_serializers); LuaObjectBase::RegisterSerializer("HyperspaceCloud", body_serializers); + + s_bodyFlags.Register(Lua::manager->GetLuaState(), "Constants.ObjectType"); } diff --git a/src/lua/LuaBody.h b/src/lua/LuaBody.h new file mode 100644 index 00000000000..025f31a3c34 --- /dev/null +++ b/src/lua/LuaBody.h @@ -0,0 +1,9 @@ +// Copyright © 2008-2023 Pioneer Developers. See AUTHORS.txt for details +// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt + +#include "Body.h" +#include "Lua.h" +#include "LuaPushPull.h" + +void pi_lua_generic_pull(lua_State *l, int index, ObjectType &bodyType); +void pi_lua_generic_push(lua_State *l, ObjectType bodyType); diff --git a/src/lua/LuaPiGui.cpp b/src/lua/LuaPiGui.cpp index d57e9b3b91d..690754dab66 100644 --- a/src/lua/LuaPiGui.cpp +++ b/src/lua/LuaPiGui.cpp @@ -111,7 +111,7 @@ static Type parse_imgui_enum(lua_State *l, int index, LuaFlags lookupTable return static_cast(lua_tointeger(l, index)); else { luaL_traceback(l, l, NULL, 1); - Error("Expected a table or integer, got %s.\n%s\n", luaL_typename(l, index), lua_tostring(l, -1)); + Error("Expected a string or integer, got %s.\n%s\n", luaL_typename(l, index), lua_tostring(l, -1)); } return static_cast(0); } diff --git a/src/lua/LuaSpace.cpp b/src/lua/LuaSpace.cpp index 23e6d239d06..792d1242aef 100644 --- a/src/lua/LuaSpace.cpp +++ b/src/lua/LuaSpace.cpp @@ -2,6 +2,8 @@ // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt #include "LuaSpace.h" +#include "Body.h" +#include "LuaBody.h" #include "CargoBody.h" #include "Frame.h" #include "Game.h" @@ -17,6 +19,7 @@ #include "Ship.h" #include "Space.h" #include "SpaceStation.h" +#include "profiler/Profiler.h" #include "ship/PrecalcPath.h" /* @@ -903,21 +906,46 @@ static int l_space_get_body(lua_State *l) return 1; } +/* + * Function: GetNumBodies + * + * Get the total number of bodies simulated in the current Space + * + * bodies = #Space.GetNumBodies() + * + * Return: + * + * num - the number of bodies currently existing in Space + * + * Availability: + * + * Oct. 2023 + * + * Status: + * + * stable + */ +static int l_space_get_num_bodies(lua_State *l) +{ + if (!Pi::game) { + return luaL_error(l, "Game is not started!"); + } + + LuaPush(l, Pi::game->GetSpace()->GetNumBodies()); + return 1; +} + /* * Function: GetBodies * - * Get all the objects that match the specified filter + * Get all the objects that match the specified filter type * - * bodies = Space.GetBodies(filter) + * bodies = Space.GetBodies([type]) * * Parameters: * - * filter - an option function. If specificed the function will be called - * once for each body with the object as the only parameter. - * If the filter function returns true then the will be - * included in the array returned by , otherwise it will - * be omitted. If no filter function is specified then all bodies - * are returned. + * type - an optional Body classname acting as a filter on the type of the + * returned bodies * * Return: * @@ -927,13 +955,13 @@ static int l_space_get_body(lua_State *l) * Example: * * > -- get all the ground-based stations - * > local stations = Space.GetBodies(function (body) + * > local stations = utils.filter_array(Space.GetBodies("SpaceStation"), function(body) * > return body.type == "STARPORT_SURFACE" * > end) * * Availability: * - * alpha 10 + * Oct. 2023 * * Status: * @@ -941,6 +969,8 @@ static int l_space_get_body(lua_State *l) */ static int l_space_get_bodies(lua_State *l) { + PROFILE_SCOPED() + if (!Pi::game) { luaL_error(l, "Game is not started"); return 0; @@ -948,36 +978,91 @@ static int l_space_get_bodies(lua_State *l) LUA_DEBUG_START(l); - bool filter = false; - if (lua_gettop(l) >= 1) { - luaL_checktype(l, 1, LUA_TFUNCTION); // any type of function - filter = true; - } + ObjectType filterBodyType = LuaPull(l, 1, ObjectType::BODY); + bool filter = filterBodyType != ObjectType::BODY; lua_newtable(l); + int idx = 1; for (Body *b : Pi::game->GetSpace()->GetBodies()) { - if (filter) { - lua_pushvalue(l, 1); - LuaObject::PushToLua(b); - if (int ret = lua_pcall(l, 1, 1, 0)) { - const char *errmsg("Unknown error"); - if (ret == LUA_ERRRUN) - errmsg = lua_tostring(l, -1); - else if (ret == LUA_ERRMEM) - errmsg = "memory allocation failure"; - else if (ret == LUA_ERRERR) - errmsg = "error in error handler function"; - luaL_error(l, "Error in filter function: %s", errmsg); - } - if (!lua_toboolean(l, -1)) { - lua_pop(l, 1); - continue; - } - lua_pop(l, 1); - } + if (filter && !b->IsType(filterBodyType)) + continue; + + lua_pushinteger(l, idx++); + LuaObject::PushToLua(b); + lua_rawset(l, -3); + } + + LUA_DEBUG_END(l, 1); + + return 1; +} + +/* + * Function: GetBodiesNear + * + * Get all the objects within a specified distance from another body + * that match the specified filter + * + * bodies = Space.GetBodiesNear(body, dist, [type]) + * + * Parameters: + * + * body - the reference body for distance + * + * dist - the maximum distance from the reference body another body can be + * + * type - optional - a PhysicsObjectType enum value + * (one of Constants.PhysicsObjectType) acting as a filter on the type + * of the returned bodies + * + * Return: + * + * bodies - an array containing zero or more objects that matched the + * filter + * + * Example: + * + * > -- get all stations within 50,000m + * > local stations = Space.GetBodiesNear(Game.player, 50000, "SPACE_STATION") + * + * Availability: + * + * Oct. 2023 + * + * Status: + * + * stable + */ +static int l_space_get_bodies_near(lua_State *l) +{ + PROFILE_SCOPED() + + if (!Pi::game) { + luaL_error(l, "Game is not started"); + return 0; + } + + LUA_DEBUG_START(l); + + Body *body = LuaPull(l, 1); + double dist = LuaPull(l, 2); + double distSqr = dist * dist; + + ObjectType filterBodyType = LuaPull(l, 3, ObjectType::BODY); + bool filter = filterBodyType != ObjectType::BODY; + + lua_newtable(l); + + int idx = 1; + for (Body *b : Pi::game->GetSpace()->GetBodiesMaybeNear(body, dist)) { + if (filter && !b->IsType(filterBodyType)) + continue; + + if (b->GetPositionRelTo(body).LengthSqr() > distSqr) + continue; - lua_pushinteger(l, lua_rawlen(l, -1) + 1); + lua_pushinteger(l, idx++); LuaObject::PushToLua(b); lua_rawset(l, -3); } @@ -1035,7 +1120,9 @@ void LuaSpace::Register() { "PutShipOnRoute", l_space_put_ship_on_route }, { "GetBody", l_space_get_body }, + { "GetNumBodies", l_space_get_num_bodies }, { "GetBodies", l_space_get_bodies }, + { "GetBodiesNear", l_space_get_bodies_near }, { "DbgDumpFrames", l_space_dump_frames }, { 0, 0 } diff --git a/src/pigui/LuaFlags.h b/src/pigui/LuaFlags.h index 62587691fda..ab4c2c71645 100644 --- a/src/pigui/LuaFlags.h +++ b/src/pigui/LuaFlags.h @@ -3,6 +3,7 @@ #pragma once +#include "enum_table.h" #include "utils.h" #include #include @@ -15,9 +16,18 @@ struct LuaFlags { std::string typeName; int lookupTableRef = LUA_NOREF; + // directly emplace from an initializer_list LuaFlags(std::initializer_list> init) : LUT(init) {} + // Build the flags table from a c++ enum table + LuaFlags(const EnumItem enumEntries[]) + { + for (size_t idx = 0; enumEntries[idx].name != nullptr; idx++) { + LUT.emplace_back(enumEntries[idx].name, static_cast(enumEntries[idx].value)); + } + } + void Register(lua_State *l, std::string name) { if (lookupTableRef != LUA_NOREF) @@ -42,6 +52,7 @@ struct LuaFlags { lookupTableRef = LUA_NOREF; } + // Parse a table of string flags or single flag into a bitwise-or'd bitflag value FlagType LookupTable(lua_State *l, int index) { FlagType flagAccum = FlagType(0); @@ -69,6 +80,7 @@ struct LuaFlags { return flagAccum; } + // Parse a single string flag value into an enum value directly FlagType LookupEnum(lua_State *l, int index) { FlagType flagAccum = FlagType(0);