mirror of
https://github.com/IntQuant/noita_entangled_worlds.git
synced 2025-10-19 07:03:16 +00:00
Some things seem to work
This commit is contained in:
parent
c7b389a389
commit
2a09cbbfaa
7 changed files with 177 additions and 312 deletions
|
@ -2,6 +2,7 @@ use std::ffi::CString;
|
|||
|
||||
use heck::ToSnekCase;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use quote::{format_ident, quote};
|
||||
use serde::Deserialize;
|
||||
|
||||
|
@ -48,15 +49,13 @@ enum Typ2 {
|
|||
#[serde(rename = "bool")]
|
||||
Bool,
|
||||
#[serde(rename = "entity_id")]
|
||||
EntityId,
|
||||
EntityID,
|
||||
#[serde(rename = "component_id")]
|
||||
ComponentId,
|
||||
ComponentID,
|
||||
#[serde(rename = "obj")]
|
||||
Obj,
|
||||
#[serde(rename = "color")]
|
||||
Color,
|
||||
// #[serde(other)]
|
||||
// Other,
|
||||
}
|
||||
|
||||
impl Typ2 {
|
||||
|
@ -64,14 +63,47 @@ impl Typ2 {
|
|||
match self {
|
||||
Typ2::Int => quote! {i32},
|
||||
Typ2::Number => quote! {f64},
|
||||
Typ2::String => quote! {String},
|
||||
Typ2::String => quote! {Cow<str>},
|
||||
Typ2::Bool => quote! {bool},
|
||||
Typ2::EntityId => quote! {EntityID},
|
||||
Typ2::ComponentId => quote!(ComponentID),
|
||||
Typ2::EntityID => quote! {EntityID},
|
||||
Typ2::ComponentID => quote!(ComponentID),
|
||||
Typ2::Obj => quote! {Obj},
|
||||
Typ2::Color => quote!(Color),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_rust_type_return(&self) -> proc_macro2::TokenStream {
|
||||
match self {
|
||||
Typ2::String => quote! {Cow<'static, str>},
|
||||
_ => self.as_rust_type(),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_lua_push(&self, arg_name: Ident) -> proc_macro2::TokenStream {
|
||||
match self {
|
||||
Typ2::Int => quote! {lua.push_integer(#arg_name as isize)},
|
||||
Typ2::Number => quote! {lua.push_number(#arg_name)},
|
||||
Typ2::String => quote! {lua.push_string(&#arg_name)},
|
||||
Typ2::Bool => quote! {lua.push_bool(#arg_name)},
|
||||
Typ2::EntityID => quote! {lua.push_integer(#arg_name.0 as isize)},
|
||||
Typ2::ComponentID => quote! {lua.push_integer(#arg_name.0 as isize)},
|
||||
Typ2::Obj => quote! { todo!() },
|
||||
Typ2::Color => quote! { todo!() },
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_lua_get(&self, index: i32) -> proc_macro2::TokenStream {
|
||||
match self {
|
||||
Typ2::Int => quote! {lua.to_integer(#index) as i32},
|
||||
Typ2::Number => quote! {lua.to_number(#index)},
|
||||
Typ2::String => quote! { lua.to_string(#index)?.into() },
|
||||
Typ2::Bool => quote! {lua.to_bool(#index)},
|
||||
Typ2::EntityID => quote! {EntityID(lua.to_integer(#index))},
|
||||
Typ2::ComponentID => quote! {ComponentID(lua.to_integer(#index))},
|
||||
Typ2::Obj => quote! { todo!() },
|
||||
Typ2::Color => quote! { todo!() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -90,8 +122,15 @@ struct Component {
|
|||
#[derive(Deserialize)]
|
||||
struct FnArg {
|
||||
name: String,
|
||||
typ: Typ2, // TODO
|
||||
default: Option<String>,
|
||||
typ: Typ2,
|
||||
// default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FnRet {
|
||||
// name: String,
|
||||
typ: Typ2,
|
||||
// optional: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -99,6 +138,7 @@ struct ApiFn {
|
|||
fn_name: String,
|
||||
desc: String,
|
||||
args: Vec<FnArg>,
|
||||
rets: Vec<FnRet>,
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
|
@ -162,10 +202,47 @@ fn generate_code_for_api_fn(api_fn: ApiFn) -> proc_macro2::TokenStream {
|
|||
}
|
||||
});
|
||||
|
||||
let put_args = api_fn.args.iter().map(|arg| {
|
||||
let arg_name = format_ident!("{}", arg.name);
|
||||
arg.typ.generate_lua_push(arg_name)
|
||||
});
|
||||
|
||||
let ret_type = if api_fn.rets.is_empty() {
|
||||
quote! { () }
|
||||
} else {
|
||||
// TODO support for more than one return value.
|
||||
// if api_fn.rets.len() == 1 {
|
||||
let ret = api_fn.rets.first().unwrap();
|
||||
ret.typ.as_rust_type_return()
|
||||
// } else {
|
||||
// quote! { ( /* todo */) }
|
||||
// }
|
||||
};
|
||||
|
||||
let ret_expr = if api_fn.rets.is_empty() {
|
||||
quote! { () }
|
||||
} else {
|
||||
// TODO support for more than one return value.
|
||||
let ret = api_fn.rets.first().unwrap();
|
||||
ret.typ.generate_lua_get(1)
|
||||
};
|
||||
|
||||
let fn_name_c = name_to_c_literal(api_fn.fn_name);
|
||||
|
||||
let arg_count = api_fn.args.len() as i32;
|
||||
let ret_count = api_fn.rets.len() as i32;
|
||||
|
||||
quote! {
|
||||
#[doc = #fn_doc]
|
||||
pub(crate) fn #fn_name(#(#args,)*) {
|
||||
pub(crate) fn #fn_name(#(#args,)*) -> eyre::Result<#ret_type> {
|
||||
let lua = LuaState::current()?;
|
||||
|
||||
lua.get_global(#fn_name_c);
|
||||
#(#put_args;)*
|
||||
|
||||
lua.call(#arg_count, #ret_count);
|
||||
|
||||
Ok(#ret_expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +262,7 @@ pub fn add_lua_fn(item: TokenStream) -> TokenStream {
|
|||
let fn_name = tokens.next().unwrap().to_string();
|
||||
let fn_name_ident = format_ident!("{fn_name}");
|
||||
let bridge_fn_name = format_ident!("{fn_name}_lua_bridge");
|
||||
let fn_name_c = proc_macro2::Literal::c_string(CString::new(fn_name).unwrap().as_c_str());
|
||||
let fn_name_c = name_to_c_literal(fn_name);
|
||||
quote! {
|
||||
unsafe extern "C" fn #bridge_fn_name(lua: *mut lua_State) -> c_int {
|
||||
let lua_state = LuaState::new(lua);
|
||||
|
@ -198,3 +275,7 @@ pub fn add_lua_fn(item: TokenStream) -> TokenStream {
|
|||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn name_to_c_literal(name: String) -> proc_macro2::Literal {
|
||||
proc_macro2::Literal::c_string(CString::new(name).unwrap().as_c_str())
|
||||
}
|
||||
|
|
|
@ -737,34 +737,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "ComponentObjectGetValue2",
|
||||
"args": [
|
||||
{
|
||||
"name": "component_id",
|
||||
"typ": "component_id",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "object_name",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "field_name",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"desc": "Returns one or many values matching the type or subtypes of the requested field in a component subobject. Reports error and returns nil if the field type is not supported or 'object_name' is not a metaobject.",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "multiple types",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "ComponentGetVectorSize",
|
||||
"args": [
|
||||
|
@ -1128,34 +1100,6 @@
|
|||
"desc": "Nolla forgot to include a description :(",
|
||||
"rets": []
|
||||
},
|
||||
{
|
||||
"fn_name": "GetParallelWorldPosition",
|
||||
"args": [
|
||||
{
|
||||
"name": "world_pos_x",
|
||||
"typ": "number",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "world_pos_y",
|
||||
"typ": "number",
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"desc": "x = 0 normal world, -1 is first west world, +1 is first east world, if y < 0 it is sky, if y > 0 it is hell",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "x",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": null,
|
||||
"typ": " y",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "BiomeMapLoad_KeepPlayer",
|
||||
"args": [
|
||||
|
@ -1173,57 +1117,6 @@
|
|||
"desc": "Nolla forgot to include a description :(",
|
||||
"rets": []
|
||||
},
|
||||
{
|
||||
"fn_name": "BiomeGetValue",
|
||||
"args": [
|
||||
{
|
||||
"name": "filename",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "field_name",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"desc": "Can be used to read biome configs. Returns one or many values matching the type or subtypes of the requested field. Reports error and returns nil if the field type is not supported or field was not found.",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "multiple types",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "BiomeMaterialGetValue",
|
||||
"args": [
|
||||
{
|
||||
"name": "filename",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "material_name",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "field_name",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"desc": "Can be used to read biome config MaterialComponents during initialization. Returns the given value in the first found MaterialComponent with matching material_name. See biome_modifiers.lua for an usage example.",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "multiple types",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "GameIsIntroPlaying",
|
||||
"args": [],
|
||||
|
@ -1661,8 +1554,8 @@
|
|||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "bool -",
|
||||
"optional": false
|
||||
"typ": "bool",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2415,29 +2308,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "EntityGetFirstHitboxCenter",
|
||||
"args": [
|
||||
{
|
||||
"name": "entity_id",
|
||||
"typ": "entity_id",
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"desc": "Returns the centroid of first enabled HitboxComponent found in entity, the position of the entity if no hitbox is found, or nil if the entity does not exist. All returned positions are in world coordinates.",
|
||||
"rets": [
|
||||
{
|
||||
"name": "(x",
|
||||
"typ": "number",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"typ": "number)",
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "Raytrace",
|
||||
"args": [
|
||||
|
@ -3331,34 +3201,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "InputGetJoystickAnalogStick",
|
||||
"args": [
|
||||
{
|
||||
"name": "joystick_index",
|
||||
"typ": "int",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "stick_id",
|
||||
"typ": "int",
|
||||
"default": "0"
|
||||
}
|
||||
],
|
||||
"desc": "Debugish function - returns analog stick positions (-1, +1). stick_id 0 = left, 1 = right, Does not depend on state. E.g. player could be in menus. See data/scripts/debug/keycodes.lua for the constants",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "float x",
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"name": null,
|
||||
"typ": " float y",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "IsPlayer",
|
||||
"args": [
|
||||
|
@ -4030,7 +3872,7 @@
|
|||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "name",
|
||||
"typ": "string",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
|
@ -4217,64 +4059,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "PhysicsAddBodyImage",
|
||||
"args": [
|
||||
{
|
||||
"name": "entity_id",
|
||||
"typ": "entity_id",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "image_file",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "material",
|
||||
"typ": "string",
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "offset_x",
|
||||
"typ": "number",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "offset_y",
|
||||
"typ": "number",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "centered",
|
||||
"typ": "bool",
|
||||
"default": "false"
|
||||
},
|
||||
{
|
||||
"name": "is_circle",
|
||||
"typ": "bool",
|
||||
"default": "false"
|
||||
},
|
||||
{
|
||||
"name": "material_image_file",
|
||||
"typ": "string",
|
||||
"default": "\"\""
|
||||
},
|
||||
{
|
||||
"name": "use_image_as_colors",
|
||||
"typ": "bool",
|
||||
"default": "true"
|
||||
}
|
||||
],
|
||||
"desc": "Does not work with PhysicsBody2Component. Returns the id of the created physics body.",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "int_body_id",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "PhysicsAddBodyCreateBox",
|
||||
"args": [
|
||||
|
@ -4833,24 +4617,6 @@
|
|||
"desc": "Nolla forgot to include a description :(",
|
||||
"rets": []
|
||||
},
|
||||
{
|
||||
"fn_name": "PhysicsBodyIDGetBodyAABB",
|
||||
"args": [
|
||||
{
|
||||
"name": "physics_body_id",
|
||||
"typ": "int",
|
||||
"default": null
|
||||
}
|
||||
],
|
||||
"desc": "Nolla forgot to include a description :(",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "nil",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "PhysicsBody2InitFromComponents",
|
||||
"args": [
|
||||
|
@ -5074,7 +4840,7 @@
|
|||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "bool_is_new",
|
||||
"typ": "bool",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
|
@ -6041,59 +5807,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "GuiTextInput",
|
||||
"args": [
|
||||
{
|
||||
"name": "gui",
|
||||
"typ": "obj",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"typ": "int",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "x",
|
||||
"typ": "number",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"typ": "number",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"typ": "string",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "width",
|
||||
"typ": "number",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "max_length",
|
||||
"typ": "int",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"name": "allowed_characters",
|
||||
"typ": "string",
|
||||
"default": "\"\""
|
||||
}
|
||||
],
|
||||
"desc": "'allowed_characters' should consist only of ASCII characters. This is not intended to be outside mod settings menu, and might bug elsewhere.",
|
||||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "new_text",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn_name": "GuiBeginAutoBox",
|
||||
"args": [
|
||||
|
@ -6826,7 +6539,7 @@
|
|||
"rets": [
|
||||
{
|
||||
"name": null,
|
||||
"typ": "boolean",
|
||||
"typ": "bool",
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
|
|
|
@ -99,6 +99,15 @@ fn on_world_initialized(lua: LuaState) {
|
|||
grab_addrs(lua);
|
||||
}
|
||||
|
||||
fn test_fn(_lua: LuaState) -> eyre::Result<()> {
|
||||
let player = noita::api::raw::entity_get_closest_with_tag(0.0, 0.0, "player_unit".into())?;
|
||||
noita::api::raw::entity_set_transform(player, 0.0, 0.0, 0.0, 1.0, 1.0)?;
|
||||
|
||||
// noita::api::raw::game_print("Test game print".into())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Only gets called by lua when loading a module.
|
||||
|
@ -112,6 +121,7 @@ pub unsafe extern "C" fn luaopen_ewext0(lua: *mut lua_State) -> c_int {
|
|||
add_lua_fn!(encode_area);
|
||||
add_lua_fn!(make_ephemerial);
|
||||
add_lua_fn!(on_world_initialized);
|
||||
add_lua_fn!(test_fn);
|
||||
}
|
||||
println!("Initializing ewext - Ok");
|
||||
1
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::{
|
||||
cell::Cell,
|
||||
ffi::{c_char, c_int, CStr},
|
||||
mem,
|
||||
mem, slice,
|
||||
};
|
||||
|
||||
use eyre::OptionExt;
|
||||
use eyre::{bail, Context, OptionExt};
|
||||
|
||||
use crate::{
|
||||
lua_bindings::{lua_CFunction, lua_State, LUA_GLOBALSINDEX},
|
||||
|
@ -20,6 +20,7 @@ pub(crate) struct LuaState {
|
|||
lua: *mut lua_State,
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl LuaState {
|
||||
pub(crate) fn new(lua: *mut lua_State) -> Self {
|
||||
Self { lua }
|
||||
|
@ -44,16 +45,52 @@ impl LuaState {
|
|||
unsafe { LUA.lua_tointeger(self.lua, index) }
|
||||
}
|
||||
|
||||
pub(crate) fn to_number(&self, index: i32) -> f64 {
|
||||
unsafe { LUA.lua_tonumber(self.lua, index) }
|
||||
}
|
||||
|
||||
pub(crate) fn to_bool(&self, index: i32) -> bool {
|
||||
unsafe { LUA.lua_toboolean(self.lua, index) > 0 }
|
||||
}
|
||||
|
||||
pub(crate) fn to_string(&self, index: i32) -> eyre::Result<String> {
|
||||
let mut size = 0;
|
||||
let buf = unsafe { LUA.lua_tolstring(self.lua, index, &mut size) };
|
||||
if buf.is_null() {
|
||||
bail!("Expected a string, but got a null pointer");
|
||||
}
|
||||
let slice = unsafe { slice::from_raw_parts(buf as *const u8, size) };
|
||||
|
||||
Ok(String::from_utf8(slice.to_owned())
|
||||
.context("Attempting to get lua string, expecting it to be utf-8")?)
|
||||
}
|
||||
|
||||
pub(crate) fn to_cfunction(&self, index: i32) -> lua_CFunction {
|
||||
unsafe { LUA.lua_tocfunction(self.lua, index) }
|
||||
}
|
||||
|
||||
pub(crate) fn push_number(&self, val: f64) {
|
||||
unsafe { LUA.lua_pushnumber(self.lua, val) };
|
||||
}
|
||||
|
||||
pub(crate) fn push_integer(&self, val: isize) {
|
||||
unsafe { LUA.lua_pushinteger(self.lua, val) };
|
||||
}
|
||||
|
||||
pub(crate) fn push_bool(&self, val: bool) {
|
||||
unsafe { LUA.lua_pushboolean(self.lua, val as i32) };
|
||||
}
|
||||
|
||||
pub(crate) fn push_string(&self, s: &str) {
|
||||
unsafe {
|
||||
LUA.lua_pushstring(self.lua, s.as_bytes().as_ptr() as *const c_char);
|
||||
LUA.lua_pushlstring(self.lua, s.as_bytes().as_ptr() as *const c_char, s.len());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call(&self, nargs: i32, nresults: i32) {
|
||||
unsafe { LUA.lua_call(self.lua, nargs, nresults) };
|
||||
}
|
||||
|
||||
pub(crate) fn get_global(&self, name: &CStr) {
|
||||
unsafe { LUA.lua_getfield(self.lua, LUA_GLOBALSINDEX, name.as_ptr()) };
|
||||
}
|
||||
|
|
|
@ -3,18 +3,21 @@ use std::{ffi::c_void, mem};
|
|||
pub(crate) mod ntypes;
|
||||
pub(crate) mod pixel;
|
||||
|
||||
mod api {
|
||||
struct EntityID(u32);
|
||||
struct ComponentID(u32);
|
||||
pub(crate) mod api {
|
||||
pub(crate) struct EntityID(isize);
|
||||
pub(crate) struct ComponentID(isize);
|
||||
|
||||
struct Obj(usize);
|
||||
pub(crate) struct Obj(usize);
|
||||
|
||||
struct Color(u32);
|
||||
pub(crate) struct Color(u32);
|
||||
|
||||
noita_api_macro::generate_components!();
|
||||
|
||||
mod raw {
|
||||
pub(crate) mod raw {
|
||||
use super::{Color, ComponentID, EntityID, Obj};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::LuaState;
|
||||
|
||||
noita_api_macro::generate_api!();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue