diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index bd6cafa462..e56a4f736a 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -261,6 +261,7 @@ Flag exe_params[] = { "-output_sexps", "Output SEXPs to sexps.html", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-output_sexps", }, { "-output_scripting", "Output scripting to scripting.html", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-output_scripting", }, { "-output_script_json", "Output scripting doc to scripting.json", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-output_script_json", }, + { "-output_script_lua", "Output scripting doc to scripting.lua", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-output_script_luastub", }, { "-controlconfig_tbl", "Save control presets to table", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-controlconfig_tbl", }, { "-save_render_target", "Save render targets to file", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-save_render_target", }, { "-verify_vps", "Spew VP CRCs to vp_crcs.txt", true, 0, EASY_DEFAULT, "Dev Tool", "http://www.hard-light.net/wiki/index.php/Command-Line_Reference#-verify_vps", }, @@ -577,6 +578,7 @@ cmdline_parm get_flags_arg(GET_FLAGS_STRING, "Output the launcher flags file", A cmdline_parm output_sexp_arg("-output_sexps", NULL, AT_NONE); //WMC - outputs all SEXPs to sexps.html cmdline_parm output_scripting_arg("-output_scripting", NULL, AT_NONE); //WMC cmdline_parm output_script_json_arg("-output_script_json", nullptr, AT_NONE); // m!m +cmdline_parm output_script_luastub_arg("-output_script_lua", nullptr, AT_NONE); // mjnmixael cmdline_parm generate_controlconfig_arg("-controlconfig_tbl", nullptr, AT_NONE); // Deprecated flags - CommanderDJ @@ -2075,6 +2077,9 @@ bool SetCmdlineParams() if (output_script_json_arg.found() ) Output_scripting_json = true; + if (output_script_luastub_arg.found()) + Output_scripting_luastub = true; + if (output_sexp_arg.found() ) { Cmdline_output_sexp_info = true; } diff --git a/code/scripting/api/libs/audio.cpp b/code/scripting/api/libs/audio.cpp index 60f400c7e2..7cfe574938 100644 --- a/code/scripting/api/libs/audio.cpp +++ b/code/scripting/api/libs/audio.cpp @@ -216,7 +216,7 @@ ADE_FUNC(playGameSound, } } -ADE_FUNC(playInterfaceSound, l_Audio, "sound index", "Plays a sound from #Interface Sounds in sounds.tbl", "boolean", "True if sound was played, false if not") +ADE_FUNC(playInterfaceSound, l_Audio, "number index", "Plays a sound from #Interface Sounds in sounds.tbl", "boolean", "True if sound was played, false if not") { int idx; if(!ade_get_args(L, "i", &idx)) diff --git a/code/scripting/api/libs/cfile.cpp b/code/scripting/api/libs/cfile.cpp index d74c2cbe7d..836f2c6ed5 100644 --- a/code/scripting/api/libs/cfile.cpp +++ b/code/scripting/api/libs/cfile.cpp @@ -58,7 +58,7 @@ ADE_FUNC(listFiles, l_CFile, "string directory, string filter", "Lists all the files in the specified directory matching a filter. The filter must have the format " "\"*\" (the wildcard has to appear at the start), \"/*\" (to check subfolder(s)) " "or \"*/*\" (for a glob search).", - "string[]", "A table with all matching files or nil on error") + "table", "A table with all matching files or nil on error") { using namespace luacpp; diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index c2659ccbc0..f6ad1d4a6b 100644 --- a/code/scripting/api/libs/graphics.cpp +++ b/code/scripting/api/libs/graphics.cpp @@ -95,11 +95,6 @@ ADE_FUNC(__len, l_Graphics_Cameras, NULL, "Gets number of cameras", "number", "N //****SUBLIBRARY: Graphics/Fonts ADE_LIB_DERIV(l_Graphics_Fonts, "Fonts", nullptr, "Font library", l_Graphics); -ADE_FUNC(__len, l_Graphics_Fonts, NULL, "Number of loaded fonts", "number", "Number of loaded fonts") -{ - return ade_set_args(L, "i", font::FontManager::numberOfFonts()); -} - ADE_INDEXER(l_Graphics_Fonts, "number/string IndexOrFilename", "Array of loaded fonts", "font", "Font handle, or invalid font handle if index is invalid") { if (lua_isnumber(L, 2)) @@ -129,6 +124,11 @@ ADE_INDEXER(l_Graphics_Fonts, "number/string IndexOrFilename", "Array of loaded } } +ADE_FUNC(__len, l_Graphics_Fonts, nullptr, "Number of loaded fonts", "number", "Number of loaded fonts") +{ + return ade_set_args(L, "i", font::FontManager::numberOfFonts()); +} + ADE_VIRTVAR(CurrentFont, l_Graphics, "font", "Current font", "font", nullptr) { font_h *newFh = nullptr; @@ -550,7 +550,7 @@ ADE_FUNC(setCamera, l_Graphics, "[camera Camera]", "Sets current camera, or rese ADE_FUNC(setColor, l_Graphics, - "number|color /* red value or color object */, number Green, number Blue, [number Alpha]", + "number|color /* red value or color object */, [number Green, number Blue, number Alpha]", "Sets 2D drawing color for the active context; each color number should be from 0 (darkest) to 255 (brightest)", nullptr, nullptr) @@ -584,7 +584,7 @@ ADE_FUNC(setColor, ADE_FUNC(getColor, l_Graphics, - "boolean", + "[boolean]", "Gets the active 2D drawing color from the active context. False to return raw rgb, true to return a color object. Defaults to false.", "number, number, number, number | color", "rgba color which is currently in use for 2D drawing") diff --git a/code/scripting/api/libs/options.cpp b/code/scripting/api/libs/options.cpp index 077785e6f9..3022be42ee 100644 --- a/code/scripting/api/libs/options.cpp +++ b/code/scripting/api/libs/options.cpp @@ -14,7 +14,7 @@ namespace api { //**********LIBRARY: Mission ADE_LIB(l_Options, "Options", "opt", "Options library"); -ADE_VIRTVAR(Options, l_Options, nullptr, "The available options.", "option[]", "A table of all the options.") +ADE_VIRTVAR(Options, l_Options, nullptr, "The available options.", "table", "A table of all the options.") { using namespace luacpp; diff --git a/code/scripting/api/libs/parse.cpp b/code/scripting/api/libs/parse.cpp index 43c302082a..4519da78d5 100644 --- a/code/scripting/api/libs/parse.cpp +++ b/code/scripting/api/libs/parse.cpp @@ -174,7 +174,7 @@ ADE_FUNC(getString, l_Parsing, nullptr, "Gets a single line of text from the fil } } -ADE_FUNC(getFloat, l_Parsing, nullptr, "Gets a floating point number from the file", "string", "number or nil on error") +ADE_FUNC(getFloat, l_Parsing, nullptr, "Gets a floating point number from the file", "number", "number or nil on error") { if (Parse_text == nullptr) { LuaError(L, "Parsing system is currently not active!"); @@ -192,7 +192,7 @@ ADE_FUNC(getFloat, l_Parsing, nullptr, "Gets a floating point number from the fi } } -ADE_FUNC(getInt, l_Parsing, nullptr, "Gets an integer number from the file", "string", "number or nil on error") +ADE_FUNC(getInt, l_Parsing, nullptr, "Gets an integer number from the file", "number", "number or nil on error") { if (Parse_text == nullptr) { LuaError(L, "Parsing system is currently not active!"); diff --git a/code/scripting/api/libs/ui.cpp b/code/scripting/api/libs/ui.cpp index 5cfed27415..399ff63c66 100644 --- a/code/scripting/api/libs/ui.cpp +++ b/code/scripting/api/libs/ui.cpp @@ -133,13 +133,15 @@ ADE_FUNC(enableInput, return ADE_RETURN_TRUE; } -ADE_FUNC(disableInput, l_UserInterface, "", "Disables UI input", "boolean", "true if successful") +ADE_FUNC(disableInput, l_UserInterface, nullptr, "Disables UI input", nullptr, "nothing") { + SCP_UNUSED(L); + scpui::disableInput(); game_flush(); Main_hall_poll_key = true; - return ADE_RETURN_TRUE; + return ADE_RETURN_NIL; } ADE_VIRTVAR(ColorTags, @@ -320,7 +322,7 @@ ADE_VIRTVAR(ErrorCount, l_UserInterface_PilotSelect, nullptr, "The amount of err } ADE_FUNC(enumeratePilots, l_UserInterface_PilotSelect, nullptr, - "Lists all pilots available for the pilot selection
", "string[]", + "Lists all pilots available for the pilot selection
", "table", "A table containing the pilots (without a file extension) or nil on error") { using namespace luacpp; @@ -521,7 +523,7 @@ ADE_LIB_DERIV(l_UserInterface_Barracks, "Barracks", nullptr, l_UserInterface); ADE_FUNC(listPilotImages, l_UserInterface_Barracks, nullptr, "Lists the names of the available pilot images.", - "string[]", "The list of pilot filenames or nil on error") + "table", "The list of pilot filenames or nil on error") { pilot_load_pic_list(); @@ -535,7 +537,7 @@ ADE_FUNC(listPilotImages, l_UserInterface_Barracks, nullptr, "Lists the names of } ADE_FUNC(listSquadImages, l_UserInterface_Barracks, nullptr, "Lists the names of the available squad images.", - "string[]", "The list of squad filenames or nil on error") + "table", "The list of squad filenames or nil on error") { pilot_load_squad_pic_list(); @@ -548,7 +550,7 @@ ADE_FUNC(listSquadImages, l_UserInterface_Barracks, nullptr, "Lists the names of return ade_set_args(L, "t", &out); } -ADE_FUNC(acceptPilot, l_UserInterface_Barracks, "player selection, boolean changeState", "Accept the given player as the current player. Set second argument to false to prevent returning to the mainhall", +ADE_FUNC(acceptPilot, l_UserInterface_Barracks, "player selection, [boolean changeState]", "Accept the given player as the current player. Set second argument to false to prevent returning to the mainhall", "boolean", "true on success, false otherwise") { player_h* plh; @@ -617,7 +619,7 @@ ADE_FUNC(getCampaignList, l_UserInterface_Campaign, nullptr, "Get the campaign name and description lists", - "string[], string[], string[]", + "table, table, table", "Three tables with the names, file names, and descriptions of the campaigns") { luacpp::LuaTable nameTable = luacpp::LuaTable::create(L); @@ -2854,7 +2856,7 @@ ADE_FUNC(getPlayerChannel, "string", "Searches for a player and returns if they were found and the channel they are on. Channel is an empty string if " "channel is private or player is not found.", - "string string", + "string, string", "The response string and the player's channel") { const char* plr_name; @@ -2977,7 +2979,7 @@ ADE_FUNC(__len, l_PXO_Channels, nullptr, "The number of channels available", "nu return ade_set_args(L, "i", static_cast(Multi_pxo_channels.size())); } -ADE_FUNC(joinPrivateChannel, l_UserInterface_MultiPXO, nullptr, "Joins the specified private channel", nullptr, nullptr) +ADE_FUNC(joinPrivateChannel, l_UserInterface_MultiPXO, "string channel", "Joins the specified private channel", nullptr, nullptr) { const char* channel; if (!ade_get_args(L, "s", &channel)) @@ -3311,7 +3313,7 @@ ADE_FUNC(sendJoinRequest, { bool observer = false; - ade_get_args(L, "*|b", &observer); + ade_get_args(L, "|b", &observer); if (Active_games.empty()) { multi_common_add_notify(XSTR("No games found!", 757)); @@ -3736,8 +3738,8 @@ ADE_FUNC(getCountdownTime, l_UserInterface_MultiSync, nullptr, "Gets the current countdown time. Will be -1 before the countdown starts otherwise will be the num seconds until missions starts.", - nullptr, - nullptr) + "number", + "The countdown in seconds") { SCP_UNUSED(L); diff --git a/code/scripting/api/objs/control_config.cpp b/code/scripting/api/objs/control_config.cpp index 500283e50e..4e7b981d4f 100644 --- a/code/scripting/api/objs/control_config.cpp +++ b/code/scripting/api/objs/control_config.cpp @@ -301,7 +301,7 @@ ADE_VIRTVAR(Conflicted, l_Control, nullptr, "Whether or not the bound control has a conflict.", - "boolean", + "string", "Returns the conflict string if true, nil otherwise") { control_h current; diff --git a/code/scripting/api/objs/file.cpp b/code/scripting/api/objs/file.cpp index 29e3fff57a..e3ce260a32 100644 --- a/code/scripting/api/objs/file.cpp +++ b/code/scripting/api/objs/file.cpp @@ -108,7 +108,7 @@ ADE_FUNC(read, "
  • \"*a\" - Reads the rest of the file and returns it as a string.
  • " "
  • \"*l\" - Reads a line. Skips the end of line markers.
  • " "
  • (number) - Reads given number of characters, then returns them as a string.
  • ", - "number|string...", + "string", "Requested data, or nil if the function fails") { cfile_h* cfp = nullptr; diff --git a/code/scripting/api/objs/model.cpp b/code/scripting/api/objs/model.cpp index 211792a922..67d3f2e747 100644 --- a/code/scripting/api/objs/model.cpp +++ b/code/scripting/api/objs/model.cpp @@ -1284,16 +1284,16 @@ ADE_FUNC(computeDocker, l_Dockingbay, "dockingbay", return ade_set_args(L, "oo", l_Vector.Set(final_pos),l_Matrix.Set(matrix_h(&final_orient))); } -ADE_FUNC(isValid, l_Dockingbay, NULL, "Detects whether is valid or not", "number", "true if valid, false otherwise") +ADE_FUNC(isValid, l_Dockingbay, nullptr, "Detects whether is valid or not", "boolean", "true if valid, false otherwise") { - dockingbay_h* dbh = NULL; + dockingbay_h* dbh = nullptr; if (!ade_get_args(L, "o", l_Dockingbay.GetPtr(&dbh))) { return ADE_RETURN_FALSE; } - if (dbh == NULL) + if (dbh == nullptr) { return ADE_RETURN_FALSE; } diff --git a/code/scripting/api/objs/multi_objects.cpp b/code/scripting/api/objs/multi_objects.cpp index e741d9a45b..b0788ac803 100644 --- a/code/scripting/api/objs/multi_objects.cpp +++ b/code/scripting/api/objs/multi_objects.cpp @@ -244,7 +244,7 @@ ADE_VIRTVAR(Description, l_Channel, nullptr, "The description of the channel", " return ade_set_args(L, "s", current.getChannel()->desc); } -ADE_VIRTVAR(NumPlayers, l_Channel, nullptr, "The number of players in the channel", "string", "The number of players") +ADE_VIRTVAR(NumPlayers, l_Channel, nullptr, "The number of players in the channel", "number", "The number of players") { channel_h current; if (!ade_get_args(L, "o", l_Channel.Get(¤t))) { @@ -261,7 +261,7 @@ ADE_VIRTVAR(NumPlayers, l_Channel, nullptr, "The number of players in the channe return ade_set_args(L, "i", static_cast(current.getChannel()->num_users)); } -ADE_VIRTVAR(NumGames, l_Channel, nullptr, "The number of games the channel", "string", "The number of games") +ADE_VIRTVAR(NumGames, l_Channel, nullptr, "The number of games the channel", "number", "The number of games") { channel_h current; if (!ade_get_args(L, "o", l_Channel.Get(¤t))) { @@ -1083,7 +1083,7 @@ ADE_VIRTVAR(EndMission, return ADE_RETURN_NIL; } -ADE_VIRTVAR(SkillLevel, l_NetGame, nullptr, "The current skill level the game, 0-4", "number", "the skill level") +ADE_VIRTVAR(SkillLevel, l_NetGame, "number", "The current skill level the game, 0-4", "number", "the skill level") { net_game_h current; int skill = 0; @@ -1098,7 +1098,7 @@ ADE_VIRTVAR(SkillLevel, l_NetGame, nullptr, "The current skill level the game, 0 return ade_set_args(L, "i", multi_ho_get_skill_level()); } -ADE_VIRTVAR(RespawnLimit, l_NetGame, nullptr, "The current respawn limit", "number", "the respawn limit") +ADE_VIRTVAR(RespawnLimit, l_NetGame, "number", "The current respawn limit", "number", "the respawn limit") { net_game_h current; int respawn = 0; @@ -1116,7 +1116,7 @@ ADE_VIRTVAR(RespawnLimit, l_NetGame, nullptr, "The current respawn limit", "numb return ade_set_args(L, "i", current.getNetgame()->options.respawn); } -ADE_VIRTVAR(TimeLimit, l_NetGame, nullptr, "The current time limit in minutes. -1 means no limit.", "number", "the time limit") +ADE_VIRTVAR(TimeLimit, l_NetGame, "number", "The current time limit in minutes. -1 means no limit.", "number", "the time limit") { net_game_h current; int time = 0; @@ -1136,7 +1136,7 @@ ADE_VIRTVAR(TimeLimit, l_NetGame, nullptr, "The current time limit in minutes. - return ade_set_args(L, "i", current.getNetgame()->options.mission_time_limit); } -ADE_VIRTVAR(KillLimit, l_NetGame, nullptr, "The current kill limit", "number", "the kill limit") +ADE_VIRTVAR(KillLimit, l_NetGame, "number", "The current kill limit", "number", "the kill limit") { net_game_h current; int kill = 0; @@ -1156,7 +1156,7 @@ ADE_VIRTVAR(KillLimit, l_NetGame, nullptr, "The current kill limit", "number", " return ade_set_args(L, "i", current.getNetgame()->options.kill_limit); } -ADE_VIRTVAR(ObserverLimit, l_NetGame, nullptr, "The current observer limit", "number", "the observer limit") +ADE_VIRTVAR(ObserverLimit, l_NetGame, "number", "The current observer limit", "number", "the observer limit") { net_game_h current; int observer = 0; @@ -1176,7 +1176,7 @@ ADE_VIRTVAR(ObserverLimit, l_NetGame, nullptr, "The current observer limit", "nu return ade_set_args(L, "i", current.getNetgame()->options.max_observers); } -ADE_VIRTVAR(Locked, l_NetGame, nullptr, "Whether or not the loadouts have been locked for the current team. Can be set only by the host or team captain.", "number", "the locked status") +ADE_VIRTVAR(Locked, l_NetGame, "boolean", "Whether or not the loadouts have been locked for the current team. Can be set only by the host or team captain.", "boolean", "the locked status") { net_game_h current; bool locked = false; diff --git a/code/scripting/api/objs/weaponclass.cpp b/code/scripting/api/objs/weaponclass.cpp index 5e4091e550..d62d02d84b 100644 --- a/code/scripting/api/objs/weaponclass.cpp +++ b/code/scripting/api/objs/weaponclass.cpp @@ -1101,7 +1101,7 @@ ADE_FUNC(renderTechModel2, ADE_FUNC(renderSelectModel, l_Weaponclass, - "number x, number y, [number width = 629, number height = 355, number currentEffectSetting = default, number zoom = 0.65]", + "boolean restart, number x, number y, [number width = 629, number height = 355, number currentEffectSetting = default, number zoom = 0.65]", "Draws the 3D select weapon model with the chosen effect at the specified coordinates. Restart should " "be true on the first frame this is called and false on subsequent frames. Note that primary weapons " "will not render anything if they do not have a valid pof model defined! Valid selection effects are 1 (fs1) or 2 (fs2), " diff --git a/code/scripting/doc_luastub.cpp b/code/scripting/doc_luastub.cpp new file mode 100644 index 0000000000..b74bc1d785 --- /dev/null +++ b/code/scripting/doc_luastub.cpp @@ -0,0 +1,642 @@ +#include "doc_html.h" +#include "utils/string_utils.h" + +#include + +namespace scripting { +namespace { + +SCP_vector> Hook_variables; +SCP_vector> Aliases; + +//Forward declrattion +SCP_string get_output_type_link(const ade_type_info& type_info, SCP_string nestedFuncName = ""); + +SCP_string sanitizeParamName(const SCP_string& name) +{ + // List of common Lua reserved keywords + static const SCP_vector luaKeywords = {"and", + "break", + "do", + "else", + "elseif", + "end", + "false", + "for", + "function", + "if", + "in", + "local", + "nil", + "not", + "or", + "repeat", + "return", + "then", + "true", + "until", + "while"}; + + // Check if the name matches any Lua keyword + if (util::isStringOneOf(name, luaKeywords)) { + return name + "Val"; + } + + return name; +} + +SCP_string create_function_alias(const ade_type_info& type_info, SCP_string aliasName = "") +{ + static int dupCount = 1; // Static counter for unique aliases + + if (aliasName.empty() || aliasName.find("param") == 0) { + aliasName = "aliasFunc"; + } + + // Sanitize aliasName + for (char& c : aliasName) { + if (c == '?') { + c = '_'; + } + } + + SCP_string alias = "fun("; + + const auto& elements = type_info.elements(); + int count = 1; + + // Process function parameters + for (auto iter = elements.begin() + 1; iter != elements.end(); ++iter) { + if (count > 1) { + alias += ", "; + } + SCP_string paramName = iter->getName().empty() ? "param" + std::to_string(count) : sanitizeParamName(iter->getName()); + alias += paramName + ": " + get_output_type_link(*iter); + count++; + } + + // Add return type + alias += "): " + get_output_type_link(elements.front()); + + // Check for duplicates + auto it = std::find_if(Aliases.begin(), Aliases.end(), [&](const auto& entry) { + return entry.second == alias; + }); + + if (it != Aliases.end()) { + return it->first; // Alias already exists, reuse it + } + + // Check if aliasName conflicts with an existing name + auto nameConflict = std::find_if(Aliases.begin(), Aliases.end(), [&](const auto& entry) { + return entry.first == aliasName; + }); + + // Modify aliasName to make it unique if necessary + if (nameConflict != Aliases.end()) { + aliasName += "_" + std::to_string(dupCount++); + } + + Aliases.emplace_back(aliasName, alias); + + return aliasName; +} + +SCP_string get_output_type_link(const ade_type_info& type_info, SCP_string nestedFuncName) +{ + SCP_string rstring; + + switch (type_info.getType()) { + case ade_type_info_type::Empty: + return "nil"; + + case ade_type_info_type::Simple: + rstring = type_info.getIdentifier(); + return lcase_equal(rstring, "void") ? "nil" : rstring; + + case ade_type_info_type::Tuple: { + bool first = true; + for (const auto& type : type_info.elements()) { + if (!first) + rstring += ", "; + first = false; + rstring += get_output_type_link(type); + } + return rstring; + } + + case ade_type_info_type::Array: + rstring = "{ "; + rstring += get_output_type_link(type_info.arrayType()); + rstring += " ... }"; + return rstring; + + case ade_type_info_type::Map: + rstring = "table"; + if (!type_info.elements().empty() && type_info.elements().size() == 2) { + rstring += "<" + get_output_type_link(type_info.elements()[0]) + ", " + + get_output_type_link(type_info.elements()[1]) + ">"; + } + return rstring; + + case ade_type_info_type::Iterator: + return get_output_type_link(type_info.arrayType()) + "[]"; + + case ade_type_info_type::Alternative: { + bool first = true; + for (const auto& type : type_info.elements()) { + if (!first) + rstring += " | "; + first = false; + rstring += get_output_type_link(type); + } + return rstring; + } + + case ade_type_info_type::Function: + return create_function_alias(type_info, std::move(nestedFuncName)); + + case ade_type_info_type::Generic: { + rstring = get_output_type_link(type_info.elements().front()); + if (type_info.elements().size() > 1) { + rstring += "<"; + bool first = true; + for (auto iter = type_info.elements().begin() + 1; iter != type_info.elements().end(); ++iter) { + if (!first) + rstring += ", "; + first = false; + rstring += get_output_type_link(*iter); + } + rstring += ">"; + } + return rstring; + } + + case ade_type_info_type::Varargs: + return get_output_type_link(type_info.elements().front()); + + default: + Assertion(false, "Unhandled type!"); + return ""; + } +} + +SCP_string escapeNewlines(const SCP_string& str) +{ + SCP_string result = str; + std::replace(result.begin(), result.end(), '\r', ' '); // Replace carriage returns + std::replace(result.begin(), result.end(), '\n', ' '); // Replace newlines + return result; +} + +SCP_string buildFullPath(const SCP_vector& parents, bool func = false) +{ + SCP_string fullPath; + if (!parents.empty()) { + for (size_t i = 0; i < parents.size(); ++i) { + SCP_string name = parents[i]->shortName.empty() ? parents[i]->name : parents[i]->shortName; // Use shortName if available, otherwise name + fullPath += name; + if (i < parents.size() - 1) { // Not the last element + fullPath += "."; + } else if (func && parents[i]->type == ElementType::Class) { // Last element and is a class + fullPath += ":"; + } else { // Last element and is not a class (e.g., library) + fullPath += "."; + } + } + } + return fullPath; +} + +// This is kind of a hacky check but it's the best we can do given the quirks of the FSO API +bool isTableLikeLibrary(const DocumentationElement* el) +{ + if (!el) { + return false; // Null pointer safety check + } + + return el->shortName.empty() && !el->children.empty() && util::isStringOneOf(el->children[0]->name, {"__indexer", "__len"}); +} + +// Function to output a Library +void outputLibrary(FILE* fp, const DocumentationElement* el, const SCP_vector& parents) +{ + SCP_string fullPath = buildFullPath(parents); + SCP_string libraryName = el->shortName.empty() ? el->name : el->shortName; + + if (isTableLikeLibrary(el)) { + // Document as a table of objects + const auto first = static_cast(el->children.front().get()); + fprintf(fp, "\n--- @field %s ", el->name.c_str()); + fputs(get_output_type_link(first->returnType).c_str(), fp); + fprintf(fp, "[] # %s - %s", escapeNewlines(first->description).c_str(), escapeNewlines(first->returnDocumentation).c_str()); + } else { + // Document as a regular library + fprintf(fp, "\n\n--- %s: %s", el->name.c_str(), escapeNewlines(el->description).c_str()); + fprintf(fp, "\n--- @class %s", el->name.c_str()); + } +} + + +// Function to output a Class +void outputClass(FILE* fp, const DocumentationElement* el) +{ + SCP_string escapedDescription; + if (!el->description.empty()) { + escapedDescription = escapeNewlines(el->description); + } + + SCP_string parentClass; + + // If the class can also access the members of the Object class + // This list may be incomplete + if (util::isStringOneOf(el->name, {"asteroid", "debris", "fireball", "ship", "waypoint", "weapon"})) { + parentClass = " : object"; + } + + // Enumeration class also works like integer apparently + if (el->name == "enumeration") { + parentClass = " : integer"; + } + + fprintf(fp, "\n\n-- %s: %s", el->name.c_str(), escapedDescription.c_str()); + fprintf(fp, "\n%s = {}", el->name.c_str()); + fprintf(fp, "\n--- @class %s%s", el->name.c_str(), parentClass.c_str()); +} + +// Function to output a Function/Operator or a Metamethod +void outputFunction(FILE* fp, const DocumentationElement* el, const SCP_vector& parents) +{ + const auto funcEl = static_cast(el); + if (funcEl) { + for (const auto& overload : funcEl->overloads) { + SCP_string functionName = el->name.empty() ? el->shortName : el->name; + + // Functions + if (functionName.rfind("__", 0) != 0) { + // Handle regular functions + fprintf(fp, "\n--- @field %s fun(", functionName.c_str()); + + // Check if the most immediate parent is a Library + bool isLibraryParent = !parents.empty() && parents.back()->type == ElementType::Library; + + int count = 1; + if (!isLibraryParent) { + // Include self: self if the parent is not a Library + // because our class-like objects always require the + // object: func syntax and this ensures that's validated + fputs("self: self", fp); + count++; + } + + // List function arguments + for (const auto& arg : overload.arguments) { + if (count > 1) { + fputs(", ", fp); + } + SCP_string paramName = arg.name.empty() ? "param" + std::to_string(count) : arg.name; + + paramName = sanitizeParamName(paramName); + + if (arg.optional) { + paramName += "?"; + } + + if (arg.type.getType() == ade_type_info_type::Varargs) { + paramName = "..."; + } + + fprintf(fp, "%s: ", paramName.c_str()); + fputs(get_output_type_link(arg.type, paramName).c_str(), fp); + count++; + } + + // Metamethods + } else { + // List of metamethods supported by LuaLS + static const std::vector supportedMetamethods = { + "__unm", // Unary minus + "__bnot", // Bitwise NOT + "__len", // Length + "__add", // Addition + "__sub", // Subtraction + "__mul", // Multiplication + "__div", // Division + "__mod", // Modulo + "__pow", // Exponentiation + "__idiv", // Floor division + "__band", // Bitwise AND + "__bor", // Bitwise OR + "__bxor", // Bitwise XOR + "__shl", // Bitwise LEFT SHIFT + "__shr", // Bitwise RIGHT SHIFT + "__concat", // Concatenation + "__call", // Call + "__indexer" // Indexing + }; + // Check if the metamethod is supported + if (!util::isStringOneOf(functionName, supportedMetamethods)) { + return; + } + + // Indexer is special + if (functionName == "__indexer") { + fputs("\n--- @field [", fp); + } else { + + functionName.erase(0, 2); + fprintf(fp, "\n--- @operator %s(", functionName.c_str()); + } + + // Documentation seems to only allow up to one argument for a metamethod + if (!overload.arguments.empty()) { + fputs(get_output_type_link(overload.arguments[0].type).c_str(), fp); + } + + } + + // Return type + if (functionName == "__indexer") { + fputs("] ", fp); + } else { + fputs("): ", fp); + } + fputs(get_output_type_link(funcEl->returnType).c_str(), fp); + + if (el->description.find("or nil") != std::string::npos || + el->description.find("nil if") != std::string::npos) { + fputs("?", fp); + } + + if (!el->deprecationMessage.empty()) { + SCP_string escapedMessage = escapeNewlines(el->deprecationMessage); + SCP_string version = gameversion::format_version(el->deprecationVersion); + fprintf(fp, " # DEPRECATED %s: %s -- ", version.c_str(), escapedMessage.c_str()); + } + + if (!el->description.empty()) { + SCP_string escapedDescription = escapeNewlines(el->description); + fprintf(fp, " # %s", escapedDescription.c_str()); + } + } + } +} + + +// Function to output a Property +void outputProperty(FILE* fp, const DocumentationElement* el) +{ + if (el) { + const auto propEl = static_cast(el); + SCP_string propName = propEl->name.empty() ? propEl->shortName : propEl->name; + + if (propEl->description.find("or nil") != std::string::npos || + propEl->description.find("nil if") != std::string::npos) + { + propName += "?"; + } + + fprintf(fp, "\n--- @field %s ", propName.c_str()); + fputs(get_output_type_link(propEl->getterType).c_str(), fp); + + if (!propEl->returnDocumentation.empty()) { + fprintf(fp, " # %s, ", propEl->returnDocumentation.c_str()); + } + + if (!el->deprecationMessage.empty()) { + SCP_string escapedMessage = escapeNewlines(el->deprecationMessage); + SCP_string version = gameversion::format_version(el->deprecationVersion); + fprintf(fp, " # DEPRECATED %s: %s -- ", version.c_str(), escapedMessage.c_str()); + } + + fprintf(fp, " %s", propEl->description.empty() ? "No description available." : propEl->description.c_str()); + } +} + +void parseFunctionSignature(SCP_string& input) +{ + // Regular expression to match the input format + static std::regex functionRegex(R"(function\((.*?)\s(\w+)\)\s->\s(.+))"); + std::smatch match; + + if (std::regex_match(input, match, functionRegex)) { + // Extract parameter types, parameter name, and return type + SCP_string paramTypes = match[1].str(); // e.g., "number | string | nil" + SCP_string paramName = match[2].str(); // e.g., "result" + SCP_string returnType = match[3].str(); // e.g., "nil" + + // Format the output as desired + input = "fun(" + paramName + ": " + paramTypes + "): " + returnType; + } +} + +void OutputElement(FILE* fp, const std::unique_ptr& el, const SCP_vector& parents) +{ + if (!el || (el->name.empty() && el->shortName.empty())) { + Warning(LOCATION, "Found ade_table_entry with no name or shortname"); + return; + } + + // First output any child libraries, excluding table-like libraries + for (const auto& child : el->children) { + if (child->type == ElementType::Library && !isTableLikeLibrary(child.get())) { + SCP_vector newParents(parents); + newParents.push_back(el.get()); + OutputElement(fp, child, newParents); + } + } + + switch (el->type) { + case ElementType::Library: + outputLibrary(fp, el.get(), parents); + break; + case ElementType::Class: + outputClass(fp, el.get()); + break; + case ElementType::Function: + case ElementType::Operator: + outputFunction(fp, el.get(), parents); + break; + case ElementType::Property: + outputProperty(fp, el.get()); + break; + default: + Warning(LOCATION, "Unknown type '%d' passed to ade_table_entry::OutputMeta", static_cast(el->type)); + break; + } + + // Now output everything else for non table-like libraries + // If we're a library and we're table-like, then do not print any children + // because we've been defined as an array already and that would be redundant + if (el->type != ElementType::Library || !isTableLikeLibrary(el.get())) { + for (const auto& child : el->children) { + + // If the child is anything but a regular library then output it, including table-like libraries + if (child->type != ElementType::Library || isTableLikeLibrary(child.get())) { + SCP_vector newParents(parents); + newParents.push_back(el.get()); + OutputElement(fp, child, newParents); + } + } + } + + // For Hook Variables we output all the possible HVs as fields of the HV class + // This allows the Lua environment to pick them up as valid variables with defined + // return types. Scripters should refer to scripting.html to know when they are valid + if (el->name == "HookVariables") { + for (const auto& var : Hook_variables) { + SCP_string type = var.second; + if (type.find("function") != SCP_string::npos) { + parseFunctionSignature(type); + } + fprintf(fp, "\n--- @field %s? %s", var.first.c_str(), type.c_str()); + } + } + + // After processing all children, initialize the table if necessary + if (el->type == ElementType::Library && !isTableLikeLibrary(el.get())) { + SCP_string fullPath = buildFullPath(parents); + SCP_string libraryName = el->shortName.empty() ? el->name : el->shortName; + fprintf(fp, "\n%s%s = {}", fullPath.c_str(), libraryName.c_str()); + } +} + +void OutputLuaMeta(FILE* fp, const ScriptingDocumentation& doc) +{ + // First pass: Output classes + for (const auto& el : doc.elements) { + if (el && el->type == ElementType::Class) { + SCP_vector emptyParents; + OutputElement(fp, el, emptyParents); + } + } + + // Second pass: Output everything else (libraries, functions, fields, etc.) + for (const auto& el : doc.elements) { + if (el && el->type != ElementType::Class) { // Skip classes this time + SCP_vector emptyParents; + OutputElement(fp, el, emptyParents); + } + } + + fputs("\n", fp); +} + +void OutputLuaJsonDocumentation(FILE* fp) { + if (!fp) return; + + fputs("--- dkjson is a built-in lua method for encoding and decoding values to/from json files\n", fp); + fputs("--- call dkjson with \n", fp); + fputs("--- @class json\n", fp); + fprintf(fp, "--- @field encode json_encode\n"); + fprintf(fp, "--- @field decode json_decode\n"); + fprintf(fp, "--- @field use_lpeg json_use_lpeg\n"); + fprintf(fp, "--- @field quotestring json_quote_string\n"); + fprintf(fp, "--- @field addnewline json_addnewline\n"); + fprintf(fp, "--- @field encodeexception json_encodeexception\n"); + fputs("json = {}\n\n", fp); + + fputs("--- Encodes a Lua value into a JSON string.\n", fp); + fputs("---- value: any The Lua value to encode (e.g., table, string, number, boolean, nil).\n", fp); + fputs("---- state: table (Optional) A table to configure encoding options:\n", fp); + fputs("--- - `indent`: Enables pretty-printing if set to true.\n", fp); + fputs("--- - `level`: Sets the current indentation level (used internally).\n", fp); + fputs("--- - `keyorder`: Specifies a custom order for object keys.\n", fp); + fputs("--- @return string The JSON string representation of the Lua value.\n", fp); + fputs("--- @alias json_encode fun(value: any, state?: table): string\n\n", fp); + + fputs("--- Decodes a JSON string into a Lua value.\n", fp); + fputs("---- jsonString: string The JSON string to decode.\n", fp); + fputs("---- pos: number (Optional) The position in the string to start decoding (default is 1).\n", fp); + fputs("---- nullval: any (Optional) A value to represent JSON `null` (default is `nil`).\n", fp); + fputs("---- ...: table (Optional) Custom metatables for objects and arrays.\n", fp); + fputs("--- @return any The Lua value resulting from the decoding.\n", fp); + fputs("--- @return number The position in the string where parsing ended.\n", fp); + fputs("--- @return string (Optional) An error message if decoding failed.\n", fp); + fputs("--- @alias json_decode fun(jsonString: string, pos?: number, nullval?: any, ...?: table): any, number, string\n\n", fp); + + fputs("--- Enables the use of LPeg for JSON parsing and encoding.\n", fp); + fputs("--- @return table The `dkjson` library with LPeg integration enabled.\n", fp); + fputs("--- @alias json_use_lpeg fun(): table\n\n", fp); + + fputs("--- Encodes a Lua string as a JSON string literal.\n", fp); + fputs("---- value: string The Lua string to encode.\n", fp); + fputs("--- @return string The JSON string literal representation of the Lua string.\n", fp); + fputs("--- @alias json_quote_string fun(value: string): string\n\n", fp); + + fputs("--- Adds a newline and indentation to the JSON buffer.\n", fp); + fputs("---- state: table The current encoding state.\n", fp); + fputs("--- @alias json_addnewline fun(state: table): nil\n\n", fp); + + fputs("--- Handles encoding exceptions by returning a placeholder JSON string.\n", fp); + fputs("---- reason: string The reason for the exception.\n", fp); + fputs("---- value: any The value that caused the exception.\n", fp); + fputs("---- state: table The current encoding state.\n", fp); + fputs("---- defaultmessage: string The default error message.\n", fp); + fputs("--- @return string A JSON string representing the exception.\n", fp); + fputs("--- @alias json_encodeexception fun(reason: string, value: any, state: table, defaultmessage: string): string\n", fp); +} + +} // namespace + +void output_luastub_doc(const ScriptingDocumentation& doc, const SCP_string& filename) +{ + FILE* fp = fopen(filename.c_str(), "w"); + + if (fp == nullptr) { + return; + } + + // Header + fprintf(fp, "-- Lua Stub File\n"); + fprintf(fp, "-- Generated for FSO v%s (%s)\n\n", FS_VERSION_FULL, doc.name.c_str()); + + //***Version info + fprintf(fp, "-- Lua Version: %s\n", LUA_RELEASE); + fputs("---@meta\n", fp); + + // Get all the hook variables to print and save them to the vector + std::unordered_set uniqueHookVars; + for (const auto& action : doc.actions) { + if (!action.description.empty()) { + for (const auto& param : action.parameters) { + if (uniqueHookVars.insert(param.name).second) { // Insert returns false if already exists + Hook_variables.emplace_back(param.name, param.type.getIdentifier()); + } + } + } + } + + OutputLuaMeta(fp, doc); + + fputs("\n-- Aliases help with nested function calls in the above pseudo classes", fp); + for (const auto& alias : Aliases) { + fprintf(fp, "\n--- @alias %s %s", alias.first.c_str(), alias.second.c_str()); + } + + fputs("\n", fp); + fputs("\n", fp); + + //***Enumerations + fprintf(fp, "-- Enumerations\n"); + for (const auto& enumeration : doc.enumerations) { + // Cyborg17 -- Omit the deprecated flag + if (enumeration.name == "VM_EXTERNAL_CAMERA_LOCKED") { + continue; + } + // WMC - For now, print to the file without the description. + fprintf(fp, "--- @const %s\n", enumeration.name.c_str()); + fprintf(fp, "%s = enumeration\n", enumeration.name.c_str()); + } + fputs("\n", fp); + + OutputLuaJsonDocumentation(fp); + fputs("\n", fp); + + fclose(fp); + + Hook_variables.clear(); + Aliases.clear(); +} + +} // namespace scripting diff --git a/code/scripting/doc_luastub.h b/code/scripting/doc_luastub.h new file mode 100644 index 0000000000..3f18e43f65 --- /dev/null +++ b/code/scripting/doc_luastub.h @@ -0,0 +1,9 @@ +#pragma once + +#include "scripting_doc.h" + +namespace scripting { + +void output_luastub_doc(const ScriptingDocumentation& doc, const SCP_string& filename); + +} diff --git a/code/scripting/scripting.cpp b/code/scripting/scripting.cpp index c8b6e683ee..93ba85a6b2 100644 --- a/code/scripting/scripting.cpp +++ b/code/scripting/scripting.cpp @@ -19,6 +19,7 @@ #include "parse/parselo.h" #include "scripting/doc_html.h" #include "scripting/doc_json.h" +#include "scripting/doc_luastub.h" #include "scripting/global_hooks.h" #include "scripting/scripting_doc.h" #include "ship/ship.h" @@ -32,6 +33,7 @@ using namespace scripting; script_state Script_system("FS2_Open Scripting"); bool Output_scripting_meta = false; bool Output_scripting_json = false; +bool Output_scripting_luastub = false; bool Scripting_game_init_run = false; flag_def_list Script_conditions[] = @@ -154,7 +156,7 @@ void script_init() mprintf(("SCRIPTING: Beginning Lua initialization...\n")); Script_system.CreateLuaState(); - if (Output_scripting_meta || Output_scripting_json) { + if (Output_scripting_meta || Output_scripting_json || Output_scripting_luastub) { const auto doc = Script_system.OutputDocumentation([](const SCP_string& error) { mprintf(("Scripting documentation: Error while parsing\n%s(This is only relevant for coders)\n\n", error.c_str())); @@ -168,6 +170,10 @@ void script_init() mprintf(("SCRIPTING: Outputting scripting metadata in JSON format...\n")); scripting::output_json_doc(doc, "scripting.json"); } + if (Output_scripting_luastub) { + mprintf(("SCRIPTING: Outputting scripting metadata in LUA Stub format...\n")); + scripting::output_luastub_doc(doc, "scripting.lua"); + } } mprintf(("SCRIPTING: Beginning main hook parse sequence....\n")); diff --git a/code/scripting/scripting.h b/code/scripting/scripting.h index d5dc9dc1fa..c9b68d6aff 100644 --- a/code/scripting/scripting.h +++ b/code/scripting/scripting.h @@ -351,6 +351,7 @@ void script_init(); extern class script_state Script_system; extern bool Output_scripting_meta; extern bool Output_scripting_json; +extern bool Output_scripting_luastub; extern bool Scripting_game_init_run; //*************************Conditional scripting************************* diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 324e2b5d5f..af16749c9e 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1267,6 +1267,8 @@ add_file_folder("Scripting" scripting/doc_html.h scripting/doc_json.cpp scripting/doc_json.h + scripting/doc_luastub.cpp + scripting/doc_luastub.h scripting/doc_parser.cpp scripting/doc_parser.h scripting/global_hooks.cpp diff --git a/code/utils/string_utils.cpp b/code/utils/string_utils.cpp index 81de4f23f6..24fef69fd0 100644 --- a/code/utils/string_utils.cpp +++ b/code/utils/string_utils.cpp @@ -6,7 +6,8 @@ namespace util { -std::vector split_string(const std::string& s, char delim) { +std::vector split_string(const std::string& s, char delim) +{ std::vector elems; split_string(s, delim, std::back_inserter(elems)); return elems;