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_enum_value_by_name: FxHashMap<String, (u32, u32)>,
79 pub(crate) global_consts: Vec<gdscript_layer::GlobalConst>,
81 pub(crate) gdscript_builtins: Vec<gdscript_layer::BuiltinFn>,
83 pub(crate) int_builtin: Option<BuiltinId>,
85 pub(crate) docs: Option<DocStore>,
89}
90
91impl EngineApi {
92 #[must_use]
95 pub fn from_data(data: ApiData) -> Self {
96 let mut class_by_name = FxHashMap::default();
97 for (i, c) in data.classes.iter().enumerate() {
98 class_by_name.insert(
99 c.name.clone(),
100 ClassId(u32::try_from(i).unwrap_or(u32::MAX)),
101 );
102 }
103 let mut builtin_by_name = FxHashMap::default();
104 for (i, b) in data.builtins.iter().enumerate() {
105 builtin_by_name.insert(
106 b.name.clone(),
107 BuiltinId(u32::try_from(i).unwrap_or(u32::MAX)),
108 );
109 }
110 let singleton_by_name = data
111 .singletons
112 .iter()
113 .map(|(name, id)| (name.clone(), *id))
114 .collect();
115 let mut utility_by_name = FxHashMap::default();
116 for (i, u) in data.utilities.iter().enumerate() {
117 utility_by_name.insert(u.name.clone(), u32::try_from(i).unwrap_or(u32::MAX));
118 }
119 let mut global_enum_by_name = FxHashMap::default();
120 let mut global_enum_value_by_name = FxHashMap::default();
121 for (i, e) in data.global_enums.iter().enumerate() {
122 let ei = u32::try_from(i).unwrap_or(u32::MAX);
123 global_enum_by_name.insert(e.name.clone(), ei);
124 for (j, v) in e.values.iter().enumerate() {
125 global_enum_value_by_name
126 .entry(v.name.clone())
127 .or_insert((ei, u32::try_from(j).unwrap_or(u32::MAX)));
128 }
129 }
130 let int_builtin = builtin_by_name.get("int").copied();
131
132 Self {
133 data,
134 class_by_name,
135 builtin_by_name,
136 singleton_by_name,
137 utility_by_name,
138 global_enum_by_name,
139 global_enum_value_by_name,
140 global_consts: gdscript_layer::global_consts(),
141 gdscript_builtins: gdscript_layer::builtin_fns(),
142 int_builtin,
143 docs: None,
144 }
145 }
146
147 pub fn set_docs(&mut self, docs: DocStore) {
149 self.docs = Some(docs);
150 }
151
152 #[must_use]
156 pub fn doc(&self, id: DocId) -> Option<&str> {
157 self.docs.as_ref()?.get(id)
158 }
159
160 pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
168 let mut aligned = rkyv::util::AlignedVec::<16>::new();
169 aligned.extend_from_slice(bytes);
170 let data = rkyv::from_bytes::<ApiData, rkyv::rancor::Error>(aligned.as_slice())
171 .map_err(|e| LoadError::Decode(e.to_string()))?;
172 Ok(Self::from_data(data))
173 }
174
175 #[must_use]
177 pub fn version(&self) -> &ApiVersion {
178 &self.data.version
179 }
180}
181
182impl DocStore {
183 pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
191 let mut aligned = rkyv::util::AlignedVec::<16>::new();
192 aligned.extend_from_slice(bytes);
193 rkyv::from_bytes::<Self, rkyv::rancor::Error>(aligned.as_slice())
194 .map_err(|e| LoadError::Decode(e.to_string()))
195 }
196}
197
198#[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
209#[must_use]
210pub fn bundled() -> &'static EngineApi {
211 use std::sync::OnceLock;
212 static BUNDLED: OnceLock<EngineApi> = OnceLock::new();
213 static BYTES: &[u8] = include_bytes!("engine_api.bin");
214 BUNDLED.get_or_init(|| {
215 let mut api =
216 EngineApi::from_bytes(BYTES).expect("the bundled engine-API blob must be valid");
217 #[cfg(feature = "bundled-docs")]
221 {
222 static DOC_BYTES: &[u8] = include_bytes!("engine_docs.bin");
223 if let Ok(docs) = DocStore::from_bytes(DOC_BYTES) {
224 api.set_docs(docs);
225 }
226 }
227 api
228 })
229}
230
231#[cfg(test)]
232mod tests {
233 #[test]
234 fn generated_metadata_is_present() {
235 assert!(!crate::generated::GODOT_VERSION.is_empty());
237 }
238
239 #[cfg(all(feature = "bundled-api", not(target_arch = "wasm32")))]
241 #[test]
242 fn bundled_blob_loads_and_resolves_golden_symbols() {
243 let api = crate::bundled();
244
245 let expected: Vec<u32> = crate::generated::GODOT_VERSION
248 .split(['.', '-'])
249 .take(2)
250 .filter_map(|p| p.parse().ok())
251 .collect();
252 assert_eq!(api.version().major, expected[0]);
253 assert_eq!(api.version().minor, expected[1]);
254
255 let node = api.class_by_name("Node").expect("Node class present");
257 let node2d = api.class_by_name("Node2D").expect("Node2D class present");
258 assert!(api.lookup_member(node, "add_child").is_some());
259 assert!(api.is_subclass(node2d, node), "Node2D is a Node");
260 assert!(
261 api.lookup_member(node2d, "add_child").is_some(),
262 "add_child is inherited onto Node2D"
263 );
264
265 let members = api.members_of(node2d);
267 assert!(members.iter().any(|m| m.name() == "add_child"));
268 assert!(members.iter().any(|m| m.name() == "position"));
269
270 assert!(api.singleton("Input").is_some());
272 let v2 = api
273 .builtin_by_name("Vector2")
274 .expect("Vector2 builtin present");
275 assert!(api.builtin_member(v2, "x").is_some());
276 assert!(api.builtin_operators(v2).iter().any(|o| o.op == "+"));
277 let process_mode = api
278 .class(node)
279 .properties
280 .iter()
281 .find(|p| p.name == "process_mode")
282 .expect("Node.process_mode present");
283 assert!(
284 process_mode.enum_of.is_some(),
285 "process_mode is recovered as enum-typed from its getter"
286 );
287
288 assert!(api.global_const("PI").is_some());
290 assert!(api.gdscript_builtin("preload").is_some());
291 }
292
293 #[cfg(all(feature = "bundled-docs", not(target_arch = "wasm32")))]
295 #[test]
296 fn bundled_docs_resolve_and_are_converted() {
297 use crate::MemberRef;
298 let api = crate::bundled();
299 let node = api.class_by_name("Node").expect("Node class");
300
301 assert!(
303 api.class(node).doc.and_then(|id| api.doc(id)).is_some(),
304 "Node has a class hover doc"
305 );
306 let MemberRef::Method(m) = api.lookup_member(node, "add_child").expect("add_child") else {
307 panic!("add_child is a method");
308 };
309 let doc = m
310 .doc
311 .and_then(|id| api.doc(id))
312 .expect("add_child hover doc");
313 assert!(!doc.contains("[/"), "BBCode leaked into hover: {doc:?}");
315 }
316}