From 33a14aeb0bf420b487b4113f24b7397eceb79e61 Mon Sep 17 00:00:00 2001 From: oznogon Date: Sun, 31 May 2026 00:10:53 -0700 Subject: [PATCH] Replace all C++ sector numbering with Lua API in scripts/api Move sector naming from C++ to the Lua scripting API, allowing scenarios to customize the functions. Move the global functions getSectorName(x,y) and sectorToXY(name) to a `Sector.` pseudonamespace to retain entity functions with the same names, and add global wrappers at the old function names for backward scenario compatibility. All radars should now show sector names as defined in the Lua API. This allows each scenario to define custom sector naming behaviors by overriding Sector.getSectorName() and Sector.sectorToXY(). This also allows redefining the default behavior by overriding or replacing the API functions. Tested against the Edge-of-Space scenario, which had already switched from hardcoded sector names in comms scripts to global functions, and set overriding sector naming functions in the scenario. The scenario seamlessly used the overrides. --- scripts/api/all.lua | 1 + scripts/api/entity/spaceobject.lua | 2 +- scripts/api/sector.lua | 117 +++++++++++++++++++++++++++++ src/gameGlobalInfo.cpp | 31 ++++---- src/gameGlobalInfo.h | 1 - src/script.cpp | 75 ------------------ 6 files changed, 136 insertions(+), 91 deletions(-) create mode 100644 scripts/api/sector.lua diff --git a/scripts/api/all.lua b/scripts/api/all.lua index a50b00b875..e6e538480e 100644 --- a/scripts/api/all.lua +++ b/scripts/api/all.lua @@ -1,3 +1,4 @@ +require("api/sector.lua") require("api/modelData.lua") require("api/shipTemplate.lua") require("api/entity/spaceobject.lua") diff --git a/scripts/api/entity/spaceobject.lua b/scripts/api/entity/spaceobject.lua index 3be3208d81..5a8e6470a0 100644 --- a/scripts/api/entity/spaceobject.lua +++ b/scripts/api/entity/spaceobject.lua @@ -288,7 +288,7 @@ end --- Example: entity:getSectorName() function Entity:getSectorName() local x, y = self:getPosition() - return getSectorName(x, y) + return Sector.getSectorName(x, y) end --- Deals a specific amount of a specific type of damage to this entity. --- Requires a numeric value for the damage amount, and accepts an optional DamageInfo type. diff --git a/scripts/api/sector.lua b/scripts/api/sector.lua new file mode 100644 index 0000000000..bd9d778420 --- /dev/null +++ b/scripts/api/sector.lua @@ -0,0 +1,117 @@ +-- Sector naming conventions + +-- The game map is divided into 20U (20,000) square sectors. +-- By default, sectors are named with a letter (Y axis) and a number (X axis) with the origin coordinates 0,0 at the northwest corner of sector F5. +-- Access sector names and coordinates using global functions Sector.getSectorName(x, y) and Sector.sectorToXY(sector_name), or their legacy wrappers without the Sector namespacing. +-- +-- Scenarios can override the naming scheme by assigning to the Sector-namespaced functions. +-- +-- Override examples: +-- +-- - Name sectors numerically (i.e. "2, 4" for two right and four down from origin) +-- +-- Sector.getSectorName = function(x, y) +-- return string.format("%d,%d", math.floor(x / 20000), math.floor(y / 20000)) +-- end +-- Sector.sectorToXY = function(name) +-- local x, y = name:match("(-?%d+),(-?%d+)") +-- if not x then return 0, 0, false end +-- return tonumber(x) * 20000, tonumber(y) * 20000, true +-- end + +Sector = {} +local SECTOR_SIZE = 20000 + +-- Truncate float division +local function truncDiv(a, b) + if a >= 0 then + return math.floor(a / b) + else + return math.ceil(a / b) + end +end + +local function truncMod(a, b) + return a - truncDiv(a, b) * b +end + +--- string Sector.getSectorName(float x, float y) +--- Given x/y coordinates, return the name of the sector that contains those coordinates as a single string. +--- This function and Sector.sectorToXY() define the naming scheme for sectors. To change how sectors are named in a scenario, override these functions to translate coordinate values. +--- Sectors are always rendered in EmptyEpsilon radar views as 20U in size and square in shape. Overriding this function changes only how the sector grid labels are named. +--- Example: +--- Sector.getSectorName(0, 0) -- returns "F5" by default +function Sector.getSectorName(x, y) + local sector_x = math.floor(x / SECTOR_SIZE) + 5 + local sector_y = math.floor(y / SECTOR_SIZE) + 5 + local y_str + local x_str + + if sector_y >= 0 then + if sector_y < 26 then + y_str = string.char(string.byte('A') + sector_y) + else + y_str = string.char(string.byte('A') - 1 + math.floor(sector_y / 26)) .. + string.char(string.byte('A') + (sector_y % 26)) + end + else + y_str = string.char(string.byte('z') + truncDiv(sector_y + 1, 26)) + if truncMod(sector_y, 26) == 0 then + y_str = y_str .. "a" + else + y_str = y_str .. string.char(string.byte('z') + 1 + truncMod(sector_y, 26)) + end + end + + x_str = tostring(sector_x) + return y_str .. x_str +end + +--- float x, float y, bool is_valid Sector.sectorToXY(string sector_name) +--- Given a sector name, return the x/y coordinates for the sector's northwest (top-left) corner point. +--- This also returns a third Boolean value that returns true if the given sector name was valid, or false if not. Check the input's returned validity before applying the returned coordinate values. +--- This function and Sector.getSectorName() define the naming scheme for sectors. To change how sectors are named in a scenario, override these functions to translate coordinate values. +--- Sectors are always rendered in EmptyEpsilon radar views as 20U in size and square in shape. Overriding this function changes only how coordinates translate to labels. +--- Example: +--- Sector.sectorToXY("F5") -- returns 0, 0, true +function Sector.sectorToXY(sector_name) + if #sector_name < 2 then + return 0, 0, false + end + + local x, y + local intpart + + local a1 = string.sub(sector_name, 1, 1) + local a2 = string.sub(sector_name, 2, 2) + + if string.byte(a1) >= string.byte('A') and string.byte(a2) >= string.byte('A') then + intpart = tonumber(string.sub(sector_name, 3)) + if not intpart then + return 0, 0, false + end + if string.byte(a1) > string.byte('a') then + y = ((string.byte('z') - string.byte(a1)) * 26 + (string.byte('z') - string.byte(a2) + 6)) * -SECTOR_SIZE + else + y = ((string.byte(a1) - string.byte('A')) * 26 + (string.byte(a2) - string.byte('A') + 21)) * SECTOR_SIZE + end + else + local alpha = string.upper(a1) + intpart = tonumber(string.sub(sector_name, 2)) + if not intpart then + return 0, 0, false + end + y = (string.byte(alpha) - string.byte('F')) * SECTOR_SIZE + end + + x = (intpart - 5) * SECTOR_SIZE + return x, y, true +end + +function getSectorName(x, y) + return Sector.getSectorName(x, y) +end + +function sectorToXY(sector_name) + return Sector.sectorToXY(sector_name) +end diff --git a/src/gameGlobalInfo.cpp b/src/gameGlobalInfo.cpp index cdf517710f..087d550bfe 100644 --- a/src/gameGlobalInfo.cpp +++ b/src/gameGlobalInfo.cpp @@ -416,18 +416,21 @@ string GameGlobalInfo::getMissionTime() { string getSectorName(glm::vec2 position) { - constexpr float sector_size = 20000; - int sector_x = floorf(position.x / sector_size) + 5; - int sector_y = floorf(position.y / sector_size) + 5; - string y; - string x; - if (sector_y >= 0) - if (sector_y < 26) - y = string(char('A' + (sector_y))); - else - y = string(char('A' - 1 + (sector_y / 26))) + string(char('A' + (sector_y % 26))); - else - y = string(char('z' + ((sector_y + 1) / 26))) + ((sector_y % 26) == 0 ? "a" : string(char('z' + 1 + (sector_y % 26)))); - x = string(sector_x); - return y + x; + if (gameGlobalInfo) + { + if (gameGlobalInfo->main_scenario_script) + { + auto res = gameGlobalInfo->main_scenario_script->call("getSectorName", position.x, position.y); + if (res.isOk()) + return res.value(); + } + if (gameGlobalInfo->script_environment_base) + { + auto res = gameGlobalInfo->script_environment_base->call("getSectorName", position.x, position.y); + if (res.isOk()) + return res.value(); + } + LOG(Warning, "Failed to call Lua getSectorName"); + } + return "??"; } diff --git a/src/gameGlobalInfo.h b/src/gameGlobalInfo.h index 62b02343f8..7c98c87b27 100644 --- a/src/gameGlobalInfo.h +++ b/src/gameGlobalInfo.h @@ -132,4 +132,3 @@ class GameGlobalInfo : public MultiplayerObject, public Updatable }; string getSectorName(glm::vec2 position); -glm::vec2 sectorToXY(string sectorName); diff --git a/src/script.cpp b/src/script.cpp index 13f587c1ac..0c38047334 100644 --- a/src/script.cpp +++ b/src/script.cpp @@ -225,11 +225,6 @@ static void luaVictory(string faction) engine->setGameSpeed(0.0); } -static string luaGetSectorName(float x, float y) -{ - return getSectorName({x, y}); -} - static string luaGetScenarioSetting(string key) { if (gameGlobalInfo->scenario_settings.find(key) != gameGlobalInfo->scenario_settings.end()) @@ -328,60 +323,6 @@ static int luaCreateAdditionalScript(lua_State* L) return 1; } -static int luaSectorToXY(lua_State* L) -{ - string sector = luaL_checkstring(L, 1); - constexpr float sector_size = 20000; - int x, y, intpart; - - if(sector.length() < 2){ - lua_pushnumber(L, 0); - lua_pushnumber(L, 0); - lua_pushboolean(L, false); - return 3; - } - - // Y axis is complicated - if(sector[0] >= char('A') && sector[1] >= char('A')) { - // Case with two letters - char a1 = sector[0]; - char a2 = sector[1]; - try{ - intpart = stoi(sector.substr(2)); - } catch(const std::exception& e) { - lua_pushnumber(L, 0); - lua_pushnumber(L, 0); - lua_pushboolean(L, false); - return 3; - } - if(a1 > char('a')){ - // Case with two lowercase letters (zz10) counting down towards the North - y = (((char('z') - a1) * 26) + (char('z') - a2 + 6)) * -sector_size; // 6 is the offset from F5 to zz5 - }else{ - // Case with two uppercase letters (AB20) counting up towards the South - y = (((a1 - char('A')) * 26) + (a2 - char('A') + 21)) * sector_size; // 21 is the offset from F5 to AA5 - } - }else{ - //Case with just one letter (A9/a9 - these are the same sector, as case only matters in the two-letter sectors) - char alphaPart = toupper(sector[0]); - try{ - intpart = stoi(sector.substr(1)); - }catch(const std::exception& e){ - lua_pushnumber(L, 0); - lua_pushnumber(L, 0); - lua_pushboolean(L, false); - return 3; - } - y = (alphaPart - char('F')) * sector_size; - } - // X axis is simple - x = (intpart - 5) * sector_size; // 5 is the numeric component of the F5 origin - lua_pushnumber(L, x); - lua_pushnumber(L, y); - lua_pushboolean(L, true); - return 3; -} - static bool luaIsInsideZone(float x, float y, sp::ecs::Entity e) { auto zone = e.getComponent(); @@ -1238,22 +1179,6 @@ bool setupScriptEnvironment(sp::script::Environment& env) /// (The GM can unpause the game, but the scenario with its update function is destroyed.) /// Example: victory("Exuari") -- ends the scenario, Exuari win env.setGlobal("victory", &luaVictory); - /// string getSectorName(float x, float y) - /// Returns the name of the sector containing the given x/y coordinates. - /// Coordinates 0,0 are the top-left ("northwest") point of sector F5. - /// See also SpaceObject:getSectorName(). - /// Example: getSectorName(20000,-40000) -- returns "D6" - env.setGlobal("getSectorName", &luaGetSectorName); - /// glm::vec2 sectorToXY(string sector_name) - /// Returns the top-left ("northwest") x/y coordinates for the given sector mame. - /// If the sector name is invalid, this returns coordinates 0, 0. This function also returns a third optional Boolean value that indicates whether the sector name was valid. - /// Examples: - /// x, y = sectorToXY("F5") -- x = 0, y = 0 - /// x, y = sectorToXY("A0") -- x = -100000, y = -100000 - /// x, y = sectorToXY("zz-23") -- x = -560000, y = -120000 - /// x, y, valid = sectorToXY("BA12") -- x = 140000, y = 940000, valid = true - /// x, y, valid = sectorToXY("FOOBAR9000") -- x = 0, y = 0, valid = false - env.setGlobal("sectorToXY", &luaSectorToXY); /// bool isInsideZone(x, y, zone_entity) /// Checks whether the given x/y coordinates are within the specified zone. /// Example: