use std::any::TypeId;
use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::HashMap;
use std::fmt;
use std::hash::Hash;
use godot_ffi as sys;
use sys::Global;
use crate::builtin::*;
use crate::obj::GodotClass;
static CLASS_ID_CACHE: Global<ClassIdCache> = Global::new(ClassIdCache::new);
pub unsafe fn cleanup() {
CLASS_ID_CACHE.lock().clear();
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ClassId {
global_index: u16,
}
impl ClassId {
pub fn new_cached<T: GodotClass>(init_fn: impl FnOnce() -> String) -> Self {
Self::new_cached_inner::<T>(init_fn)
}
fn new_cached_inner<T: 'static>(init_fn: impl FnOnce() -> String) -> ClassId {
let type_id = TypeId::of::<T>();
let mut cache = CLASS_ID_CACHE.lock();
if let Some(global_index) = cache.get_by_type_id(type_id) {
return ClassId { global_index };
}
let name = init_fn();
#[cfg(before_api = "4.4")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.4")))]
assert!(
name.is_ascii(),
"In Godot < 4.4, class name must be ASCII: '{name}'"
);
cache.insert_class_id(Cow::Owned(name), Some(type_id), false)
}
pub fn new_dynamic(class_name: impl Into<CowStr>) -> Self {
let mut cache = CLASS_ID_CACHE.lock();
cache.insert_class_id(class_name.into(), None, false)
}
#[cfg(feature = "trace")] #[doc(hidden)]
pub fn __cached<T: 'static>(init_fn: impl FnOnce() -> String) -> Self {
Self::new_cached_inner::<T>(init_fn)
}
#[cfg(feature = "trace")] #[doc(hidden)]
pub fn __dynamic(class_name: &str) -> Self {
Self::new_dynamic(class_name.to_string())
}
pub fn none() -> Self {
Self { global_index: 0 }
}
#[doc(hidden)]
pub fn __alloc_next_unicode(class_name_str: &'static str) -> Self {
#[cfg(before_api = "4.4")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.4")))]
assert!(
class_name_str.is_ascii(),
"Before Godot 4.4, class names must be ASCII, but '{class_name_str}' is not.\nSee https://github.com/godotengine/godot/pull/96501."
);
let source = Cow::Borrowed(class_name_str);
let mut cache = CLASS_ID_CACHE.lock();
cache.insert_class_id(source, None, true)
}
#[doc(hidden)]
pub fn is_none(&self) -> bool {
self.global_index == 0
}
pub fn to_gstring(&self) -> GString {
self.with_string_name(|s| s.into())
}
pub fn to_string_name(&self) -> StringName {
self.with_string_name(|s| s.clone())
}
pub fn to_cow_str(&self) -> CowStr {
let cache = CLASS_ID_CACHE.lock();
let entry = cache.get_entry(self.global_index as usize);
entry.rust_str.clone()
}
#[doc(hidden)]
pub fn string_sys(&self) -> sys::GDExtensionConstStringNamePtr {
self.with_string_name(|s| s.string_sys())
}
fn with_string_name<R>(&self, func: impl FnOnce(&StringName) -> R) -> R {
let cache = CLASS_ID_CACHE.lock();
let entry = cache.get_entry(self.global_index as usize);
let string_name = entry
.godot_str
.get_or_init(|| StringName::from(entry.rust_str.as_ref()));
func(string_name)
}
}
impl fmt::Display for ClassId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.to_cow_str().fmt(f)
}
}
impl fmt::Debug for ClassId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = self.to_cow_str();
if name.is_empty() {
write!(f, "ClassId(none)")
} else {
write!(f, "ClassId({:?})", name)
}
}
}
struct ClassIdEntry {
rust_str: CowStr,
godot_str: OnceCell<StringName>,
}
impl ClassIdEntry {
const fn new(rust_str: CowStr) -> Self {
Self {
rust_str,
godot_str: OnceCell::new(),
}
}
fn none() -> Self {
Self::new(Cow::Borrowed(""))
}
}
struct ClassIdCache {
entries: Vec<ClassIdEntry>,
type_to_index: HashMap<TypeId, u16>,
string_to_index: HashMap<String, u16>,
}
impl ClassIdCache {
fn new() -> Self {
let mut string_to_index = HashMap::new();
string_to_index.insert(String::new(), 0);
Self {
entries: vec![ClassIdEntry::none()],
type_to_index: HashMap::new(),
string_to_index,
}
}
fn insert_class_id(
&mut self,
source: CowStr,
type_id: Option<TypeId>,
expect_first: bool,
) -> ClassId {
if expect_first {
if !sys::is_initialized()
&& let Some(&existing_index) = self.string_to_index.get(source.as_ref())
{
return ClassId {
global_index: existing_index,
};
}
sys::balanced_assert!(
!self.string_to_index.contains_key(source.as_ref()),
"insert_class_name() called for already-existing string: {}",
source
);
} else {
if let Some(&existing_index) = self.string_to_index.get(source.as_ref()) {
if let Some(type_id) = type_id {
self.type_to_index.entry(type_id).or_insert(existing_index);
}
return ClassId {
global_index: existing_index,
};
}
}
let global_index =
self.entries.len().try_into().unwrap_or_else(|_| {
panic!("ClassId cache exceeded maximum capacity of 65536 entries")
});
self.entries.push(ClassIdEntry::new(source.clone()));
self.string_to_index
.insert(source.into_owned(), global_index);
if let Some(type_id) = type_id {
self.type_to_index.insert(type_id, global_index);
}
ClassId { global_index }
}
fn get_by_type_id(&self, type_id: TypeId) -> Option<u16> {
self.type_to_index.get(&type_id).copied()
}
fn get_entry(&self, index: usize) -> &ClassIdEntry {
&self.entries[index]
}
fn clear(&mut self) {
for entry in &mut self.entries {
entry.godot_str = OnceCell::new();
}
self.type_to_index.clear();
}
}