Files
fs2open.github.com/code/scripting/ade_args.h
T
Goober5000andClaude Fable 5.1 37b0b6ea82 Standardize isValid across the Lua API by making the types auto-generate it (#7761)
* Automatically generate the isValid() Lua API function for handle types

If an ADE object's stored type has an isValid() member, ade_obj now registers an isValid function for it automatically.  An explicit ADE_FUNC(isValid, ...) declared after the ADE_OBJ replaces the generated one, so types with special semantics (e.g. sound) keep their own.  The 67 hand-written functions that merely delegated to the member are removed, and 26 types that previously exposed no isValid() gain one.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Add ADE_OBJ_VALIDATOR for index-based Lua handles

Int-index handles (shipclass, weaponclass, team, etc.) can now declare their validity predicate with a single ADE_OBJ_VALIDATOR or ADE_OBJ_VALIDATOR_RANGE line after the ADE_OBJ.  The registered validator drives the generated isValid() function and is also honored by ade_set_args when the mod table asks for nil instead of invalid objects, which previously never applied to these handles.  The 19 hand-written range-check functions are replaced, and loadout_ship, default_primary, default_secondary, and support_rearm_pool_team gain isValid().  Add a validator to HudGauge while we're at it.

Also fully qualify the scripting namespace in LuaConvert.h so its templates can be instantiated from the unit tests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-09-08 10:50:53 -04:00

398 lines
13 KiB
C++

//
//
#ifndef FS2_OPEN_ADE_ARGS_H
#define FS2_OPEN_ADE_ARGS_H
#include "globalincs/pstypes.h"
#include "mod_table/mod_table.h"
#include "scripting/ade.h"
#include "scripting/lua/LuaConvert.h"
#include "scripting/lua/LuaFunction.h"
#include "scripting/lua/LuaTable.h"
#include <utf8/checked.h>
extern "C" {
#include <lauxlib.h>
#include <lualib.h>
}
namespace scripting {
namespace internal {
//*************************Lua hacks*************************
// WMC - Hack to allow for quick&easy return value parsing
extern int Ade_get_args_skip;
// WMC - Tell ade_get_args it is parsing scripting functions,
// which have no upvalues
extern bool Ade_get_args_lfunction;
struct get_args_state {
int nargs = -1;
int counted_args = -1;
int needed_args = -1;
int total_args = -1;
bool optional_args = false;
char funcname[128] = "\0";
};
bool get_single_arg(lua_State* L, const get_args_state& state, char fmt, bool* b);
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, bool>::type
get_single_arg(lua_State* L, const get_args_state& state, char fmt, T* f)
{
Assertion(fmt == 'f' || fmt == 'd', "Invalid character '%c' for number type!", fmt);
if (lua_isnumber(L, state.nargs)) {
*f = (T)lua_tonumber(L, state.nargs);
} else {
LuaError(L, "%s: Argument %d is an invalid type '%s'; number expected", state.funcname, state.nargs,
ade_get_type_string(L, state.nargs));
return false;
}
return true;
}
template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
get_single_arg(lua_State* L, const get_args_state& state, char fmt, T* i)
{
// fix is also an int for C++ so we need to check the format character to determine what should be done
Assertion(fmt == 'i' || fmt == 'x', "Invalid character '%c' for number type!", fmt);
if (lua_isnumber(L, state.nargs)) {
if (fmt == 'x') {
*i = (T)fl2f((float)lua_tonumber(L, state.nargs));
} else {
*i = (T)lua_tonumber(L, state.nargs);
}
} else {
LuaError(L, "%s: Argument %d is an invalid type '%s'; number expected", state.funcname, state.nargs,
ade_get_type_string(L, state.nargs));
return false;
}
return true;
}
bool get_single_arg(lua_State* L, const get_args_state& state, char fmt, const char** s);
template <typename T>
bool ade_odata_getter_helper(lua_State* L, const get_args_state& state, char fmt, T&& od) {
Assertion(fmt == 'o', "Invalid character '%c' for object type!", fmt);
if (lua_isuserdata(L, state.nargs)) {
// Use the helper function
if (!luacpp::convert::popValue(L, std::forward<T>(od), state.nargs, false)) {
return false;
}
} else if (lua_isnil(L, state.nargs) && state.optional_args) {
// WMC - Modder has chosen to ignore this argument
} else {
LuaError(L, "%s: Argument %d is an invalid type '%s'; type '%s' expected", state.funcname, state.nargs,
ade_get_type_string(L, state.nargs), internal::getTableEntry(od.idx).GetName());
return false;
}
return true;
}
template <typename T>
bool get_single_arg(lua_State* L, const get_args_state& state, char fmt, ade_odata_getter<T>&& od)
{
return ade_odata_getter_helper(L, state, fmt, std::forward<ade_odata_getter<T>>(od));
}
template <typename T>
bool get_single_arg(lua_State* L, const get_args_state& state, char fmt, ade_odata_ptr_getter<T>&& od)
{
return ade_odata_getter_helper(L, state, fmt, std::forward<ade_odata_ptr_getter<T>>(od));
}
bool get_single_arg(lua_State* L, const get_args_state& state, char fmt, luacpp::LuaTable* t);
bool get_single_arg(lua_State* L, const get_args_state& state, char fmt, luacpp::LuaFunction* f);
bool get_single_arg(lua_State* L, const get_args_state& state, char fmt, luacpp::LuaValue* f);
// This is not a template function so we can put the implementation in a source file
inline bool get_args_actual(lua_State* /*L*/, get_args_state& state, const char* fmt)
{
while (*fmt != '\0') {
switch(*fmt) {
case '|':
state.optional_args = true;
break;
case '*':
state.nargs++;
state.counted_args++;
break;
default:
Assertion(false, "Invalid format string '%s'!", fmt);
return false;
}
// Ignored parameters are still valid here so we skip them until we reach the end
++fmt;
}
// Now there should be nothing left in the parameter string
Assertion(strlen(fmt) == 0, "No class parameters left but format is not empty!");
return true;
}
template <typename T, typename... Args>
bool get_args_actual(lua_State* L, get_args_state& state, const char* fmt, T&& current, Args&&... args)
{
Assertion(strlen(fmt) > 0, "Format was empty but there were still parameters in the argument list!");
if (state.nargs > state.total_args) {
return true;
}
if (*fmt == '*') {
state.nargs++;
state.counted_args++;
return get_args_actual(L, state, fmt + 1, std::forward<T>(current), std::forward<Args>(args)...);
}
if (*fmt == '|') {
state.optional_args = true;
return get_args_actual(L, state, fmt + 1, std::forward<T>(current), std::forward<Args>(args)...);
}
if (!get_single_arg(L, state, *fmt, std::forward<T>(current))) {
if (!state.optional_args) {
return false;
}
}
state.nargs++;
state.counted_args++;
return get_args_actual(L, state, fmt + 1, std::forward<Args>(args)...);
}
} // namespace internal
/**
* @brief Parses arguments from the Lua stack
*
* based on "Programming in Lua"
*
* Parses arguments from string to variables given
* a '|' divides required and optional arguments.
* Returns 0 if a required argument is invalid,
* or there are too few arguments actually passed
*
* @note This function essentially takes objects
* from the stack in series, so it can easily be used
* to get the return values from a chunk of Lua code
* after it has been executed. See RunByteCode()
*
* @param L The lua state
* @param fmt The argument format string
* @param ... The parameters
* @return The number of arguments taken from the stack or 0 on error
*/
template <typename... Args>
inline int ade_get_args(lua_State* L, const char* fmt, Args&&... args)
{
// Capture these variables locally and reset them. This is needed in case we indirectly call this function again
// from below which would cause some weird issues. This can happen if we call a Lua API function which then causes a
// GC run which then calls one of our destructors.
const auto get_args_skip = internal::Ade_get_args_skip;
const auto get_args_lfunction = internal::Ade_get_args_lfunction;
internal::Ade_get_args_skip = 0;
internal::Ade_get_args_lfunction = false;
// Check that we have all the arguments that we need
// If we don't, return 0
internal::get_args_state state;
state.needed_args = (int)strlen(fmt);
state.total_args = lua_gettop(L) - get_args_skip;
if (strchr(fmt, '|') != nullptr) {
state.needed_args = (int)(strchr(fmt, '|') - fmt);
}
memset(state.funcname, 0, sizeof(state.funcname));
#ifndef NDEBUG
lua_Debug ar;
memset(&ar, 0, sizeof(ar));
if (lua_getstack(L, 0, &ar)) {
lua_getinfo(L, "nl", &ar);
strcpy_s(state.funcname, "");
if (ar.name != nullptr) {
strcat_s(state.funcname, ar.name);
}
if (ar.currentline > -1) {
char buf[33];
sprintf(buf, "%d", ar.currentline);
strcat_s(state.funcname, " (Line ");
strcat_s(state.funcname, buf);
strcat_s(state.funcname, ")");
}
}
#endif
if (!strlen(state.funcname)) {
// WMC - This was causing crashes with user-defined functions.
// WMC - Try and get at function name from upvalue
if (!get_args_lfunction && !lua_isnone(L, lua_upvalueindex(ADE_FUNCNAME_UPVALUE_INDEX))) {
if (lua_type(L, lua_upvalueindex(ADE_FUNCNAME_UPVALUE_INDEX)) == LUA_TSTRING)
strcpy_s(state.funcname, lua_tostring_nullsafe(L, lua_upvalueindex(ADE_FUNCNAME_UPVALUE_INDEX)));
}
// WMC - Totally unknown function
if (!strlen(state.funcname)) {
strcpy_s(state.funcname, "<UNKNOWN>");
}
}
if (state.total_args < state.needed_args) {
LuaError(L,
"Not enough arguments for '%s' - need %d, had %d. If you are using objects or handles, make sure that "
"you are using \":\" to access member functions, rather than \".\"",
state.funcname, state.needed_args, state.total_args);
return 0;
}
// Start throught
state.counted_args = 0;
// Are we parsing optional args yet?
state.optional_args = false;
state.nargs = 1 + get_args_skip;
state.total_args += get_args_skip;
if (!internal::get_args_actual(L, state, fmt, std::forward<Args>(args)...)) {
return 0;
}
return state.counted_args;
}
namespace internal {
struct set_args_state {
int nargs;
int setargs; // args actually set
};
// This is an extremely complicated way of telling the compiler that this function should not be called with pointers
template <typename T>
typename std::enable_if<std::is_same<typename std::remove_cv<T>::type, bool>::value, void>::type
set_single_arg(lua_State* L, char fmt, T b)
{
Assertion(fmt == 'b', "Invalid format character '%c' for boolean type!", fmt);
lua_pushboolean(L, b ? 1 : 0);
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type set_single_arg(lua_State* L, char fmt, T f)
{
Assertion(fmt == 'f' || fmt == 'd', "Invalid character '%c' for number type!", fmt);
lua_pushnumber(L, static_cast<lua_Number>(f));
}
template <typename T>
typename std::enable_if<!std::is_same<T, bool>::value && std::is_integral<T>::value, void>::type
set_single_arg(lua_State* L, char fmt, T i)
{
// fix is also an int for C++ so we need to check the format character to determine what should be done
Assertion(fmt == 'i' || fmt == 'x', "Invalid character '%c' for number type!", fmt);
if (fmt == 'x') {
lua_pushnumber(L, static_cast<lua_Number>(f2fl((fix)i)));
} else {
lua_pushinteger(L, static_cast<lua_Integer>(i));
}
}
void set_single_arg(lua_State* L, char fmt, const char* s);
void set_single_arg(lua_State* L, char fmt, const SCP_string& s);
template<typename T>
void set_single_arg(lua_State* L, char fmt, ade_odata_setter<T>&& od)
{
Assertion(fmt == 'o', "Invalid format character '%c' for object type!", fmt);
if (Lua_API_returns_nil_instead_of_invalid_object) {
// A registered validator wins; otherwise, if the underlying type has an isValid, it'll be evaluated;
// otherwise it'll be always true.
bool isValid = ade_object_is_valid(od.idx, od.value);
if (!isValid) {
lua_pushnil(L);
return;
}
}
// Use the common helper method
luacpp::convert::pushValue(L, std::forward<ade_odata_setter<T>>(od));
}
void set_single_arg(lua_State* /*L*/, char fmt, luacpp::LuaTable* table);
void set_single_arg(lua_State* /*L*/, char fmt, const luacpp::LuaTable& table);
void set_single_arg(lua_State* /*L*/, char fmt, luacpp::LuaFunction* func);
void set_single_arg(lua_State* /*L*/, char fmt, const luacpp::LuaFunction& func);
void set_single_arg(lua_State* /*L*/, char fmt, const luacpp::LuaValue& func);
// This is not a template function so we can put the implementation in a source file
inline void set_args_actual(lua_State* /*L*/, set_args_state& /*state*/, const char* fmt)
{
Assertion(strlen(fmt) == 0, "No class parameters left but format is not empty!");
}
template <typename T, typename... Args>
void set_args_actual(lua_State* L, set_args_state& state, const char* fmt, T&& current, Args&&... args)
{
Assertion(strlen(fmt) > 0, "Format was empty but there were still parameters in the argument list!");
if (*fmt == '*') {
lua_pushnil(L);
state.nargs++;
state.setargs++;
return set_args_actual(L, state, fmt + 1, std::forward<T>(current), std::forward<Args>(args)...);
}
set_single_arg(L, *fmt, std::forward<T>(current));
state.nargs++;
state.setargs++;
return set_args_actual(L, state, fmt + 1, std::forward<Args>(args)...);
}
} // namespace internal
// ade_set_args(state, arguments, variables)
//----------------------------------------------
// based on "Programming in Lua"
//
// Takes variables given and pushes them onto the
// Lua stack. Use it to return variables from a
// Lua scripting function.
//
// NOTE: You can also use this to push arguments
// on to the stack in series. See script_state::SetHookVar
template <typename... Args>
int ade_set_args(lua_State* L, const char* fmt, Args&&... args)
{
// Start throught
internal::set_args_state state{};
state.nargs = 0;
state.setargs = 0;
internal::set_args_actual(L, state, fmt, std::forward<Args>(args)...);
return state.setargs;
}
/**
* @brief Return a value in error. Normally this behaves just like ade_set_args, but it can be configured to return nil instead.
*
* Should be used if the value is not valid
*
* @ingroup ade_api
*/
template <typename... Args>
inline int ade_set_error(lua_State* L, const char* fmt, Args&&... args)
{
if (Lua_API_returns_nil_instead_of_invalid_object)
return 0; // ADE_RETURN_NIL
else
return ade_set_args(L, fmt, std::forward<Args>(args)...);
}
}
#endif //FS2_OPEN_ADE_ARGS_H