Skip to main content

bevy_dlc/
macros.rs

1//! A collection of helper macros to reduce boilerplate when working with DLC packs
2//! and assets. These are primarily convenience utilities used by tests and examples
3//! (and available to library users) for quickly defining common patterns.
4//!
5//! The macros are all exported at the crate root so you can invoke them as
6//! `bevy_dlc::pack_items!()`, `bevy_dlc::dlc_register_types!()`, etc.
7
8#[doc(hidden)]
9pub fn __decode_embedded_signed_license_aes(encrypted_b64: &str, key: &str) -> crate::SignedLicense {
10    use base64::Engine as _;
11
12    let value = base64::prelude::BASE64_STANDARD
13        .decode(encrypted_b64.as_bytes())
14        .expect("invalid embedded base64 in signed license");
15    let cryptor = byte_aes::Aes256Cryptor::try_from(key)
16        .expect("invalid AES key for signed license");
17    let decoded_data = cryptor
18        .decrypt(value)
19        .expect("failed to decrypt embedded signed license");
20
21    // String::from_utf8 moves decoded_data without cloning — same heap
22    // allocation, now owned by SignedLicense, which zeroes it on drop
23    // via dynamic_alias!.
24    crate::SignedLicense::from(
25        String::from_utf8(decoded_data)
26            .expect("embedded signed license is not valid UTF-8"),
27    )
28}
29
30/// format a byte count into a human-readable string (KB/MB/GB)
31///
32/// The macro returns a `String`; the formatting matches the previous
33/// `human_bytes` helper, using two decimal places of precision for units
34/// greater than bytes.
35///
36/// Example usage (hidden from docs):
37/// ```ignore
38/// use bevy_dlc::human_bytes;
39/// let s = human_bytes!(123456);
40/// ```
41#[macro_export]
42#[doc(hidden)]
43macro_rules! human_bytes {
44    ($bytes:expr) => {{
45        let bytes: u64 = $bytes as u64;
46        const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
47        let mut size = bytes as f64;
48        let mut unit = 0;
49        while size >= 1024.0 && unit < UNITS.len() - 1 {
50            size /= 1024.0;
51            unit += 1;
52        }
53        if unit == 0 {
54            format!("{} {}", size as u64, UNITS[unit])
55        } else {
56            format!("{:.2} {}", size, UNITS[unit])
57        }
58    }};
59}
60
61/// Builds a `Vec<PackItem>` from a list of entries.  Each entry specifies a
62/// path and its raw contents; optional query-style suffixes allow specifying an
63/// explicit original extension or a type path as well.
64///
65/// Examples:
66///
67/// ```ignore
68/// use bevy_dlc::pack_items;
69///
70/// let items = pack_items![
71///     "note.txt" => b"hello",
72///     // override the extension that would otherwise be inferred
73///     "other" => b"data"; ext="bin",
74///     // supply a type path to help the runtime choose a loader (optional)
75///     "model" => b"objdata"; type="bevy::gltf::Gltf",
76/// ];
77/// ```
78#[macro_export]
79macro_rules! pack_items {
80    (
81        $(
82            $path:expr => $bytes:expr
83            $(; ext=$ext:expr)?
84            $(; type=$type_path:expr)?
85        ),* $(,)?
86    ) => {{
87        let mut v = Vec::new();
88        $(
89            let mut item = $crate::PackItem::new($path, $bytes).expect("forbidden extension or invalid dlcpack content");
90            $( item = item.with_extension($ext).expect("forbidden extension"); )?
91            $( item = item.with_type_path($type_path); )?
92            v.push(item);
93        )*
94        v
95    }};
96}
97
98/// Calls `App::register_dlc_type::<T>()` for each given type on the provided
99/// Bevy `App` instance.
100///
101/// Useful when registering several DLC-supported asset types in one line:
102///
103/// ```ignore
104/// use bevy::prelude::*;
105/// use bevy_dlc::prelude::*;
106///
107/// let mut app = App::new();
108/// // an `AssetPlugin` (or AssetLoaders for the asset types) must be added before registering
109/// // asset types; this mirrors real usage inside a Bevy application.
110/// app.add_plugins(AssetPlugin::default());
111/// // bring the `AppExt` trait into scope so the generated calls resolve
112/// dlc_register_types!(app, Image, Mesh);
113/// ```
114#[macro_export]
115macro_rules! dlc_register_types {
116    ($app:expr, $($ty:ty),* $(,)?) => {{
117        $(
118            $app.register_dlc_type::<$ty>();
119        )*
120    }};
121}
122
123/// Defines a simple "text-like" asset type along with a loader and a Bevy
124/// plugin for it.  The generated loader reads the file as UTF-8 and stores a
125/// `String` in the asset.  The plugin registers the loader and the DLC type.
126///
127/// This is the same pattern used throughout the repository for example assets
128/// (see `examples/mod.rs`) and in the test helpers.  Rather than copy/paste the
129/// struct/loader/plugin boilerplate each time, this macro generates it for you.
130///
131/// The macro takes explicit identifiers for the asset struct, its loader, and
132/// the plugin; this avoids any need for identifier concatenation in
133/// `macro_rules!`.
134///
135/// Parameters:
136/// * `$name`: identifier for the new asset struct.
137/// * `$loader`: identifier for the asset/loader type.
138/// * `$plugin`: identifier for the plugin struct.
139/// * `$($ext:expr),+`: one or more file extensions the loader will recognise.
140///
141/// Example:
142///
143/// ```ignore
144/// // demonstration only; we don't actually run a Bevy app inside the doctest
145/// use bevy::prelude::AssetApp;
146/// use bevy_dlc::dlc_simple_asset;
147///
148/// dlc_simple_asset!(TextAsset, TextAssetLoader, TextAssetPlugin, "txt", "text");
149/// ```
150#[macro_export]
151macro_rules! dlc_simple_asset {
152    ($name:ident, $loader:ident, $plugin:ident, $($ext:expr),+ $(,)?) => {
153        #[derive(bevy::asset::Asset, bevy::reflect::Reflect, serde::Serialize, serde::Deserialize)]
154        pub struct $name(pub String);
155
156        #[derive(Default, bevy::reflect::Reflect)]
157        pub struct $loader;
158
159        impl bevy::asset::AssetLoader for $loader {
160            type Asset = $name;
161            type Settings = ();
162            type Error = std::io::Error;
163
164            async fn load(
165                &self,
166                reader: &mut dyn bevy::asset::io::Reader,
167                _settings: &(),
168                _load_context: &mut bevy::asset::LoadContext<'_>,
169            ) -> Result<Self::Asset, Self::Error> {
170                let mut bytes = Vec::new();
171                reader.read_to_end(&mut bytes).await?;
172                let s = String::from_utf8(bytes)
173                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
174                Ok($name(s))
175            }
176
177            fn extensions(&self) -> &[&str] {
178                static EXTS: &[&str] = &[$($ext),+];
179                EXTS
180            }
181        }
182
183        pub struct $plugin;
184
185        impl bevy::prelude::Plugin for $plugin {
186            fn build(&self, app: &mut bevy::prelude::App) {
187                app.init_asset_loader::<$loader>();
188                // call via fully qualified path so the `AppExt` trait need not be
189                // imported by the macro user.
190                // use `$crate` to refer to the current crate from derived code.  `AppExt`
191                // is re-exported at the crate root, so we do not need to touch the
192                // private `ext` module here.
193                $crate::AppExt::register_dlc_type::<$name>(app);
194            }
195        }
196
197        impl Default for $plugin {
198            fn default() -> Self {
199                $plugin
200            }
201        }
202    };
203}
204
205/// Includes a secure license token and a public key from files.
206///
207/// Returns a `(DlcKey, SignedLicense)` tuple.
208#[macro_export]
209macro_rules! include_dlc_key_and_license_aes {
210    ($pubkey_path:expr, $license_path:expr, $license_key:expr $(,)?) => {{
211        let signed_license = $crate::include_signed_license_aes!($license_path, $license_key);
212        let dlc_key = $crate::DlcKey::public(include_str!($pubkey_path))
213            .expect("invalid dlc pubkey");
214        (dlc_key, signed_license)
215    }};
216}
217