use mlua::{Lua, Result as LuaResult};
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PobError {
#[error("Lua error: {0}")]
Lua(#[from] mlua::Error),
#[error("PoB not initialized")]
NotInitialized,
#[error("Invalid build code: {0}")]
InvalidBuildCode(String),
#[error("Calculation failed: {0}")]
CalculationFailed(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub struct PobHeadless {
lua: Lua,
initialized: bool,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct BuildStats {
pub total_dps: f64,
pub effective_hp: f64,
pub life: f64,
pub energy_shield: f64,
pub armour: f64,
pub evasion: f64,
pub fire_res: i32,
pub cold_res: i32,
pub lightning_res: i32,
pub chaos_res: i32,
}
impl PobHeadless {
pub fn new() -> LuaResult<Self> {
let lua = Lua::new();
Ok(Self {
lua,
initialized: false,
})
}
pub fn init(&mut self, pob_path: &str) -> Result<(), PobError> {
let pob_path = Path::new(pob_path);
let pob_src_path = pob_path.join("src");
let pob_runtime_lua = pob_path.join("runtime/lua");
let original_cwd = std::env::current_dir()?;
std::env::set_current_dir(&pob_src_path)?;
tracing::info!("Initializing PoB from {:?}", pob_src_path);
let runtime_lua_path = pob_runtime_lua
.to_str()
.ok_or_else(|| PobError::CalculationFailed("Invalid path".to_owned()))?;
self.lua
.load(format!(
r#"package.path = package.path .. ";{0}/?.lua;{0}/?/init.lua""#,
runtime_lua_path
))
.exec()?;
self.lua
.load(
r#"
package.preload['lua-utf8'] = function()
local utf8 = {}
utf8.reverse = string.reverse
utf8.gsub = string.gsub
utf8.find = string.find
utf8.sub = string.sub
utf8.match = string.match
utf8.len = string.len
function utf8.next(s, i, offset)
if offset == -1 then
return i > 1 and i - 1 or nil
else
return i < #s and i + 1 or nil
end
end
return utf8
end
"#,
)
.exec()?;
self.lua.load("arg = {}").exec()?;
let result = self.lua.load("dofile('HeadlessWrapper.lua')").exec();
std::env::set_current_dir(original_cwd)?;
result?;
self.initialized = true;
tracing::info!("PoB headless initialized successfully");
Ok(())
}
pub fn load_build_xml(&self, xml: &str) -> Result<(), PobError> {
if !self.initialized {
return Err(PobError::NotInitialized);
}
let load_fn: mlua::Function = self.lua.globals().get("loadBuildFromXML")?;
load_fn.call::<()>((xml, "imported_build"))?;
tracing::debug!("Build loaded from XML");
Ok(())
}
pub fn import_build(&self, _code: &str) -> Result<(), PobError> {
if !self.initialized {
return Err(PobError::NotInitialized);
}
Err(PobError::InvalidBuildCode(
"Build code import not yet implemented (requires Inflate)".to_owned(),
))
}
pub fn calculate(&self) -> Result<BuildStats, PobError> {
if !self.initialized {
return Err(PobError::NotInitialized);
}
let build: mlua::Table = self.lua.globals().get("build")?;
let calcs_tab: mlua::Table = build.get("calcsTab")?;
let main_output: mlua::Table = calcs_tab.get("mainOutput")?;
let calcs_output: mlua::Table = calcs_tab.get("calcsOutput")?;
let total_dps = main_output
.get::<f64>("TotalDPS")
.or_else(|_| main_output.get::<f64>("CombinedDPS"))
.unwrap_or(0.0);
let life = main_output.get::<f64>("Life").unwrap_or(0.0);
let energy_shield = main_output.get::<f64>("EnergyShield").unwrap_or(0.0);
let armour = main_output.get::<f64>("Armour").unwrap_or(0.0);
let evasion = main_output.get::<f64>("Evasion").unwrap_or(0.0);
let fire_res = main_output.get::<i32>("FireResist").unwrap_or(0);
let cold_res = main_output.get::<i32>("ColdResist").unwrap_or(0);
let lightning_res = main_output.get::<i32>("LightningResist").unwrap_or(0);
let chaos_res = main_output.get::<i32>("ChaosResist").unwrap_or(0);
let effective_hp = calcs_output
.get::<f64>("PhysicalMaximumHitTaken")
.unwrap_or(0.0);
Ok(BuildStats {
total_dps,
effective_hp,
life,
energy_shield,
armour,
evasion,
fire_res,
cold_res,
lightning_res,
chaos_res,
})
}
pub fn export_build(&self) -> Result<String, PobError> {
if !self.initialized {
return Err(PobError::NotInitialized);
}
Ok(String::new())
}
pub fn set_passive_tree(&self, _tree_data: &str) -> Result<(), PobError> {
if !self.initialized {
return Err(PobError::NotInitialized);
}
Ok(())
}
pub fn set_main_skill(&self, _skill_name: &str) -> Result<(), PobError> {
if !self.initialized {
return Err(PobError::NotInitialized);
}
Ok(())
}
}
impl Default for PobHeadless {
fn default() -> Self {
Self::new().expect("Failed to create Lua runtime")
}
}