Merge pull request #233 from IntQuant/rust-api

Implement rust api to allow writing modules in rust.
This commit is contained in:
IQuant 2024-11-26 14:26:47 +03:00 committed by GitHub
commit 5628b78ae2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 9535 additions and 260 deletions

2
.gitignore vendored
View file

@ -4,4 +4,4 @@ save_state
*png~ *png~
/quant.ew/files/system/player/tmp/ /quant.ew/files/system/player/tmp/
/quant.ew/ewext.dll /quant.ew/ewext.dll
/quant.ew/ewext0.dll /quant.ew/ewext0.dll

132
ewext/Cargo.lock generated
View file

@ -40,11 +40,24 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "ewext" name = "ewext"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"eyre",
"iced-x86", "iced-x86",
"libloading", "libloading",
"noita_api",
"noita_api_macro",
]
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
] ]
[[package]] [[package]]
@ -53,6 +66,12 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "iced-x86" name = "iced-x86"
version = "1.21.0" version = "1.21.0"
@ -62,6 +81,18 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -99,6 +130,26 @@ dependencies = [
"adler2", "adler2",
] ]
[[package]]
name = "noita_api"
version = "0.1.0"
dependencies = [
"eyre",
"libloading",
"noita_api_macro",
]
[[package]]
name = "noita_api_macro"
version = "0.1.0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.5" version = "0.36.5"
@ -108,12 +159,91 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ewext" name = "ewext"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
[lib] [lib]
@ -15,3 +15,6 @@ strip = true
libloading = "0.8.5" libloading = "0.8.5"
backtrace = "0.3.74" backtrace = "0.3.74"
iced-x86 = "1.21.0" iced-x86 = "1.21.0"
noita_api_macro = {path = "noita_api_macro"}
eyre = "0.6.12"
noita_api = {path = "noita_api"}

View file

@ -0,0 +1,9 @@
[package]
name = "noita_api"
version = "0.1.0"
edition = "2021"
[dependencies]
eyre = "0.6.12"
libloading = "0.8.5"
noita_api_macro = {path = "../noita_api_macro"}

View file

