#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod gdscript_layer;
pub mod generated;
pub mod lookup;
pub mod model;
use rustc_hash::FxHashMap;
pub use lookup::MemberRef;
pub use model::{
ApiData, ApiType, ApiVersion, BuiltinData, BuiltinId, BuiltinMember, ClassData, ClassId,
ConstInfo, DocId, DocStore, ElemRef, EnumInfo, EnumValue, MethodSig, OperatorSig, Param,
PropertyInfo, SignalSig, TyRef, UtilityFn,
};
#[must_use]
pub fn godot_version() -> &'static str {
generated::GODOT_VERSION
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadError {
Decode(String),
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Decode(msg) => write!(f, "failed to decode engine-API blob: {msg}"),
}
}
}
impl std::error::Error for LoadError {}
#[derive(Debug)]
pub struct EngineApi {
pub(crate) data: ApiData,
pub(crate) class_by_name: FxHashMap<String, ClassId>,
pub(crate) builtin_by_name: FxHashMap<String, BuiltinId>,
pub(crate) singleton_by_name: FxHashMap<String, ClassId>,
pub(crate) utility_by_name: FxHashMap<String, u32>,
pub(crate) global_enum_by_name: FxHashMap<String, u32>,
pub(crate) global_consts: Vec<gdscript_layer::GlobalConst>,
pub(crate) gdscript_builtins: Vec<gdscript_layer::BuiltinFn>,
pub(crate) int_builtin: Option<BuiltinId>,
pub(crate) docs: Option<DocStore>,
}
impl EngineApi {
#[must_use]
pub fn from_data(data: ApiData) -> Self {
let mut class_by_name = FxHashMap::default();
for (i, c) in data.classes.iter().enumerate() {
class_by_name.insert(
c.name.clone(),
ClassId(u32::try_from(i).unwrap_or(u32::MAX)),
);
}
let mut builtin_by_name = FxHashMap::default();
for (i, b) in data.builtins.iter().enumerate() {
builtin_by_name.insert(
b.name.clone(),
BuiltinId(u32::try_from(i).unwrap_or(u32::MAX)),
);
}
let singleton_by_name = data
.singletons
.iter()
.map(|(name, id)| (name.clone(), *id))
.collect();
let mut utility_by_name = FxHashMap::default();
for (i, u) in data.utilities.iter().enumerate() {
utility_by_name.insert(u.name.clone(), u32::try_from(i).unwrap_or(u32::MAX));
}
let mut global_enum_by_name = FxHashMap::default();
for (i, e) in data.global_enums.iter().enumerate() {
global_enum_by_name.insert(e.name.clone(), u32::try_from(i).unwrap_or(u32::MAX));
}
let int_builtin = builtin_by_name.get("int").copied();
Self {
data,
class_by_name,
builtin_by_name,
singleton_by_name,
utility_by_name,
global_enum_by_name,
global_consts: gdscript_layer::global_consts(),
gdscript_builtins: gdscript_layer::builtin_fns(),
int_builtin,
docs: None,
}
}
pub fn set_docs(&mut self, docs: DocStore) {
self.docs = Some(docs);
}
#[must_use]
pub fn doc(&self, id: DocId) -> Option<&str> {
self.docs.as_ref()?.get(id)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
let mut aligned = rkyv::util::AlignedVec::<16>::new();
aligned.extend_from_slice(bytes);
let data = rkyv::from_bytes::<ApiData, rkyv::rancor::Error>(aligned.as_slice())
.map_err(|e| LoadError::Decode(e.to_string()))?;
Ok(Self::from_data(data))
}
#[must_use]
pub fn version(&self) -> &ApiVersion {
&self.data.version
}
}
impl DocStore {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
let mut aligned = rkyv::util::AlignedVec::<16>::new();
aligned.extend_from_slice(bytes);
rkyv::from_bytes::<Self, rkyv::rancor::Error>(aligned.as_slice())
.map_err(|e| LoadError::Decode(e.to_string()))
}
}
#[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
#[must_use]
pub fn bundled() -> &'static EngineApi {
use std::sync::OnceLock;
static BUNDLED: OnceLock<EngineApi> = OnceLock::new();
static BYTES: &[u8] = include_bytes!("engine_api.bin");
BUNDLED.get_or_init(|| {
let mut api =
EngineApi::from_bytes(BYTES).expect("the bundled engine-API blob must be valid");
#[cfg(feature = "bundled-docs")]
{
static DOC_BYTES: &[u8] = include_bytes!("engine_docs.bin");
if let Ok(docs) = DocStore::from_bytes(DOC_BYTES) {
api.set_docs(docs);
}
}
api
})
}
#[cfg(test)]
mod tests {
#[test]
fn generated_metadata_is_present() {
assert!(!crate::generated::GODOT_VERSION.is_empty());
}
#[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
#[test]
fn bundled_blob_loads_and_resolves_golden_symbols() {
let api = crate::bundled();
assert_eq!(api.version().major, 4);
assert_eq!(api.version().minor, 5);
let node = api.class_by_name("Node").expect("Node class present");
let node2d = api.class_by_name("Node2D").expect("Node2D class present");
assert!(api.lookup_member(node, "add_child").is_some());
assert!(api.is_subclass(node2d, node), "Node2D is a Node");
assert!(
api.lookup_member(node2d, "add_child").is_some(),
"add_child is inherited onto Node2D"
);
let members = api.members_of(node2d);
assert!(members.iter().any(|m| m.name() == "add_child"));
assert!(members.iter().any(|m| m.name() == "position"));
assert!(api.singleton("Input").is_some());
let v2 = api
.builtin_by_name("Vector2")
.expect("Vector2 builtin present");
assert!(api.builtin_member(v2, "x").is_some());
assert!(api.builtin_operators(v2).iter().any(|o| o.op == "+"));
let process_mode = api
.class(node)
.properties
.iter()
.find(|p| p.name == "process_mode")
.expect("Node.process_mode present");
assert!(
process_mode.enum_of.is_some(),
"process_mode is recovered as enum-typed from its getter"
);
assert!(api.global_const("PI").is_some());
assert!(api.gdscript_builtin("preload").is_some());
}
#[cfg(all(feature = "bundled-docs", not(target_arch = "wasm32")))]
#[test]
fn bundled_docs_resolve_and_are_converted() {
use crate::MemberRef;
let api = crate::bundled();
let node = api.class_by_name("Node").expect("Node class");
assert!(
api.class(node).doc.and_then(|id| api.doc(id)).is_some(),
"Node has a class hover doc"
);
let MemberRef::Method(m) = api.lookup_member(node, "add_child").expect("add_child") else {
panic!("add_child is a method");
};
let doc = m
.doc
.and_then(|id| api.doc(id))
.expect("add_child hover doc");
assert!(!doc.contains("[/"), "BBCode leaked into hover: {doc:?}");
}
}