1#![cfg_attr(docsrs, feature(doc_cfg))]
23
24pub mod gdscript_layer;
25pub mod generated;
27pub mod lookup;
28pub mod model;
29
30use rustc_hash::FxHashMap;
31
32pub use lookup::MemberRef;
33pub use model::{
34 ApiData, ApiType, ApiVersion, BuiltinData, BuiltinId, BuiltinMember, ClassData, ClassId,
35 ConstInfo, DocId, DocStore, ElemRef, EnumInfo, EnumValue, MethodSig, OperatorSig, Param,
36 PropertyInfo, SignalSig, TyRef, UtilityFn,
37};
38
39#[must_use]
41pub fn godot_version() -> &'static str {
42 generated::GODOT_VERSION
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum LoadError {
48 Decode(String),
50}
51
52impl std::fmt::Display for LoadError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::Decode(msg) => write!(f, "failed to decode engine-API blob: {msg}"),
56 }
57 }
58}
59
60impl std::error::Error for LoadError {}
61
62#[derive(Debug)]
68pub struct EngineApi {
69 pub(crate) data: ApiData,
70 pub(crate) class_by_name: FxHashMap<String, ClassId>,
71 pub(crate) builtin_by_name: FxHashMap<String, BuiltinId>,
72 pub(crate) singleton_by_name: FxHashMap<String, ClassId>,
73 pub(crate) utility_by_name: FxHashMap<String, u32>,
74 pub(crate) global_enum_by_name: FxHashMap<String, u32>,
75 pub(crate) global_consts: Vec<gdscript_layer::GlobalConst>,
77 pub(crate) gdscript_builtins: Vec<gdscript_layer::BuiltinFn>,
79 pub(crate) int_builtin: Option<BuiltinId>,
81 pub(crate) docs: Option<DocStore>,
85}
86
87impl EngineApi {
88 #[must_use]
91 pub fn from_data(data: ApiData) -> Self {
92 let mut class_by_name = FxHashMap::default();
93 for (i, c) in data.classes.iter().enumerate() {
94 class_by_name.insert(
95 c.name.clone(),
96 ClassId(u32::try_from(i).unwrap_or(u32::MAX)),
97 );
98 }
99 let mut builtin_by_name = FxHashMap::default();
100 for (i, b) in data.builtins.iter().enumerate() {
101 builtin_by_name.insert(
102 b.name.clone(),
103 BuiltinId(u32::try_from(i).unwrap_or(u32::MAX)),
104 );
105 }
106 let singleton_by_name = data
107 .singletons
108 .iter()
109 .map(|(name, id)| (name.clone(), *id))
110 .collect();
111 let mut utility_by_name = FxHashMap::default();
112 for (i, u) in data.utilities.iter().enumerate() {
113 utility_by_name.insert(u.name.clone(), u32::try_from(i).unwrap_or(u32::MAX));
114 }
115 let mut global_enum_by_name = FxHashMap::default();
116 for (i, e) in data.global_enums.iter().enumerate() {
117 global_enum_by_name.insert(e.name.clone(), u32::try_from(i).unwrap_or(u32::MAX));
118 }
119 let int_builtin = builtin_by_name.get("int").copied();
120
121 Self {
122 data,
123 class_by_name,
124 builtin_by_name,
125 singleton_by_name,
126 utility_by_name,
127 global_enum_by_name,
128 global_consts: gdscript_layer::global_consts(),
129 gdscript_builtins: gdscript_layer::builtin_fns(),
130 int_builtin,
131 docs: None,
132 }
133 }
134
135 pub fn set_docs(&mut self, docs: DocStore) {
137 self.docs = Some(docs);
138 }
139
140 #[must_use]
144 pub fn doc(&self, id: DocId) -> Option<&str> {
145 self.docs.as_ref()?.get(id)
146 }
147
148 pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
156 let mut aligned = rkyv::util::AlignedVec::<16>::new();
157 aligned.extend_from_slice(bytes);
158 let data = rkyv::from_bytes::<ApiData, rkyv::rancor::Error>(aligned.as_slice())
159 .map_err(|e| LoadError::Decode(e.to_string()))?;
160 Ok(Self::from_data(data))
161 }
162
163 #[must_use]
165 pub fn version(&self) -> &ApiVersion {
166 &self.data.version
167 }
168}
169
170impl DocStore {
171 pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
179 let mut aligned = rkyv::util::AlignedVec::<16>::new();
180 aligned.extend_from_slice(bytes);
181 rkyv::from_bytes::<Self, rkyv::rancor::Error>(aligned.as_slice())
182 .map_err(|e| LoadError::Decode(e.to_string()))
183 }
184}
185
186#[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
197#[must_use]
198pub fn bundled() -> &'static EngineApi {
199 use std::sync::OnceLock;
200 static BUNDLED: OnceLock<EngineApi> = OnceLock::new();
201 static BYTES: &[u8] = include_bytes!("engine_api.bin");
202 BUNDLED.get_or_init(|| {
203 let mut api =
204 EngineApi::from_bytes(BYTES).expect("the bundled engine-API blob must be valid");
205 #[cfg(feature = "bundled-docs")]
209 {
210 static DOC_BYTES: &[u8] = include_bytes!("engine_docs.bin");
211 if let Ok(docs) = DocStore::from_bytes(DOC_BYTES) {
212 api.set_docs(docs);
213 }
214 }
215 api
216 })
217}
218
219#[cfg(test)]
220mod tests {
221 #[test]
222 fn generated_metadata_is_present() {
223 assert!(!crate::generated::GODOT_VERSION.is_empty());
225 }
226
227 #[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
229 #[test]
230 fn bundled_blob_loads_and_resolves_golden_symbols() {
231 let api = crate::bundled();
232
233 assert_eq!(api.version().major, 4);
235 assert_eq!(api.version().minor, 5);
236
237 let node = api.class_by_name("Node").expect("Node class present");
239 let node2d = api.class_by_name("Node2D").expect("Node2D class present");
240 assert!(api.lookup_member(node, "add_child").is_some());
241 assert!(api.is_subclass(node2d, node), "Node2D is a Node");
242 assert!(
243 api.lookup_member(node2d, "add_child").is_some(),
244 "add_child is inherited onto Node2D"
245 );
246
247 let members = api.members_of(node2d);
249 assert!(members.iter().any(|m| m.name() == "add_child"));
250 assert!(members.iter().any(|m| m.name() == "position"));
251
252 assert!(api.singleton("Input").is_some());
254 let v2 = api
255 .builtin_by_name("Vector2")
256 .expect("Vector2 builtin present");
257 assert!(api.builtin_member(v2, "x").is_some());
258 assert!(api.builtin_operators(v2).iter().any(|o| o.op == "+"));
259 let process_mode = api
260 .class(node)
261 .properties
262 .iter()
263 .find(|p| p.name == "process_mode")
264 .expect("Node.process_mode present");
265 assert!(
266 process_mode.enum_of.is_some(),
267 "process_mode is recovered as enum-typed from its getter"
268 );
269
270 assert!(api.global_const("PI").is_some());
272 assert!(api.gdscript_builtin("preload").is_some());
273 }
274
275 #[cfg(all(feature = "bundled-docs", not(target_arch = "wasm32")))]
277 #[test]
278 fn bundled_docs_resolve_and_are_converted() {
279 use crate::MemberRef;
280 let api = crate::bundled();
281 let node = api.class_by_name("Node").expect("Node class");
282
283 assert!(
285 api.class(node).doc.and_then(|id| api.doc(id)).is_some(),
286 "Node has a class hover doc"
287 );
288 let MemberRef::Method(m) = api.lookup_member(node, "add_child").expect("add_child") else {
289 panic!("add_child is a method");
290 };
291 let doc = m
292 .doc
293 .and_then(|id| api.doc(id))
294 .expect("add_child hover doc");
295 assert!(!doc.contains("[/"), "BBCode leaked into hover: {doc:?}");
297 }
298}