@ -0,0 +1,81 @@
use std::{borrow::Cow, num::NonZero};
use eyre::{eyre, Context};
pub mod lua;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EntityID(pub NonZero<isize>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComponentID(pub NonZero<isize>);
pub struct Obj(pub usize);
pub struct Color(pub u32);
pub trait Component: From<ComponentID> {
const NAME_STR: &'static str;
}
noita_api_macro::generate_components!();
impl EntityID {
pub fn try_get_first_component<C: Component>(
self,
tag: Option<Cow<'_, str>>,
) -> eyre::Result<Option<C>> {
raw::entity_get_first_component(self, C::NAME_STR.into(), tag)
.map(|x| x.flatten().map(Into::into))
.wrap_err_with(|| eyre!("Failed to get first component {} for {self:?}", C::NAME_STR))
}
pub fn get_first_component<C: Component>(self, tag: Option<Cow<'_, str>>) -> eyre::Result<C> {
self.try_get_first_component(tag)?
.ok_or_else(|| eyre!("Entity {self:?} has no component {}", C::NAME_STR))
}
}
pub mod raw {
use eyre::eyre;
use eyre::Context;
use super::{Color, ComponentID, EntityID, Obj};
use crate::lua::LuaGetValue;
use crate::lua::LuaPutValue;
use std::borrow::Cow;
use crate::lua::LuaState;
noita_api_macro::generate_api!();
pub(crate) fn component_get_value<T>(component: ComponentID, field: &str) -> eyre::Result<T>
where
T: LuaGetValue,
{
let lua = LuaState::current()?;
lua.get_global(c"ComponentGetValue2");
lua.push_integer(component.0.into());
lua.push_string(field);
lua.call(2, T::size_on_stack());
let ret = T::get(lua, -1);
lua.pop_last_n(T::size_on_stack());
ret.wrap_err_with(|| eyre!("Getting {field} for {component:?}"))
}
pub(crate) fn component_set_value<T>(
component: ComponentID,
field: &str,
value: T,
) -> eyre::Result<()>
where
T: LuaPutValue,
{
let lua = LuaState::current()?;
lua.get_global(c"ComponentSetValue2");
lua.push_integer(component.0.into());
lua.push_string(field);
value.put(lua);
lua.call(2 + T::size_on_stack(), 0);
Ok(())
}
}

507
ewext/noita_api/src/lua.rs Normal file
View file

@ -0,0 +1,507 @@
pub mod lua_bindings;
use std::{
borrow::Cow,
cell::Cell,
ffi::{c_char, c_int, CStr},
mem, slice,
sync::LazyLock,
};
use eyre::{bail, Context, OptionExt};
use lua_bindings::{lua_CFunction, lua_State, Lua51, LUA_GLOBALSINDEX};
use crate::{Color, ComponentID, EntityID, Obj};
thread_local! {
static CURRENT_LUA_STATE: Cell<Option<LuaState>> = Cell::default();
}
pub static LUA: LazyLock<Lua51> = LazyLock::new(|| unsafe {
let lib = libloading::Library::new("./lua51.dll").expect("library to exist");
Lua51::from_library(lib).expect("library to be lua")
});
#[derive(Clone, Copy)]
pub struct LuaState {
lua: *mut lua_State,
}
impl LuaState {
pub fn new(lua: *mut lua_State) -> Self {
Self { lua }
}
/// Returns a lua state that is considered "current". Usually set when we get called from noita.
pub fn current() -> eyre::Result<Self> {
CURRENT_LUA_STATE
.get()
.ok_or_eyre("No current lua state available")
}
pub fn make_current(self) {
CURRENT_LUA_STATE.set(Some(self));
}
pub fn raw(&self) -> *mut lua_State {
self.lua
}
pub fn to_integer(&self, index: i32) -> isize {
unsafe { LUA.lua_tointeger(self.lua, index) }
}
pub fn to_number(&self, index: i32) -> f64 {
unsafe { LUA.lua_tonumber(self.lua, index) }
}
pub fn to_bool(&self, index: i32) -> bool {
unsafe { LUA.lua_toboolean(self.lua, index) > 0 }
}
pub 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())
.wrap_err("Attempting to get lua string, expecting it to be utf-8")?)
}
pub fn to_cfunction(&self, index: i32) -> lua_CFunction {
unsafe { LUA.lua_tocfunction(self.lua, index) }
}
pub fn push_number(&self, val: f64) {
unsafe { LUA.lua_pushnumber(self.lua, val) };
}
pub fn push_integer(&self, val: isize) {
unsafe { LUA.lua_pushinteger(self.lua, val) };
}
pub fn push_bool(&self, val: bool) {
unsafe { LUA.lua_pushboolean(self.lua, val as i32) };
}
pub fn push_string(&self, s: &str) {
unsafe {
LUA.lua_pushlstring(self.lua, s.as_bytes().as_ptr() as *const c_char, s.len());
}
}
pub fn push_nil(&self) {
unsafe { LUA.lua_pushnil(self.lua) }
}
pub fn call(&self, nargs: i32, nresults: i32) {
unsafe { LUA.lua_call(self.lua, nargs, nresults) };
}
pub fn get_global(&self, name: &CStr) {
unsafe { LUA.lua_getfield(self.lua, LUA_GLOBALSINDEX, name.as_ptr()) };
}
pub fn objlen(&self, index: i32) -> usize {
unsafe { LUA.lua_objlen(self.lua, index) }
}
pub fn index_table(&self, table_index: i32, index_in_table: usize) {
self.push_integer(index_in_table as isize);
if table_index < 0 {
unsafe { LUA.lua_gettable(self.lua, table_index - 1) };
} else {
unsafe { LUA.lua_gettable(self.lua, table_index) };
}
}
pub fn pop_last(&self) {
unsafe { LUA.lua_settop(self.lua, -2) };
}
pub fn pop_last_n(&self, n: i32) {
unsafe { LUA.lua_settop(self.lua, -1 - (n)) };
}
/// Raise an error with message `s`
///
/// This takes String so that it gets deallocated properly, as this functions doesn't return.
unsafe fn raise_error(&self, s: String) -> ! {
self.push_string(&s);
mem::drop(s);
unsafe { LUA.lua_error(self.lua) };
// lua_error does not return.
unreachable!()
}
fn is_nil_or_none(&self, index: i32) -> bool {
(unsafe { LUA.lua_type(self.lua, index) }) <= 0
}
}
/// Used for types that can be returned from functions that were defined in rust to lua.
pub trait LuaFnRet {
fn do_return(self, lua: LuaState) -> c_int;
}
/// Function intends to return several values that it has on stack.
pub struct ValuesOnStack(pub c_int);
impl LuaFnRet for ValuesOnStack {
fn do_return(self, _lua: LuaState) -> c_int {
self.0
}
}
impl LuaFnRet for () {
fn do_return(self, _lua: LuaState) -> c_int {
0
}
}
impl<R: LuaFnRet> LuaFnRet for eyre::Result<R> {
fn do_return(self, lua: LuaState) -> c_int {
match self {
Ok(ok) => ok.do_return(lua),
Err(err) => unsafe {
lua.raise_error(format!("Error in rust call: {:?}", err));
},
}
}
}
/// Trait for arguments that can be put on lua stack.
pub(crate) trait LuaPutValue {
fn put(&self, lua: LuaState);
fn is_non_empty(&self) -> bool {
true
}
fn size_on_stack() -> i32 {
1
}
}
impl LuaPutValue for i32 {
fn put(&self, lua: LuaState) {
lua.push_integer(*self as isize);
}
}
impl LuaPutValue for isize {
fn put(&self, lua: LuaState) {
lua.push_integer(*self);
}
}
impl LuaPutValue for u32 {
fn put(&self, lua: LuaState) {
lua.push_integer(unsafe { mem::transmute::<_, i32>(*self) as isize });
}
}
impl LuaPutValue for f32 {
fn put(&self, lua: LuaState) {
lua.push_number(*self as f64);
}
}
impl LuaPutValue for f64 {
fn put(&self, lua: LuaState) {
lua.push_number(*self);
}
}
impl LuaPutValue for bool {
fn put(&self, lua: LuaState) {
lua.push_bool(*self);
}
}
impl LuaPutValue for Cow<'_, str> {
fn put(&self, lua: LuaState) {
lua.push_string(self.as_ref());
}
}
impl LuaPutValue for str {
fn put(&self, lua: LuaState) {
lua.push_string(self);
}
}
impl LuaPutValue for EntityID {
fn put(&self, lua: LuaState) {
isize::from(self.0).put(lua);
}
}
impl LuaPutValue for ComponentID {
fn put(&self, lua: LuaState) {
isize::from(self.0).put(lua);
}
}
impl LuaPutValue for Color {
fn put(&self, _lua: LuaState) {
todo!()
}
}
impl LuaPutValue for Obj {
fn put(&self, _lua: LuaState) {
todo!()
}
}
impl<T: LuaPutValue> LuaPutValue for Option<T> {
fn put(&self, lua: LuaState) {
match self {
Some(val) => val.put(lua),
None => lua.push_nil(),
}
}
fn is_non_empty(&self) -> bool {
match self {
Some(val) => val.is_non_empty(),
None => false,
}
}
}
/// Trait for arguments that can be retrieved from the lua stack.
pub(crate) trait LuaGetValue {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self>
where
Self: Sized;
fn size_on_stack() -> i32 {
1
}
}
impl LuaGetValue for i32 {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(lua.to_integer(index) as Self)
}
}
impl LuaGetValue for isize {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(lua.to_integer(index) as Self)
}
}
impl LuaGetValue for u32 {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(unsafe { mem::transmute(lua.to_integer(index) as i32) })
}
}
impl LuaGetValue for f32 {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(lua.to_number(index) as f32)
}
}
impl LuaGetValue for f64 {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(lua.to_number(index))
}
}
impl LuaGetValue for bool {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(lua.to_bool(index))
}
}
impl LuaGetValue for Option<EntityID> {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
let ent = lua.to_integer(index);
Ok(if ent == 0 {
None
} else {
Some(EntityID(ent.try_into().unwrap()))
})
}
}
impl LuaGetValue for Option<ComponentID> {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
let com = lua.to_integer(index);
Ok(if com == 0 {
None
} else {
Some(ComponentID(com.try_into().unwrap()))
})
}
}
impl LuaGetValue for Cow<'static, str> {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(lua.to_string(index)?.into())
}
}
impl LuaGetValue for () {
fn get(_lua: LuaState, _index: i32) -> eyre::Result<Self> {
Ok(())
}
}
impl LuaGetValue for Obj {
fn get(_lua: LuaState, _index: i32) -> eyre::Result<Self> {
todo!()
}
}
impl LuaGetValue for Color {
fn get(_lua: LuaState, _index: i32) -> eyre::Result<Self> {
todo!()
}
}
impl<T: LuaGetValue> LuaGetValue for Option<T> {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok(if lua.is_nil_or_none(index) {
None
} else {
Some(T::get(lua, index)?)
})
}
}
impl<T: LuaGetValue> LuaGetValue for Vec<T> {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
if T::size_on_stack() != 1 {
bail!("Encountered Vec<T> where T needs more than 1 slot on the stack. This isn't supported");
}
let len = lua.objlen(index);
let mut res = Vec::with_capacity(len);
for i in 1..=len {
lua.index_table(index, dbg!(i));
let get = T::get(lua, -1);
lua.pop_last();
res.push(get?);
}
Ok(res)
}
}
impl<T0: LuaGetValue, T1: LuaGetValue> LuaGetValue for (T0, T1) {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self>
where
Self: Sized,
{
Ok((
T0::get(lua, index - T1::size_on_stack())?,
T1::get(lua, index)?,
))
}
fn size_on_stack() -> i32 {
T0::size_on_stack() + T1::size_on_stack()
}
}
impl<T0: LuaGetValue, T1: LuaGetValue, T2: LuaGetValue> LuaGetValue for (T0, T1, T2) {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self>
where
Self: Sized,
{
Ok((
T0::get(lua, index - T1::size_on_stack() - T2::size_on_stack())?,
T1::get(lua, index - T2::size_on_stack())?,
T2::get(lua, index)?,
))
}
fn size_on_stack() -> i32 {
T0::size_on_stack() + T1::size_on_stack() + T2::size_on_stack()
}
}
impl<T0: LuaGetValue, T1: LuaGetValue, T2: LuaGetValue, T3: LuaGetValue> LuaGetValue
for (T0, T1, T2, T3)
{
fn get(lua: LuaState, index: i32) -> eyre::Result<Self>
where
Self: Sized,
{
Ok((
T0::get(
lua,
index - T1::size_on_stack() - T2::size_on_stack() - T3::size_on_stack(),
)?,
T1::get(lua, index - T2::size_on_stack() - T3::size_on_stack())?,
T2::get(lua, index - T3::size_on_stack())?,
T3::get(lua, index)?,
))
}
fn size_on_stack() -> i32 {
T0::size_on_stack() + T1::size_on_stack() + T2::size_on_stack() + T3::size_on_stack()
}
}
impl<T0: LuaGetValue, T1: LuaGetValue, T2: LuaGetValue, T3: LuaGetValue, T4: LuaGetValue>
LuaGetValue for (T0, T1, T2, T3, T4)
{
fn get(lua: LuaState, index: i32) -> eyre::Result<Self>
where
Self: Sized,
{
let prev = <(T0, T1, T2, T3)>::get(lua, index - T4::size_on_stack())?;
Ok((prev.0, prev.1, prev.2, prev.3, T4::get(lua, index)?))
}
fn size_on_stack() -> i32 {
<(T0, T1, T2, T3)>::size_on_stack() + T4::size_on_stack()
}
}
impl<
T0: LuaGetValue,
T1: LuaGetValue,
T2: LuaGetValue,
T3: LuaGetValue,
T4: LuaGetValue,
T5: LuaGetValue,
> LuaGetValue for (T0, T1, T2, T3, T4, T5)
{
fn get(lua: LuaState, index: i32) -> eyre::Result<Self>
where
Self: Sized,
{
let prev = <(T0, T1, T2, T3, T4)>::get(lua, index - T5::size_on_stack())?;
Ok((prev.0, prev.1, prev.2, prev.3, prev.4, T5::get(lua, index)?))
}
fn size_on_stack() -> i32 {
<(T0, T1, T2, T3, T4)>::size_on_stack() + T5::size_on_stack()
}
}
impl LuaGetValue for (bool, bool, bool, f64, f64, f64, f64, f64, f64, f64, f64) {
fn get(lua: LuaState, index: i32) -> eyre::Result<Self> {
Ok((
bool::get(lua, index - 10)?,
bool::get(lua, index - 9)?,
bool::get(lua, index - 8)?,
f64::get(lua, index - 7)?,
f64::get(lua, index - 6)?,
f64::get(lua, index - 5)?,
f64::get(lua, index - 4)?,
f64::get(lua, index - 3)?,
f64::get(lua, index - 2)?,
f64::get(lua, index - 1)?,
f64::get(lua, index)?,
))
}
fn size_on_stack() -> i32 {
11
}
}

