feat(assets): implement virtual AssetManager with non-destructive mod overlays
This commit is contained in:
parent
f6e7bbd7a5
commit
1969f7aae9
3 changed files with 87 additions and 0 deletions
71
crates/molotov-core/src/assets/manager.rs
Normal file
71
crates/molotov-core/src/assets/manager.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
use super::asset::AssetFile;
|
||||||
|
use super::dir::AssetDir;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct AssetManager {
|
||||||
|
base_assets: HashMap<String, Vec<u8>>,
|
||||||
|
mod_overrides: HashMap<String, Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AssetManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
base_assets: HashMap::new(),
|
||||||
|
mod_overrides: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_asset(&mut self, path: &str, data: Vec<u8>) {
|
||||||
|
self.base_assets.insert(path.to_string(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_mod_override(&mut self, path: &str, data: Vec<u8>) {
|
||||||
|
self.mod_overrides.insert(path.to_string(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(&self, filename: &str) -> Option<AssetFile> {
|
||||||
|
// 1. Check user mod overrides first (non-destructive mod engine)
|
||||||
|
if let Some(data) = self.mod_overrides.get(filename) {
|
||||||
|
tracing::info!("AssetManager: redirected '{}' to active user mod", filename);
|
||||||
|
return Some(AssetFile::new(data.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to base asset
|
||||||
|
if let Some(data) = self.base_assets.get(filename) {
|
||||||
|
return Some(AssetFile::new(data.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::warn!("AssetManager: asset '{}' not found", filename);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_dir(&self, dir_name: &str) -> AssetDir {
|
||||||
|
let prefix = if dir_name.is_empty() || dir_name == "." {
|
||||||
|
"".to_string()
|
||||||
|
} else if !dir_name.ends_with('/') {
|
||||||
|
format!("{}/", dir_name)
|
||||||
|
} else {
|
||||||
|
dir_name.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut files = Vec::new();
|
||||||
|
for key in self.base_assets.keys().chain(self.mod_overrides.keys()) {
|
||||||
|
if key.starts_with(&prefix) {
|
||||||
|
let rest = &key[prefix.len()..];
|
||||||
|
if !rest.contains('/') {
|
||||||
|
files.push(rest.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.sort();
|
||||||
|
files.dedup();
|
||||||
|
AssetDir::new(files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
pub mod asset;
|
pub mod asset;
|
||||||
pub mod dir;
|
pub mod dir;
|
||||||
|
pub mod manager;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
|
||||||
15
crates/molotov-core/tests/asset_manager_virtual_test.rs
Normal file
15
crates/molotov-core/tests/asset_manager_virtual_test.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
use molotov_core::assets::manager::AssetManager;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_asset_manager_open_with_mod_override() {
|
||||||
|
let mut mgr = AssetManager::new();
|
||||||
|
mgr.add_asset("content/sounds/ouch.ogg", b"BASE_SOUND".to_vec());
|
||||||
|
mgr.add_mod_override("content/sounds/ouch.ogg", b"CUSTOM_MOD_SOUND".to_vec());
|
||||||
|
|
||||||
|
let mut asset = mgr.open("content/sounds/ouch.ogg").expect("Should open asset");
|
||||||
|
let mut content = vec![0u8; asset.length() as usize];
|
||||||
|
asset.read(&mut content);
|
||||||
|
|
||||||
|
assert_eq!(&content, b"CUSTOM_MOD_SOUND", "Must redirect to user mod bytes");
|
||||||
|
}
|
||||||
Reference in a new issue