mod cache;
mod config;
mod context;
mod discovery;
mod error;
pub use cache::{CURRENT_VERSION, CacheManager};
pub use config::{
LoaderConfig, LoggingConfig, LuaConfig, PastaConfig, PersistenceConfig, TalkConfig,
default_libs, default_log_file_path, default_lua_search_paths,
};
pub use context::LoaderContext;
pub use error::{LoaderError, TranspileFailure};
use crate::context::TranspileContext;
use crate::runtime::{PastaLuaRuntime, RuntimeConfig};
use crate::transpiler::LuaTranspiler;
use std::fs;
use std::path::Path;
use tracing::{debug, error, info, warn};
struct ProcessStats {
transpiled: usize,
skipped: usize,
failed: usize,
copied: usize,
}
pub struct PastaLoader;
impl PastaLoader {
pub fn load(base_dir: impl AsRef<Path>) -> Result<PastaLuaRuntime, LoaderError> {
Self::load_with_config(base_dir, RuntimeConfig::new())
}
pub fn load_with_config(
base_dir: impl AsRef<Path>,
runtime_config: RuntimeConfig,
) -> Result<PastaLuaRuntime, LoaderError> {
let base_dir = base_dir.as_ref();
if !base_dir.exists() {
return Err(LoaderError::DirectoryNotFound(base_dir.to_path_buf()));
}
info!(path = %base_dir.display(), "Starting pasta loader");
debug!("Phase 1: Loading configuration");
let config = PastaConfig::load(base_dir)?;
debug!("Stage 1.5: Applying logging configuration");
let logger = Self::create_and_register_logger(base_dir, &config)?;
debug!("Phase 2: Preparing directories and cache");
Self::prepare_directories(base_dir, &config.loader)?;
let cache_manager =
CacheManager::new(base_dir.to_path_buf(), &config.loader.transpiled_output_dir);
cache_manager.prepare_cache_dir()?;
debug!("Phase 3: Discovering pasta and lua files");
let (pasta_files, lua_files) =
Self::discover_all_files(base_dir, &config.loader.pasta_patterns)?;
let total_files = pasta_files.len() + lua_files.len();
if total_files == 0 {
warn!(path = %base_dir.display(), "No .pasta or .lua files found");
} else {
info!(
pasta = pasta_files.len(),
lua = lua_files.len(),
"Found files"
);
}
debug!("Phase 4: Incremental processing");
let (context, module_names, stats) =
Self::process_incremental(base_dir, &pasta_files, &lua_files, &cache_manager)?;
if config.loader.debug_mode {
info!(
transpiled = stats.transpiled,
skipped = stats.skipped,
failed = stats.failed,
copied = stats.copied,
"Processing statistics"
);
}
let all_source_files: Vec<_> = pasta_files
.iter()
.chain(lua_files.iter())
.cloned()
.collect();
let orphans = cache_manager.find_orphaned_caches(&all_source_files);
if !orphans.is_empty() && config.loader.debug_mode {
for orphan in &orphans {
warn!(path = %orphan.display(), "Orphaned cache file detected");
}
}
debug!("Phase 5: Generating scene_dic.lua");
let scene_dic_path = cache_manager.generate_scene_dic(&module_names)?;
debug!("Phase 6: Initializing runtime");
let loader_context = LoaderContext::from_config(base_dir, &config);
let runtime = PastaLuaRuntime::from_loader_with_scene_dic(
context,
loader_context,
runtime_config,
Some(config),
logger,
&scene_dic_path,
)?;
info!(path = %base_dir.display(), "Startup sequence completed");
Ok(runtime)
}
fn create_and_register_logger(
base_dir: &Path,
config: &PastaConfig,
) -> Result<Option<std::sync::Arc<crate::logging::PastaLogger>>, LoaderError> {
let logging_config = config.logging();
match crate::logging::PastaLogger::new(base_dir, logging_config.as_ref()) {
Ok(logger) => {
let logger = std::sync::Arc::new(logger);
info!(path = %logger.log_path().display(), "Created instance logger");
crate::logging::GlobalLoggerRegistry::instance()
.register(base_dir.to_path_buf(), logger.clone());
if let Some(ref cfg) = logging_config {
crate::logging::update_tracing_filter(cfg);
}
Ok(Some(logger))
}
Err(e) => {
warn!(error = %e, "Failed to create instance logger, logging disabled");
Ok(None)
}
}
}
fn prepare_directories(base_dir: &Path, config: &LoaderConfig) -> Result<(), LoaderError> {
let dirs = [
"profile/pasta/save",
"profile/pasta/save/lua",
"profile/pasta/cache",
&config.transpiled_output_dir,
];
for dir in &dirs {
let path = base_dir.join(dir);
if !path.exists() {
fs::create_dir_all(&path).map_err(|e| LoaderError::io(&path, e))?;
debug!(path = %path.display(), "Created directory");
}
}
Ok(())
}
fn discover_all_files(
base_dir: &Path,
pasta_patterns: &[String],
) -> Result<(Vec<std::path::PathBuf>, Vec<std::path::PathBuf>), LoaderError> {
let pasta_files = discovery::discover_files(base_dir, pasta_patterns)?;
let lua_patterns: Vec<String> = pasta_patterns
.iter()
.filter_map(|p| {
if let Some(stem) = p.strip_suffix(".pasta") {
Some(format!("{}.lua", stem))
} else {
warn!(pattern = %p, "Cannot convert pattern to .lua, skipping");
None
}
})
.collect();
let lua_files = if lua_patterns.is_empty() {
Vec::new()
} else {
discovery::discover_files(base_dir, &lua_patterns).unwrap_or_else(|e| {
warn!(error = %e, "Failed to discover .lua files, skipping");
Vec::new()
})
};
for file in pasta_files.iter().chain(lua_files.iter()) {
if let Some(file_name) = file.file_name().and_then(|n| n.to_str())
&& (file_name == "init.lua" || file_name == "init.pasta")
{
return Err(LoaderError::invalid_file_name(file));
}
}
let pasta_module_names: std::collections::HashSet<String> = pasta_files
.iter()
.map(|f| {
let relative = f
.strip_prefix(base_dir)
.unwrap_or(f)
.to_string_lossy()
.to_string();
let without_prefix = relative
.strip_prefix("dic")
.unwrap_or(&relative)
.trim_start_matches(['/', '\\'])
.to_string();
let stem = std::path::Path::new(&without_prefix)
.with_extension("")
.to_string_lossy()
.to_string();
stem.replace(['/', '\\'], ".").replace('-', "_")
})
.collect();
let mut filtered_lua_files = Vec::new();
for lua_file in &lua_files {
let relative = lua_file
.strip_prefix(base_dir)
.unwrap_or(lua_file)
.to_string_lossy()
.to_string();
let without_prefix = relative
.strip_prefix("dic")
.unwrap_or(&relative)
.trim_start_matches(['/', '\\'])
.to_string();
let stem = std::path::Path::new(&without_prefix)
.with_extension("")
.to_string_lossy()
.to_string();
let module_key = stem.replace(['/', '\\'], ".").replace('-', "_");
if pasta_module_names.contains(&module_key) {
warn!(
lua_file = %lua_file.display(),
module_name = %format!("pasta.scene.{}", module_key),
"Module name conflict: .pasta file takes priority, .lua file ignored"
);
} else {
filtered_lua_files.push(lua_file.clone());
}
}
Ok((pasta_files, filtered_lua_files))
}
fn process_incremental(
_base_dir: &Path,
pasta_files: &[std::path::PathBuf],
lua_files: &[std::path::PathBuf],
cache_manager: &CacheManager,
) -> Result<(TranspileContext, Vec<String>, ProcessStats), LoaderError> {
let transpiler = LuaTranspiler::default();
let mut combined_context = TranspileContext::new();
let total_count = pasta_files.len() + lua_files.len();
let mut module_names = Vec::with_capacity(total_count);
let mut failures = Vec::new();
let mut stats = ProcessStats {
transpiled: 0,
skipped: 0,
failed: 0,
copied: 0,
};
for file_path in pasta_files {
let needs_transpile = cache_manager.needs_transpile(file_path).unwrap_or(true);
let module_name = cache_manager.source_to_module_name(file_path);
module_names.push(module_name.clone());
if !needs_transpile {
stats.skipped += 1;
debug!(file = %file_path.display(), "Skipped (cache up-to-date)");
continue;
}
let content = match fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Read error: {}", e),
});
stats.failed += 1;
continue;
}
};
let filename = file_path.to_string_lossy().to_string();
let pasta_file = match pasta_dsl::parse_str(&content, &filename) {
Ok(pf) => pf,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Parse error: {}", e),
});
stats.failed += 1;
continue;
}
};
let mut output = Vec::new();
let file_context = match transpiler.transpile(&pasta_file, &mut output) {
Ok(ctx) => ctx,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Transpile error: {}", e),
});
stats.failed += 1;
continue;
}
};
combined_context.merge_from(file_context);
let lua_code = match String::from_utf8(output) {
Ok(s) => s,
Err(e) => {
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("UTF-8 error: {}", e),
});
stats.failed += 1;
continue;
}
};
if let Err(e) = cache_manager.save_cache(file_path, &lua_code) {
warn!(file = %file_path.display(), error = %e, "Failed to save cache");
}
stats.transpiled += 1;
debug!(file = %file_path.display(), module = %module_name, "Transpiled");
}
for file_path in lua_files {
let needs_copy = cache_manager.needs_transpile(file_path).unwrap_or(true);
let module_name = cache_manager.source_to_module_name(file_path);
module_names.push(module_name.clone());
if !needs_copy {
stats.skipped += 1;
debug!(file = %file_path.display(), "Skipped .lua (cache up-to-date)");
continue;
}
let content = match fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
error!(
file = %file_path.display(),
error = %e,
"Failed to read .lua file"
);
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Read error: {}", e),
});
stats.failed += 1;
continue;
}
};
if let Err(e) = cache_manager.save_cache(file_path, &content) {
error!(
file = %file_path.display(),
error = %e,
"Failed to copy .lua file to cache"
);
failures.push(TranspileFailure {
source_path: file_path.clone(),
error: format!("Cache write error: {}", e),
});
stats.failed += 1;
continue;
}
stats.copied += 1;
debug!(file = %file_path.display(), module = %module_name, "Copied .lua");
}
if !failures.is_empty() {
for failure in &failures {
error!(
path = %failure.source_path.display(),
error = %failure.error,
"Process failure"
);
}
let succeeded = stats.transpiled + stats.skipped + stats.copied;
return Err(LoaderError::partial_transpile(
succeeded,
stats.failed,
failures,
));
}
Ok((combined_context, module_names, stats))
}
}
#[derive(Debug, Clone)]
pub struct TranspileResult {
pub module_name: String,
pub lua_code: String,
pub source_path: std::path::PathBuf,
}