105
ewext/noita_api_macro/Cargo.lock generated Normal file
View file

@ -0,0 +1,105 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "noita_api_macro"
version = "0.1.0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"serde",
"serde_json",
]
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"

View file

@ -0,0 +1,18 @@
[workspace]
resolver = "2"
members = ["noita_api", "noita_api_macro"]
[package]
name = "noita_api_macro"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
heck = "0.5.0"
proc-macro2 = "1.0.89"
quote = "1.0.37"
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,312 @@
use std::ffi::CString;
use heck::ToSnekCase;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use serde::Deserialize;
#[derive(Deserialize)]
enum Typ {
#[serde(rename = "int")]
Int,
#[serde(rename = "uint32")]
UInt,
#[serde(rename = "float")]
Float,
#[serde(rename = "double")]
Double,
#[serde(rename = "bool")]
Bool,
#[serde(rename = "std::string")]
StdString,
#[serde(rename = "vec2")]
Vec2,
#[serde(other)]
Other,
}
impl Typ {
fn as_rust_type(&self) -> proc_macro2::TokenStream {
match self {
Typ::Int => quote!(i32),
Typ::UInt => quote!(u32),
Typ::Float => quote!(f32),
Typ::Double => quote!(f64),
Typ::Bool => quote!(bool),
Typ::StdString => quote!(Cow<'_, str>),
Typ::Vec2 => todo!(),
Typ::Other => todo!(),
}
}
}
#[derive(Deserialize)]
enum Typ2 {
#[serde(rename = "int")]
Int,
#[serde(rename = "number")]
Number,
#[serde(rename = "string")]
String,
#[serde(rename = "bool")]
Bool,
#[serde(rename = "entity_id")]
EntityID,
#[serde(rename = "component_id")]
ComponentID,
#[serde(rename = "obj")]
Obj,
#[serde(rename = "color")]
Color,
}
impl Typ2 {
fn as_rust_type(&self) -> proc_macro2::TokenStream {
match self {
Typ2::Int => quote! {i32},
Typ2::Number => quote! {f64},
Typ2::String => quote! {Cow<str>},
Typ2::Bool => quote! {bool},
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>},
Typ2::EntityID => quote! {Option<EntityID>},
Typ2::ComponentID => quote!(Option<ComponentID>),
_ => self.as_rust_type(),
}
}
}
#[derive(Deserialize)]
struct Field {
field: String,
typ: Typ,
desc: String,
}
#[derive(Deserialize)]
struct Component {
name: String,
fields: Vec<Field>,
}
#[derive(Deserialize)]
struct FnArg {
name: String,
typ: Typ2,
default: Option<String>,
}
#[derive(Deserialize)]
struct FnRet {
// name: String,
typ: Typ2,
optional: bool,
is_vec: bool,
}
impl FnRet {
fn as_rust_type_return(&self) -> proc_macro2::TokenStream {
let mut ret = self.typ.as_rust_type_return();
if self.is_vec {
ret = quote! {
Vec<#ret>
};
}
if self.optional {
ret = quote! {
Option<#ret>
};
}
ret
}
}
#[derive(Deserialize)]
struct ApiFn {
fn_name: String,
desc: String,
args: Vec<FnArg>,
rets: Vec<FnRet>,
}
#[proc_macro]
pub fn generate_components(_item: TokenStream) -> TokenStream {
let components: Vec<Component> = serde_json::from_str(include_str!("components.json")).unwrap();
let res = components.into_iter().map(generate_code_for_component);
quote! {#(#res)*}.into()
}
fn convert_field_name(field_name: &str) -> String {
if field_name == "type" {
return "type_fld".to_owned();
}
if field_name == "loop" {
return "loop_fld".to_owned();
}
field_name.to_snek_case()
}
fn generate_code_for_component(com: Component) -> proc_macro2::TokenStream {
let component_name = format_ident!("{}", com.name);
let impls = com.fields.iter().filter_map(|field| {
let field_name_s = convert_field_name(&field.field);
let field_name = format_ident!("{}", field_name_s);
let field_doc = &field.desc;
let set_method_name = format_ident!("set_{}", field_name);
match field.typ {
Typ::Int | Typ::UInt | Typ::Float | Typ::Double | Typ::Bool => {
let field_type = field.typ.as_rust_type();
Some(quote! {
#[doc = #field_doc]
pub fn #field_name(self) -> eyre::Result<#field_type> {
// This trasmute is used to reinterpret i32 as u32 in one case.
raw::component_get_value(self.0, #field_name_s)
}
#[doc = #field_doc]
pub fn #set_method_name(self, value: #field_type) -> eyre::Result<()> {
raw::component_set_value(self.0, #field_name_s, value)
}
})
}
_ => None,
}
});
let com_name = com.name;
quote! {
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct #component_name(pub ComponentID);
impl Component for #component_name {
const NAME_STR: &'static str = #com_name;
}
impl From<ComponentID> for #component_name {
fn from(com: ComponentID) -> Self {
#component_name(com)
}
}
impl #component_name {
#(#impls)*
}
}
}
fn generate_code_for_api_fn(api_fn: ApiFn) -> proc_macro2::TokenStream {
let fn_name = format_ident!("{}", api_fn.fn_name.to_snek_case());
let fn_doc = api_fn.desc;
let args = api_fn.args.iter().map(|arg| {
let arg_name = format_ident!("{}", arg.name);
let arg_type = arg.typ.as_rust_type();
let optional = arg.default.is_some();
if optional {
quote! {
#arg_name: Option<#arg_type>
}
} else {
quote! {
#arg_name: #arg_type
}
}
});
let put_args_pre = api_fn.args.iter().enumerate().map(|(i, arg)| {
let arg_name = format_ident!("{}", arg.name);
let i = i as i32;
quote! {
if LuaPutValue::is_non_empty(&#arg_name) {
last_non_empty = #i;
}
}
});
let put_args = api_fn.args.iter().enumerate().map(|(i, arg)| {
let arg_name = format_ident!("{}", arg.name);
let i = i as i32;
quote! {
if #i <= last_non_empty {
LuaPutValue::put(&#arg_name, lua);
}
}
});
let ret_type = if api_fn.rets.is_empty() {
quote! { () }
} else {
if api_fn.rets.len() == 1 {
let ret = api_fn.rets.first().unwrap();
ret.as_rust_type_return()
} else {
let ret_types = api_fn.rets.iter().map(|ret| ret.as_rust_type_return());
quote! { ( #(#ret_types),* ) }
}
};
let fn_name_c = name_to_c_literal(api_fn.fn_name);
let ret_count = api_fn.rets.len() as i32;
quote! {
#[doc = #fn_doc]
pub fn #fn_name(#(#args,)*) -> eyre::Result<#ret_type> {
let lua = LuaState::current()?;
lua.get_global(#fn_name_c);
let mut last_non_empty: i32 = -1;
#(#put_args_pre)*
#(#put_args)*
lua.call(last_non_empty+1, #ret_count);
let ret = LuaGetValue::get(lua, -1);
lua.pop_last_n(#ret_count);
ret
}
}
}
#[proc_macro]
pub fn generate_api(_item: TokenStream) -> TokenStream {
let api_fns: Vec<ApiFn> = serde_json::from_str(include_str!("lua_api.json")).unwrap();
let res = api_fns.into_iter().map(generate_code_for_api_fn);
quote! {#(#res)*}.into()
}
#[proc_macro]
pub fn add_lua_fn(item: TokenStream) -> TokenStream {
let mut tokens = item.into_iter();
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 = name_to_c_literal(fn_name);
quote! {
unsafe extern "C" fn #bridge_fn_name(lua: *mut lua_State) -> c_int {
let lua_state = noita_api::lua::LuaState::new(lua);
lua_state.make_current();
noita_api::lua::LuaFnRet::do_return(#fn_name_ident(lua_state), lua_state)
}
LUA.lua_pushcclosure(lua, Some(#bridge_fn_name), 0);
LUA.lua_setfield(lua, -2, #fn_name_c.as_ptr());
}
.into()
}
fn name_to_c_literal(name: String) -> proc_macro2::Literal {
proc_macro2::Literal::c_string(CString::new(name).unwrap().as_c_str())
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,11 @@
use std::{os::raw::c_void, ptr}; use std::{mem, os::raw::c_void, ptr, sync::OnceLock};
use iced_x86::{Decoder, DecoderOptions, Mnemonic}; use iced_x86::{Decoder, DecoderOptions, Mnemonic};
use noita_api::lua::LuaState;
use crate::noita::ntypes::{EntityManager, ThiscallFn};
static GRABBED: OnceLock<Grabbed> = OnceLock::new();
pub(crate) unsafe fn grab_addr_from_instruction( pub(crate) unsafe fn grab_addr_from_instruction(
func: *const c_void, func: *const c_void,
@ -25,3 +30,80 @@ pub(crate) unsafe fn grab_addr_from_instruction(
instruction.memory_displacement32() as *mut c_void instruction.memory_displacement32() as *mut c_void
} }
struct Grabbed {
globals: GrabbedGlobals,
fns: GrabbedFns,
}
// This only stores pointers that are constant, so should be safe to share between threads.
unsafe impl Sync for Grabbed {}
unsafe impl Send for Grabbed {}
pub(crate) struct GrabbedGlobals {
// These 3 actually point to a pointer.
pub(crate) _game_global: *mut usize,
pub(crate) _world_state_entity: *mut usize,
pub(crate) entity_manager: *const *mut EntityManager,
}
pub(crate) struct GrabbedFns {
pub(crate) get_entity: *const ThiscallFn, //unsafe extern "C" fn(*const EntityManager, u32) -> *mut Entity,
}
pub(crate) fn grab_addrs(lua: LuaState) {
lua.get_global(c"GameGetWorldStateEntity");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let world_state_entity =
unsafe { grab_addr_from_instruction(base, 0x007aa7ce - 0x007aa540, Mnemonic::Mov).cast() };
println!(
"World state entity addr: 0x{:x}",
world_state_entity as usize
);
lua.pop_last();
lua.get_global(c"GameGetFrameNum");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let load_game_global =
unsafe { grab_addr_from_instruction(base, 0x007bf3c9 - 0x007bf140, Mnemonic::Call) }; // CALL load_game_global
println!("Load game global addr: 0x{:x}", load_game_global as usize);
let game_global = unsafe {
grab_addr_from_instruction(load_game_global, 0x00439c17 - 0x00439bb0, Mnemonic::Mov).cast()
};
println!("Game global addr: 0x{:x}", game_global as usize);
lua.pop_last();
lua.get_global(c"EntityGetFilename");
let base = lua.to_cfunction(-1).unwrap() as *const c_void;
let get_entity = unsafe {
mem::transmute_copy(&grab_addr_from_instruction(
base,
0x0079782b - 0x00797570,
Mnemonic::Call,
))
};
println!("get_entity addr: 0x{:x}", get_entity as usize);
let entity_manager =
unsafe { grab_addr_from_instruction(base, 0x00797821 - 0x00797570, Mnemonic::Mov).cast() };
println!("entity_manager addr: 0x{:x}", entity_manager as usize);
lua.pop_last();
GRABBED
.set(Grabbed {
globals: GrabbedGlobals {
_game_global: game_global,
_world_state_entity: world_state_entity,
entity_manager,
},
fns: GrabbedFns { get_entity },
})
.ok();
}
pub(crate) fn grabbed_fns() -> &'static GrabbedFns {
&GRABBED.get().expect("to be initialized early").fns
}
pub(crate) fn grabbed_globals() -> &'static GrabbedGlobals {
&GRABBED.get().expect("to be initialized early").globals
}

View file

@ -2,28 +2,23 @@ use std::{
arch::asm, arch::asm,
cell::{LazyCell, RefCell}, cell::{LazyCell, RefCell},
ffi::{c_int, c_void}, ffi::{c_int, c_void},
mem, time::Instant,
sync::LazyLock,
}; };
use iced_x86::Mnemonic; use addr_grabber::{grab_addrs, grabbed_fns, grabbed_globals};
use lua_bindings::{lua_State, Lua51, LUA_GLOBALSINDEX}; use eyre::{bail, OptionExt};
use noita::{
ntypes::{Entity, EntityManager, ThiscallFn}, use noita::{ntypes::Entity, pixel::NoitaPixelRun, ParticleWorldState};
NoitaPixelRun, ParticleWorldState, use noita_api::{
lua::{lua_bindings::lua_State, LuaState, ValuesOnStack, LUA},
DamageModelComponent,
}; };
use noita_api_macro::add_lua_fn;
mod lua_bindings; pub mod noita;
mod noita;
mod addr_grabber; mod addr_grabber;
static LUA: LazyLock<Lua51> = LazyLock::new(|| unsafe {
let lib = libloading::Library::new("./lua51.dll").expect("library to exist");
Lua51::from_library(lib).expect("library to be lua")
});
thread_local! { thread_local! {
static STATE: LazyCell<RefCell<ExtState>> = LazyCell::new(|| { static STATE: LazyCell<RefCell<ExtState>> = LazyCell::new(|| {
println!("Initializing ExtState"); println!("Initializing ExtState");
@ -31,37 +26,16 @@ thread_local! {
}); });
} }
struct SavedWorldState {
game_global: usize,
world_state_entity: usize,
}
struct GrabbedGlobals {
// These 3 actually point to a pointer.
game_global: *mut usize,
world_state_entity: *mut usize,
entity_manager: *const *mut EntityManager,
}
struct GrabbedFns {
get_entity: *const ThiscallFn, //unsafe extern "C" fn(*const EntityManager, u32) -> *mut Entity,
}
#[derive(Default)] #[derive(Default)]
struct ExtState { struct ExtState {
particle_world_state: Option<ParticleWorldState>, particle_world_state: Option<ParticleWorldState>,
globals: Option<GrabbedGlobals>,
saved_world_state: Option<SavedWorldState>,
fns: Option<GrabbedFns>,
} }
// const EWEXT: [(&'static str, Function); 1] = [("testfn", None)]; fn init_particle_world_state(lua: LuaState) {
unsafe extern "C" fn init_particle_world_state(lua: *mut lua_State) -> c_int {
println!("\nInitializing particle world state"); println!("\nInitializing particle world state");
let world_pointer = unsafe { LUA.lua_tointeger(lua, 1) }; let world_pointer = lua.to_integer(1);
let chunk_map_pointer = unsafe { LUA.lua_tointeger(lua, 2) }; let chunk_map_pointer = lua.to_integer(2);
let material_list_pointer = unsafe { LUA.lua_tointeger(lua, 3) }; let material_list_pointer = lua.to_integer(3);
println!("pws stuff: {world_pointer:?} {chunk_map_pointer:?}"); println!("pws stuff: {world_pointer:?} {chunk_map_pointer:?}");
STATE.with(|state| { STATE.with(|state| {
@ -72,10 +46,10 @@ unsafe extern "C" fn init_particle_world_state(lua: *mut lua_State) -> c_int {
runner: Default::default(), runner: Default::default(),
}); });
}); });
0
} }
unsafe extern "C" fn encode_area(lua: *mut lua_State) -> c_int { fn encode_area(lua: LuaState) -> ValuesOnStack {
let lua = lua.raw();
let start_x = unsafe { LUA.lua_tointeger(lua, 1) } as i32; let start_x = unsafe { LUA.lua_tointeger(lua, 1) } as i32;
let start_y = unsafe { LUA.lua_tointeger(lua, 2) } as i32; let start_y = unsafe { LUA.lua_tointeger(lua, 2) } as i32;
let end_x = unsafe { LUA.lua_tointeger(lua, 3) } as i32; let end_x = unsafe { LUA.lua_tointeger(lua, 3) } as i32;
@ -88,125 +62,79 @@ unsafe extern "C" fn encode_area(lua: *mut lua_State) -> c_int {
let runs = unsafe { pws.encode_area(start_x, start_y, end_x, end_y, encoded_buffer) }; let runs = unsafe { pws.encode_area(start_x, start_y, end_x, end_y, encoded_buffer) };
unsafe { LUA.lua_pushinteger(lua, runs as isize) }; unsafe { LUA.lua_pushinteger(lua, runs as isize) };
}); });
1 ValuesOnStack(1)
} }
unsafe fn save_world_state() { fn make_ephemerial(lua: LuaState) -> eyre::Result<()> {
STATE.with(|state| {
let mut state = state.borrow_mut();
let game_global = state.globals.as_ref().unwrap().game_global.read();
let world_state_entity = state.globals.as_ref().unwrap().world_state_entity.read();
state.saved_world_state = Some(SavedWorldState {
game_global,
world_state_entity,
})
});
}
unsafe fn load_world_state() {
println!("Loading world state");
STATE.with(|state| {
let state = state.borrow_mut();
let saved_ws = state.saved_world_state.as_ref().unwrap();
let globals = state.globals.as_ref().unwrap();
globals.game_global.write(saved_ws.game_global);
globals
.world_state_entity
.write(saved_ws.world_state_entity);
});
}
unsafe extern "C" fn save_world_state_lua(lua: *mut lua_State) -> i32 {
if STATE.with(|state| state.borrow().globals.is_none()) {
grab_addrs(lua);
}
save_world_state();
0
}
unsafe extern "C" fn load_world_state_lua(_lua: *mut lua_State) -> i32 {
load_world_state();
0
}
unsafe fn grab_addrs(lua: *mut lua_State) {
LUA.lua_getfield(lua, LUA_GLOBALSINDEX, c"GameGetWorldStateEntity".as_ptr());
let base = LUA.lua_tocfunction(lua, -1).unwrap() as *const c_void;
let world_state_entity =
addr_grabber::grab_addr_from_instruction(base, 0x007aa7ce - 0x007aa540, Mnemonic::Mov)
.cast();
println!(
"World state entity addr: 0x{:x}",
world_state_entity as usize
);
// Pop the last element.
LUA.lua_settop(lua, -2);
LUA.lua_getfield(lua, LUA_GLOBALSINDEX, c"GameGetFrameNum".as_ptr());
let base = LUA.lua_tocfunction(lua, -1).unwrap() as *const c_void;
let load_game_global =
addr_grabber::grab_addr_from_instruction(base, 0x007bf3c9 - 0x007bf140, Mnemonic::Call); // CALL load_game_global
println!("Load game global addr: 0x{:x}", load_game_global as usize);
let game_global = addr_grabber::grab_addr_from_instruction(
load_game_global,
0x00439c17 - 0x00439bb0,
Mnemonic::Mov,
)
.cast();
println!("Game global addr: 0x{:x}", game_global as usize);
// Pop the last element.
LUA.lua_settop(lua, -2);
LUA.lua_getfield(lua, LUA_GLOBALSINDEX, c"EntityGetFilename".as_ptr());
let base = LUA.lua_tocfunction(lua, -1).unwrap() as *const c_void;
let get_entity = mem::transmute_copy(&addr_grabber::grab_addr_from_instruction(
base,
0x0079782b - 0x00797570,
Mnemonic::Call,
));
println!("get_entity addr: 0x{:x}", get_entity as usize);
let entity_manager =
addr_grabber::grab_addr_from_instruction(base, 0x00797821 - 0x00797570, Mnemonic::Mov)
.cast();
println!("entity_manager addr: 0x{:x}", entity_manager as usize);
// Pop the last element.
LUA.lua_settop(lua, -2);
STATE.with(|state| {
state.borrow_mut().globals = Some(GrabbedGlobals {
game_global,
world_state_entity,
entity_manager,
});
state.borrow_mut().fns = Some(GrabbedFns { get_entity })
});
}
unsafe extern "C" fn make_ephemerial(lua: *mut lua_State) -> c_int {
unsafe { unsafe {
let entity_id = LUA.lua_tointeger(lua, 1) as u32; let entity_id = lua.to_integer(1) as u32;
STATE.with(|state| {
let state = state.borrow(); let entity_manager = grabbed_globals().entity_manager.read();
let entity_manager = state.globals.as_ref().unwrap().entity_manager.read(); let mut entity: *mut Entity;
let mut entity: *mut Entity; asm!(
asm!( "mov ecx, {entity_manager}",
"mov ecx, {entity_manager}", "push {entity_id:e}",
"push {entity_id:e}", "call {get_entity}",
"call {get_entity}", entity_manager = in(reg) entity_manager,
entity_manager = in(reg) entity_manager, get_entity = in(reg) grabbed_fns().get_entity,
get_entity = in(reg) state.fns.as_ref().unwrap().get_entity, entity_id = in(reg) entity_id,
entity_id = in(reg) entity_id, clobber_abi("C"),
clobber_abi("C"), out("ecx") _,
out("ecx") _, out("eax") entity,
out("eax") entity, );
); if entity.is_null() {
// let entity = (state.fns.as_ref().unwrap().get_entity)(entity_manager, entity_id); bail!("Entity {} not found", entity_id);
entity.cast::<c_void>().offset(0x8).cast::<u32>().write(0); }
}) entity.cast::<c_void>().offset(0x8).cast::<u32>().write(0);
} }
0 Ok(())
}
fn on_world_initialized(lua: LuaState) {
grab_addrs(lua);
}
fn bench_fn(_lua: LuaState) -> eyre::Result<()> {
let start = Instant::now();
let iters = 10000;
for _ in 0..iters {
let player = noita_api::raw::entity_get_closest_with_tag(0.0, 0.0, "player_unit".into())?
.ok_or_eyre("Entity not found")?;
noita_api::raw::entity_set_transform(player, 0.0, Some(0.0), None, None, None)?;
}
let elapsed = start.elapsed();
noita_api::raw::game_print(
format!(
"Took {}us to test, {}ns per call",
elapsed.as_micros(),
elapsed.as_nanos() / iters
)
.into(),
)?;
Ok(())
}
fn test_fn(_lua: LuaState) -> eyre::Result<()> {
let player = noita_api::raw::entity_get_closest_with_tag(0.0, 0.0, "player_unit".into())?
.ok_or_eyre("Entity not found")?;
let damage_model: DamageModelComponent = player.get_first_component(None)?;
let hp = damage_model.hp()?;
damage_model.set_hp(hp - 1.0)?;
let (x, y, _, _, _) = noita_api::raw::entity_get_transform(player)?;
noita_api::raw::game_print(
format!("Component: {:?}, Hp: {}", damage_model.0, hp * 25.0,).into(),
)?;
let entities = noita_api::raw::entity_get_in_radius_with_tag(x, y, 300.0, "enemy".into())?;
noita_api::raw::game_print(format!("{:?}", entities).into())?;
// noita::api::raw::entity_set_transform(player, 0.0, 0.0, 0.0, 1.0, 1.0)?;
Ok(())
} }
/// # Safety /// # Safety
@ -218,16 +146,12 @@ pub unsafe extern "C" fn luaopen_ewext0(lua: *mut lua_State) -> c_int {
unsafe { unsafe {
LUA.lua_createtable(lua, 0, 0); LUA.lua_createtable(lua, 0, 0);
LUA.lua_pushcclosure(lua, Some(init_particle_world_state), 0); add_lua_fn!(init_particle_world_state);
LUA.lua_setfield(lua, -2, c"init_particle_world_state".as_ptr()); add_lua_fn!(encode_area);
LUA.lua_pushcclosure(lua, Some(encode_area), 0); add_lua_fn!(make_ephemerial);
LUA.lua_setfield(lua, -2, c"encode_area".as_ptr()); add_lua_fn!(on_world_initialized);
LUA.lua_pushcclosure(lua, Some(load_world_state_lua), 0); add_lua_fn!(test_fn);
LUA.lua_setfield(lua, -2, c"load_world_state".as_ptr()); add_lua_fn!(bench_fn);
LUA.lua_pushcclosure(lua, Some(save_world_state_lua), 0);
LUA.lua_setfield(lua, -2, c"save_world_state".as_ptr());
LUA.lua_pushcclosure(lua, Some(make_ephemerial), 0);
LUA.lua_setfield(lua, -2, c"make_ephemerial".as_ptr());
} }
println!("Initializing ewext - Ok"); println!("Initializing ewext - Ok");
1 1

View file

@ -1,92 +1,14 @@
use std::{ffi::c_void, mem}; use std::{ffi::c_void, mem};
pub(crate) mod ntypes; pub(crate) mod ntypes;
pub(crate) mod pixel;
#[repr(packed)]
pub(crate) struct NoitaPixelRun {
length: u16,
material: u16,
flags: u8,
}
/// Copied from proxy.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub(crate) struct RawPixel {
pub material: u16,
pub flags: u8,
}
/// Copied from proxy.
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug)]
struct PixelRun<Pixel> {
pub length: u32,
pub data: Pixel,
}
/// Copied from proxy.
/// Converts a normal sequence of pixels to a run-length-encoded one.
pub(crate) struct PixelRunner<Pixel> {
current_pixel: Option<Pixel>,
current_run_len: u32,
runs: Vec<PixelRun<Pixel>>,
}
impl<Pixel: Eq + Copy> Default for PixelRunner<Pixel> {
fn default() -> Self {
Self::new()
}
}
impl<Pixel: Eq + Copy> PixelRunner<Pixel> {
fn new() -> Self {
Self {
current_pixel: None,
current_run_len: 0,
runs: Vec::new(),
}
}
fn put_pixel(&mut self, pixel: Pixel) {
if let Some(current) = self.current_pixel {
if pixel != current {
self.runs.push(PixelRun {
length: self.current_run_len,
data: current,
});
self.current_pixel = Some(pixel);
self.current_run_len = 1;
} else {
self.current_run_len += 1;
}
} else {
self.current_pixel = Some(pixel);
self.current_run_len = 1;
}
}
fn build(&mut self) -> &[PixelRun<Pixel>] {
if self.current_run_len > 0 {
self.runs.push(PixelRun {
length: self.current_run_len,
data: self.current_pixel.expect("has current pixel"),
});
}
&mut self.runs
}
fn clear(&mut self) {
self.current_pixel = None;
self.current_run_len = 0;
self.runs.clear();
}
}
pub(crate) struct ParticleWorldState { pub(crate) struct ParticleWorldState {
pub(crate) _world_ptr: *mut c_void, pub(crate) _world_ptr: *mut c_void,
pub(crate) chunk_map_ptr: *mut c_void, pub(crate) chunk_map_ptr: *mut c_void,
pub(crate) material_list_ptr: *const c_void, pub(crate) material_list_ptr: *const c_void,
pub(crate) runner: PixelRunner<RawPixel>, pub(crate) runner: pixel::PixelRunner<pixel::RawPixel>,
} }
impl ParticleWorldState { impl ParticleWorldState {
@ -127,7 +49,7 @@ impl ParticleWorldState {
start_y: i32, start_y: i32,
end_x: i32, end_x: i32,
end_y: i32, end_y: i32,
mut pixel_runs: *mut NoitaPixelRun, mut pixel_runs: *mut pixel::NoitaPixelRun,
) -> usize { ) -> usize {
// Allow compiler to generate better code. // Allow compiler to generate better code.
assert_eq!(start_x % 128, 0); assert_eq!(start_x % 128, 0);
@ -137,7 +59,7 @@ impl ParticleWorldState {
for y in start_y..end_y { for y in start_y..end_y {
for x in start_x..end_x { for x in start_x..end_x {
let mut raw_pixel = RawPixel { let mut raw_pixel = pixel::RawPixel {
material: 0, material: 0,
flags: 0, flags: 0,
}; };

78
ewext/src/noita/pixel.rs Normal file
View file

@ -0,0 +1,78 @@
#[repr(packed)]
pub(crate) struct NoitaPixelRun {
pub(crate) length: u16,
pub(crate) material: u16,
pub(crate) flags: u8,
}
/// Copied from proxy.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub(crate) struct RawPixel {
pub material: u16,
pub flags: u8,
}
/// Copied from proxy.
/// Stores a run of pixels.
/// Not specific to Noita side - length is an actual length
#[derive(Debug)]
pub(crate) struct PixelRun<Pixel> {
pub length: u32,
pub data: Pixel,
}
/// Copied from proxy.
/// Converts a normal sequence of pixels to a run-length-encoded one.
pub(crate) struct PixelRunner<Pixel> {
pub(crate) current_pixel: Option<Pixel>,
pub(crate) current_run_len: u32,
pub(crate) runs: Vec<PixelRun<Pixel>>,
}
impl<Pixel: Eq + Copy> Default for PixelRunner<Pixel> {
fn default() -> Self {
Self::new()
}
}
impl<Pixel: Eq + Copy> PixelRunner<Pixel> {
pub(crate) fn new() -> Self {
Self {
current_pixel: None,
current_run_len: 0,
runs: Vec::new(),
}
}
pub(crate) fn put_pixel(&mut self, pixel: Pixel) {
if let Some(current) = self.current_pixel {
if pixel != current {
self.runs.push(PixelRun {
length: self.current_run_len,
data: current,
});
self.current_pixel = Some(pixel);
self.current_run_len = 1;
} else {
self.current_run_len += 1;
}
} else {
self.current_pixel = Some(pixel);
self.current_run_len = 1;
}
}
pub(crate) fn build(&mut self) -> &[PixelRun<Pixel>] {
if self.current_run_len > 0 {
self.runs.push(PixelRun {
length: self.current_run_len,
data: self.current_pixel.expect("has current pixel"),
});
}
&mut self.runs
}
pub(crate) fn clear(&mut self) {
self.current_pixel = None;
self.current_run_len = 0;
self.runs.clear();
}
}

View file

@ -9,7 +9,7 @@ local module = {}
function module.on_world_initialized() function module.on_world_initialized()
initial_world_state_entity = GameGetWorldStateEntity() initial_world_state_entity = GameGetWorldStateEntity()
ewext.save_world_state() ewext.on_world_initialized()
local grid_world = world_ffi.get_grid_world() local grid_world = world_ffi.get_grid_world()
local chunk_map = grid_world.vtable.get_chunk_map(grid_world) local chunk_map = grid_world.vtable.get_chunk_map(grid_world)
grid_world = tonumber(ffi.cast("intptr_t", grid_world)) grid_world = tonumber(ffi.cast("intptr_t", grid_world))
@ -35,10 +35,34 @@ function module.on_local_player_spawn()
EntitySetTransform(GameGetWorldStateEntity(), 0, 0) EntitySetTransform(GameGetWorldStateEntity(), 0, 0)
end end
local function fw_button(label)
return imgui.Button(label, imgui.GetWindowWidth() - 15, 20)
end
local function bench_fn_lua()
local start = GameGetRealWorldTimeSinceStarted()
for i=1,10000 do
local player = EntityGetClosestWithTag(0, 0, "player_unit")
EntitySetTransform(player, 0, 0, 0, 1, 1)
end
local elapsed = GameGetRealWorldTimeSinceStarted() - start
GamePrint(elapsed*1000000)
end
function module.on_draw_debug_window(imgui)
if imgui.CollapsingHeader("ewext") then
if fw_button("test_fn") then
ewext.test_fn()
end
if fw_button("bench") then
ewext.bench_fn()
bench_fn_lua()
end
end
end
function module.on_world_update() function module.on_world_update()
if GameGetWorldStateEntity() ~= initial_world_state_entity then if GameGetWorldStateEntity() ~= initial_world_state_entity then
-- -- EntityKill(GameGetWorldStateEntity())
-- ewext.load_world_state()
oh_another_world_state(GameGetWorldStateEntity()) oh_another_world_state(GameGetWorldStateEntity())
initial_world_state_entity = GameGetWorldStateEntity() initial_world_state_entity = GameGetWorldStateEntity()
end end

View file

@ -1,23 +1,40 @@
import shlex import shlex
import json import json
all_types = set()
renames = {
"std_string": "std::string",
}
def parse_component(component): def parse_component(component):
it = iter(component) it = iter(component)
name = next(it) c_name = next(it)
c_name = c_name.strip("\n")
if "-" in c_name or "\n" in c_name:
print(component)
exit(-1)
fields = [] fields = []
for line in it: for line in it:
line = line.strip() line = line.strip()
if line.startswith("-"): if line.startswith("-"):
continue continue
typ, name, *range_info, desc = shlex.split(line) typ, name, *range_info, desc = shlex.split(line)
name = name.strip("\n")
if name == "-":
print(f"Field of type {typ} skipped")
continue
typ = renames.get(typ, typ)
fields.append({ fields.append({
"field": name, "field": name,
"typ": typ, "typ": typ,
"desc": desc, "desc": desc,
}) })
all_types.add(typ)
#print(name, typ, desc, range_info) #print(name, typ, desc, range_info)
return { return {
"name": name, "name": c_name,
"fields": fields, "fields": fields,
} }
@ -26,7 +43,8 @@ path = "/home/quant/.local/share/Steam/steamapps/common/Noita/tools_modding/comp
components = [] components = []
current = [] current = []
for line in open(path):
for i, line in enumerate(open(path)):
if line == "\n": if line == "\n":
if current: if current:
components.append(current) components.append(current)
@ -37,5 +55,7 @@ for line in open(path):
assert not current assert not current
parsed = [parse_component(component) for component in components] parsed = [parse_component(component) for component in components]
json.dump(parsed, open("components.json", "w"), indent=None) json.dump(parsed, open("ewext/noita_api_macro/src/components.json", "w"), indent=None)
#print(*all_types, sep="\n")

170
scripts/parse_lua_api.py Normal file
View file

@ -0,0 +1,170 @@
import json
path = "/home/quant/.local/share/Steam/steamapps/common/Noita/tools_modding/lua_api_documentation.html"
lines = open(path).readlines()
lines_iter = iter(lines)
parsed = []
def maybe_map_types(name, typ):
if typ == "multiple types":
raise ValueError("no 'multiple types' either")
if name == "entity_id":
typ = "entity_id"
if name == "component_id":
typ = "component_id"
if typ == "float":
typ = "number"
if typ == "uint":
typ = "color"
if typ == "uint32":
typ = "color"
if typ == "name":
typ = "string"
if typ == "bool_is_new":
typ = "bool"
if typ == "boolean":
typ = "bool"
if typ == "item_entity_id":
typ = "entity_id"
if typ == "physics_body_id":
raise ValueError(f"{typ} not supported")
return typ
def parse_arg(arg_s):
if "|" in arg_s:
raise ValueError("multiple argument types not supported")
if "{" in arg_s:
raise ValueError("no table support for now")
if "multiple_types" in arg_s:
raise ValueError("no 'multiple_types' either")
other, *default = arg_s.split("=", maxsplit=1)
other = other.strip()
if default:
default = default[0].strip()
else:
default = None
name, typ = other.split(":", maxsplit=1)
typ = maybe_map_types(name, typ)
return {
"name": name,
"typ": typ,
"default": default,
}
def parse_ret(ret_s):
if not ret_s:
return None
optional = ret_s.endswith("|nil")
ret_s = ret_s.removesuffix("|nil")
if "|" in ret_s:
raise ValueError("multiple return types not supported")
if "multiple_types" in ret_s:
raise ValueError("no 'multiple_types' either")
returns_vec = False
if ret_s.startswith("{"):
ret_s = ret_s.removeprefix("{").removesuffix("}")
returns_vec = True
if "-" in ret_s:
raise ValueError("No support for key-value tables in returns")
typ = ret_s
name = None
if ":" in ret_s:
name, typ = ret_s.split(":", maxsplit=1)
if typ.endswith(" -"):
optional = True
typ = typ.removesuffix(" -")
typ = maybe_map_types(name, typ)
return {
"name": name,
"typ": typ,
"optional": optional,
"is_vec": returns_vec,
}
ignore = {
# Those have some specifics that make generic way of handling things not work on them
"PhysicsApplyForceOnArea",
"GetRandomActionWithType",
"GetParallelWorldPosition",
"EntityGetFirstHitboxCenter",
"InputGetJoystickAnalogStick",
"PhysicsAddBodyImage",
"PhysicsBodyIDGetBodyAABB",
"GuiTextInput",
}
skipped = 0
deprecated = 0
# 2 lazy 2 parse xml properly
try:
while True:
line = next(lines_iter)
if line.startswith('<th><span class="function">'):
fn_line = line.strip()
ret_line = next(lines_iter).strip()
desc_line = next(lines_iter).strip()
fn_name, other = fn_line.removeprefix('<th><span class="function">').split('</span>(<span class="field_name">', maxsplit=1)
args = other.removesuffix('</span><span class="func">)</span></th>').strip().split(", ")
try:
args = [parse_arg(arg) for arg in args if ":" in arg]
except ValueError as e:
skipped += 1
print(f"Skipping {fn_name}: {e}")
continue
rets = ret_line.removeprefix('<th><span class="field_name">').removesuffix('</span></th></th>').strip()
desc = desc_line.removeprefix('<th><span class="description">').removesuffix('</span></th></th>').strip().replace("</br>", "\n")
if "Debugish" in rets:
rets, desc = rets.split(" (", maxsplit=1)
desc = desc.removesuffix(")")
rets = rets.split(", ")
try:
rets = [parse_ret(ret) for ret in rets if ret]
except ValueError as e:
print(f"Skipping {fn_name}: {e}")
skipped += 1
continue
if not desc:
desc = "Nolla forgot to include a description :("
if "Deprecated" in desc_line:
deprecated += 1
print(f"Skipping {fn_name}: deprecated")
continue
#print(fn_line, ret_line, desc_line)
if fn_name not in ignore:
#print(fn_name, args, "->", ret)
parsed.append({
"fn_name": fn_name,
"args": args,
"desc": desc,
"rets": rets
})
else:
skipped += 1
except StopIteration:
pass
print("Total skipped:", skipped, deprecated)
print("Total parsed:", len(parsed))
json.dump(parsed, open("ewext/noita_api_macro/src/lua_api.json", "w"), indent=2)