use pyo3::prelude::*;
use pyo3::exceptions::{PyIOError, PyValueError};
use std::path::Path;
use std::collections::HashMap;
use crate::resource::prp::{self, PlasmaRead};
use crate::core::class_index::ClassIndex;
#[pyclass]
#[derive(Clone)]
pub struct PrpFile {
inner: std::sync::Arc<prp::PrpPage>,
}
#[pymethods]
impl PrpFile {
#[staticmethod]
fn load(path: &str) -> PyResult<Self> {
let page = prp::PrpPage::from_file(Path::new(path))
.map_err(|e| PyIOError::new_err(format!("{}", e)))?;
Ok(Self { inner: std::sync::Arc::new(page) })
}
#[staticmethod]
fn from_bytes(data: &[u8]) -> PyResult<Self> {
let page = prp::PrpPage::from_bytes(data.to_vec())
.map_err(|e| PyIOError::new_err(format!("{}", e)))?;
Ok(Self { inner: std::sync::Arc::new(page) })
}
fn save(&self, path: &str) -> PyResult<()> {
self.inner.save(Path::new(path))
.map_err(|e| PyIOError::new_err(format!("{}", e)))
}
fn to_bytes(&self) -> PyResult<Vec<u8>> {
self.inner.to_bytes()
.map_err(|e| PyIOError::new_err(format!("{}", e)))
}
#[getter]
fn age_name(&self) -> &str {
&self.inner.header.age_name
}
#[getter]
fn page_name(&self) -> &str {
&self.inner.header.page_name
}
fn __len__(&self) -> usize {
self.inner.keys.len()
}
fn __iter__(&self) -> PyResult<ObjectIterator> {
Ok(ObjectIterator {
page: self.inner.clone(),
index: 0,
})
}
fn objects(&self) -> Vec<PrpObject> {
self.inner.keys.iter().map(|k| PrpObject {
key: k.clone(),
page: self.inner.clone(),
}).collect()
}
fn objects_of_type(&self, class_type: u16) -> Vec<PrpObject> {
self.inner.keys.iter()
.filter(|k| k.class_type == class_type)
.map(|k| PrpObject {
key: k.clone(),
page: self.inner.clone(),
})
.collect()
}
fn objects_by_name(&self, class_name: &str) -> Vec<PrpObject> {
self.inner.keys.iter()
.filter(|k| ClassIndex::class_name(k.class_type) == class_name)
.map(|k| PrpObject {
key: k.clone(),
page: self.inner.clone(),
})
.collect()
}
fn count_by_type(&self) -> HashMap<String, usize> {
let mut counts: HashMap<String, usize> = HashMap::new();
for key in &self.inner.keys {
let name = ClassIndex::class_name(key.class_type).to_string();
*counts.entry(name).or_insert(0) += 1;
}
counts
}
#[getter]
fn header(&self) -> PageHeaderInfo {
PageHeaderInfo {
version: self.inner.header.version,
sequence_number: self.inner.header.sequence_number,
flags: self.inner.header.flags,
age_name: self.inner.header.age_name.clone(),
page_name: self.inner.header.page_name.clone(),
major_version: self.inner.header.major_version,
checksum: self.inner.header.checksum,
data_start: self.inner.header.data_start,
index_start: self.inner.header.index_start,
}
}
fn __repr__(&self) -> String {
format!("PrpFile('{}', '{}', {} objects)",
self.inner.header.age_name,
self.inner.header.page_name,
self.inner.keys.len())
}
}
#[pyclass]
struct ObjectIterator {
page: std::sync::Arc<prp::PrpPage>,
index: usize,
}
#[pymethods]
impl ObjectIterator {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf }
fn __next__(&mut self) -> Option<PrpObject> {
if self.index >= self.page.keys.len() {
return None;
}
let key = self.page.keys[self.index].clone();
self.index += 1;
Some(PrpObject { key, page: self.page.clone() })
}
}
#[pyclass]
#[derive(Clone)]
pub struct PrpObject {
key: prp::ObjectKey,
page: std::sync::Arc<prp::PrpPage>,
}
#[pymethods]
impl PrpObject {
#[getter]
fn name(&self) -> &str {
&self.key.object_name
}
#[getter]
fn class_type(&self) -> u16 {
self.key.class_type
}
#[getter]
fn class_name(&self) -> &str {
ClassIndex::class_name(self.key.class_type)
}
#[getter]
fn object_id(&self) -> u32 {
self.key.object_id
}
#[getter]
fn location_sequence(&self) -> u32 {
self.key.location_sequence
}
#[getter]
fn data(&self) -> Option<Vec<u8>> {
self.page.object_data(&self.key).map(|d| d.to_vec())
}
#[getter]
fn data_len(&self) -> u32 {
self.key.data_len
}
fn as_scene_object(&self) -> PyResult<SceneObject> {
if self.key.class_type != 0x0001 {
return Err(PyValueError::new_err(format!(
"Not a plSceneObject (class 0x{:04X})", self.key.class_type)));
}
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
SceneObject::parse(data)
}
fn as_mipmap(&self) -> PyResult<Mipmap> {
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
Mipmap::parse(data)
}
fn as_material(&self) -> PyResult<Material> {
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
Material::parse(data)
}
fn as_layer(&self) -> PyResult<Layer> {
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
Layer::parse(data)
}
fn as_python_file_mod(&self) -> PyResult<PythonFileMod> {
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
PythonFileMod::parse(data)
}
fn as_responder(&self) -> PyResult<ResponderMod> {
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
ResponderMod::parse(data)
}
fn as_physical(&self) -> PyResult<PhysicalData> {
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
PhysicalData::parse(data)
}
fn as_sound(&self) -> PyResult<SoundInfo> {
let data = self.page.object_data(&self.key)
.ok_or_else(|| PyIOError::new_err("Object data not found"))?;
SoundInfo::parse(data)
}
fn __repr__(&self) -> String {
format!("PrpObject('{}', class=0x{:04X}/{})",
self.key.object_name, self.key.class_type,
ClassIndex::class_name(self.key.class_type))
}
}
#[pyclass]
#[derive(Clone)]
pub struct PageHeaderInfo {
#[pyo3(get)] pub version: u32,
#[pyo3(get)] pub sequence_number: u32,
#[pyo3(get)] pub flags: u16,
#[pyo3(get)] pub age_name: String,
#[pyo3(get)] pub page_name: String,
#[pyo3(get)] pub major_version: u16,
#[pyo3(get)] pub checksum: u32,
#[pyo3(get)] pub data_start: u32,
#[pyo3(get)] pub index_start: u32,
}
#[pymethods]
impl PageHeaderInfo {
fn __repr__(&self) -> String {
format!("PageHeader(age='{}', page='{}', v{})",
self.age_name, self.page_name, self.version)
}
}
#[pyclass]
#[derive(Clone)]
pub struct SceneObject {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub draw_interface_names: Vec<String>,
#[pyo3(get)] pub coord_interface_name: Option<String>,
#[pyo3(get)] pub modifier_names: Vec<String>,
}
impl SceneObject {
fn parse(data: &[u8]) -> PyResult<Self> {
use crate::core::scene_object::SceneObjectData;
let mut cursor = std::io::Cursor::new(data);
let _class_idx = cursor.read_i16().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let so = SceneObjectData::read(&mut cursor)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let name = so.self_key.as_ref().map(|u| u.object_name.clone()).unwrap_or_default();
Ok(Self {
name,
draw_interface_names: so.draw_interface
.iter().map(|u| u.object_name.clone()).collect(),
coord_interface_name: so.coord_interface
.as_ref().map(|u| u.object_name.clone()),
modifier_names: so.modifiers.iter()
.filter_map(|u| u.as_ref().map(|k| k.object_name.clone())).collect(),
})
}
}
#[pymethods]
impl SceneObject {
fn __repr__(&self) -> String {
format!("SceneObject('{}', {} drawables, {} modifiers)",
self.name, self.draw_interface_names.len(), self.modifier_names.len())
}
}
#[pyclass]
#[derive(Clone)]
pub struct Mipmap {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub width: u32,
#[pyo3(get)] pub height: u32,
#[pyo3(get)] pub num_levels: u8,
#[pyo3(get)] pub compression_type: u8,
#[pyo3(get)] pub dxt_type: u8,
#[pyo3(get)] pub total_size: u32,
pixel_data: Vec<u8>,
}
impl Mipmap {
fn parse(data: &[u8]) -> PyResult<Self> {
use crate::core::uoid::read_key_uoid;
let mut cursor = std::io::Cursor::new(data);
let _class_idx = cursor.read_i16().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let self_key = read_key_uoid(&mut cursor)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let name = self_key.as_ref().map(|u| u.object_name.clone()).unwrap_or_default();
let version = cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
if version != 2 {
return Err(PyValueError::new_err(format!("Unsupported bitmap version: {}", version)));
}
let _pixel_size = cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let _space = cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let _flags = cursor.read_u16().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let compression_type = cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let (dxt_type, _block_size) = match compression_type {
0 | 2 | 3 => { (cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?, 0u8) }
1 => {
let bs = cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let ct = cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
(ct, bs)
}
_ => { return Err(PyValueError::new_err(format!("Unknown compression: {}", compression_type))); }
};
let mut _skip = [0u8; 8];
std::io::Read::read_exact(&mut cursor, &mut _skip)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let width = cursor.read_u32().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let height = cursor.read_u32().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let _row_bytes = cursor.read_u32().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let total_size = cursor.read_u32().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let num_levels = cursor.read_u8().map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let pos = cursor.position() as usize;
let remaining = data.len().saturating_sub(pos);
let read_size = (total_size as usize).min(remaining);
let pixel_data = data[pos..pos + read_size].to_vec();
Ok(Self {
name,
width,
height,
num_levels,
compression_type,
dxt_type,
total_size,
pixel_data,
})
}
}
#[pymethods]
impl Mipmap {
#[getter]
fn pixel_data(&self) -> &[u8] {
&self.pixel_data
}
fn __repr__(&self) -> String {
format!("Mipmap('{}', {}x{}, {} levels, {} bytes)",
self.name, self.width, self.height, self.num_levels, self.total_size)
}
}
#[pyclass]
#[derive(Clone)]
pub struct Material {
#[pyo3(get)] pub layer_names: Vec<String>,
}
impl Material {
fn parse(data: &[u8]) -> PyResult<Self> {
let names = prp::parse_material_layers(data)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
Ok(Self { layer_names: names })
}
}
#[pymethods]
impl Material {
fn __repr__(&self) -> String {
format!("Material({} layers: {:?})", self.layer_names.len(), self.layer_names)
}
}
#[pyclass]
#[derive(Clone)]
pub struct Layer {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub texture_name: Option<String>,
#[pyo3(get)] pub blend_flags: u32,
#[pyo3(get)] pub shade_flags: u32,
#[pyo3(get)] pub misc_flags: u32,
#[pyo3(get)] pub z_flags: u32,
#[pyo3(get)] pub uv_channel: u8,
#[pyo3(get)] pub opacity: f32,
#[pyo3(get)] pub preshade_color: [f32; 4],
#[pyo3(get)] pub runtime_color: [f32; 4],
#[pyo3(get)] pub ambient_color: [f32; 4],
#[pyo3(get)] pub specular_color: [f32; 4],
}
impl Layer {
fn parse(data: &[u8]) -> PyResult<Self> {
let ls = prp::parse_layer_state(data)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
Ok(Self {
name: ls.name,
texture_name: ls.texture_name,
blend_flags: ls.blend_flags,
shade_flags: ls.shade_flags,
misc_flags: ls.misc_flags,
z_flags: ls.z_flags,
uv_channel: ls.uv_channel,
opacity: ls.opacity,
preshade_color: ls.preshade_color,
runtime_color: ls.runtime_color,
ambient_color: ls.ambient_color,
specular_color: ls.specular_color,
})
}
}
#[pymethods]
impl Layer {
fn __repr__(&self) -> String {
format!("Layer('{}', texture={:?}, opacity={:.2})",
self.name, self.texture_name, self.opacity)
}
}
#[pyclass]
#[derive(Clone)]
pub struct PythonFileMod {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub python_file: String,
#[pyo3(get)] pub receiver_names: Vec<String>,
#[pyo3(get)] pub params: Vec<PythonParam>,
}
#[pyclass]
#[derive(Clone)]
pub struct PythonParam {
#[pyo3(get)] pub id: u32,
#[pyo3(get)] pub param_type: String,
#[pyo3(get)] pub value: String,
}
impl PythonFileMod {
fn parse(data: &[u8]) -> PyResult<Self> {
let pfm = prp::parse_python_file_mod(data)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let name = pfm.self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
Ok(Self {
name,
python_file: pfm.script_file,
receiver_names: pfm.receivers.iter().map(|u| u.object_name.clone()).collect(),
params: pfm.parameters.iter().map(|p| PythonParam {
id: p.id as u32,
param_type: format!("{:?}", p.value),
value: match &p.value {
prp::PythonParamValue::Int(v) => v.to_string(),
prp::PythonParamValue::Float(v) => v.to_string(),
prp::PythonParamValue::Bool(v) => v.to_string(),
prp::PythonParamValue::String(v) => v.clone(),
prp::PythonParamValue::Key(v) => v.as_ref()
.map(|u| u.object_name.clone())
.unwrap_or_else(|| "None".to_string()),
prp::PythonParamValue::None => "None".to_string(),
},
}).collect(),
})
}
}
#[pymethods]
impl PythonFileMod {
fn __repr__(&self) -> String {
format!("PythonFileMod('{}', file='{}', {} params)",
self.name, self.python_file, self.params.len())
}
}
#[pymethods]
impl PythonParam {
fn __repr__(&self) -> String {
format!("PythonParam(id={}, {}={})", self.id, self.param_type, self.value)
}
}
#[pyclass]
#[derive(Clone)]
pub struct ResponderMod {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub num_states: usize,
#[pyo3(get)] pub cur_state: u8,
#[pyo3(get)] pub enabled: bool,
}
impl ResponderMod {
fn parse(data: &[u8]) -> PyResult<Self> {
let r = prp::parse_responder_modifier(data)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
let name = r.self_key.as_ref().map(|k| k.object_name.clone()).unwrap_or_default();
Ok(Self {
name,
num_states: r.states.len(),
cur_state: r.cur_state,
enabled: r.enabled,
})
}
}
#[pymethods]
impl ResponderMod {
fn __repr__(&self) -> String {
format!("ResponderMod('{}', {} states, enabled={})",
self.name, self.num_states, self.enabled)
}
}
#[pyclass]
#[derive(Clone)]
pub struct PhysicalData {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub mass: f32,
#[pyo3(get)] pub friction: f32,
#[pyo3(get)] pub restitution: f32,
#[pyo3(get)] pub group: String,
#[pyo3(get)] pub bounds_type: String,
#[pyo3(get)] pub num_verts: usize,
#[pyo3(get)] pub num_faces: usize,
}
impl PhysicalData {
fn parse(data: &[u8]) -> PyResult<Self> {
let p = prp::parse_px_physical(data)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
Ok(Self {
name: p.name,
mass: p.mass,
friction: p.friction,
restitution: p.restitution,
group: format!("{:?}", p.group),
bounds_type: format!("{:?}", p.bounds),
num_verts: match &p.shape {
prp::PhysShapeData::TriMesh { vertices, .. } => vertices.len(),
prp::PhysShapeData::Hull { vertices, .. } => vertices.len(),
_ => 0,
},
num_faces: match &p.shape {
prp::PhysShapeData::TriMesh { indices, .. } => indices.len() / 3,
_ => 0,
},
})
}
}
#[pymethods]
impl PhysicalData {
fn __repr__(&self) -> String {
format!("PhysicalData('{}', mass={:.1}, group={}, bounds={}, {} verts)",
self.name, self.mass, self.group, self.bounds_type, self.num_verts)
}
}
#[pyclass]
#[derive(Clone)]
pub struct SoundInfo {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub sound_file: String,
#[pyo3(get)] pub volume: f32,
#[pyo3(get)] pub is_3d: bool,
#[pyo3(get)] pub auto_start: bool,
#[pyo3(get)] pub looping: bool,
#[pyo3(get)] pub min_distance: f32,
#[pyo3(get)] pub max_distance: f32,
}
impl SoundInfo {
fn parse(data: &[u8]) -> PyResult<Self> {
let s = prp::parse_win32_sound(data)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
Ok(Self {
name: s.name,
sound_file: s.buffer_name.unwrap_or_default(),
volume: s.volume,
is_3d: s.is_3d,
auto_start: s.auto_start,
looping: s.looping,
min_distance: s.min_falloff,
max_distance: s.max_falloff,
})
}
}
#[pymethods]
impl SoundInfo {
fn __repr__(&self) -> String {
format!("SoundInfo('{}', file='{}', vol={:.2}, 3d={})",
self.name, self.sound_file, self.volume, self.is_3d)
}
}
#[pyclass]
#[derive(Clone)]
pub struct AgeFile {
#[pyo3(get)] pub age_name: String,
#[pyo3(get)] pub sequence_prefix: i32,
#[pyo3(get)] pub max_capacity: i32,
#[pyo3(get)] pub day_length: f32,
#[pyo3(get)] pub pages: Vec<PageEntry>,
}
#[pyclass]
#[derive(Clone)]
pub struct PageEntry {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub seq_suffix: u32,
#[pyo3(get)] pub flags: u8,
#[pyo3(get)] pub auto_load: bool,
}
#[pymethods]
impl AgeFile {
#[staticmethod]
fn load(path: &str) -> PyResult<Self> {
let desc = crate::age::description::AgeDescription::from_file(Path::new(path))
.map_err(|e| PyIOError::new_err(format!("{}", e)))?;
Ok(Self::from_desc(desc))
}
#[staticmethod]
fn parse(age_name: &str, content: &str) -> PyResult<Self> {
let desc = crate::age::description::AgeDescription::parse(age_name, content)
.map_err(|e| PyValueError::new_err(format!("{}", e)))?;
Ok(Self::from_desc(desc))
}
fn prp_filename(&self, page_name: &str) -> Option<String> {
self.pages.iter()
.find(|p| p.name == page_name)
.map(|_| format!("{}_District_{}.prp", self.age_name, page_name))
}
fn __repr__(&self) -> String {
format!("AgeFile('{}', {} pages, prefix={})",
self.age_name, self.pages.len(), self.sequence_prefix)
}
}
impl AgeFile {
fn from_desc(desc: crate::age::description::AgeDescription) -> Self {
Self {
pages: desc.pages.iter().map(|p| PageEntry {
name: p.name.clone(),
seq_suffix: p.seq_suffix,
flags: p.flags,
auto_load: p.auto_load(),
}).collect(),
age_name: desc.age_name,
sequence_prefix: desc.sequence_prefix,
max_capacity: desc.max_capacity,
day_length: desc.day_length,
}
}
}
#[pymethods]
impl PageEntry {
fn __repr__(&self) -> String {
format!("PageEntry('{}', seq={}, auto_load={})",
self.name, self.seq_suffix, self.auto_load)
}
}
#[pyclass]
pub struct SdlFile {
inner: crate::sdl::SdlManager,
}
#[pymethods]
impl SdlFile {
#[new]
fn new() -> Self {
Self { inner: crate::sdl::SdlManager::new() }
}
fn load_directory(&mut self, path: &str) -> PyResult<usize> {
self.inner.load_directory(Path::new(path))
.map_err(|e| PyIOError::new_err(format!("{}", e)))
}
fn load_file(&mut self, path: &str) -> PyResult<usize> {
self.inner.load_file(Path::new(path))
.map_err(|e| PyIOError::new_err(format!("{}", e)))
}
fn find(&self, name: &str, version: u32) -> Option<SdlDescriptor> {
self.inner.find(name, version).map(|d| SdlDescriptor {
name: d.name.clone(),
version: d.version,
variables: d.variables.iter().map(|v| SdlVariable {
name: v.name.clone(),
var_type: format!("{:?}", v.var_type),
count: v.count,
default_value: v.default_value.clone(),
}).collect(),
})
}
fn __len__(&self) -> usize {
self.inner.descriptor_count()
}
fn __repr__(&self) -> String {
format!("SdlFile({} descriptors)", self.inner.descriptor_count())
}
}
#[pyclass]
#[derive(Clone)]
pub struct SdlDescriptor {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub version: u32,
#[pyo3(get)] pub variables: Vec<SdlVariable>,
}
#[pymethods]
impl SdlDescriptor {
fn __repr__(&self) -> String {
format!("SdlDescriptor('{}', v{}, {} vars)",
self.name, self.version, self.variables.len())
}
}
#[pyclass]
#[derive(Clone)]
pub struct SdlVariable {
#[pyo3(get)] pub name: String,
#[pyo3(get)] pub var_type: String,
#[pyo3(get)] pub count: usize,
#[pyo3(get)] pub default_value: Option<String>,
}
#[pymethods]
impl SdlVariable {
fn __repr__(&self) -> String {
format!("SdlVariable('{}', type={}, count={}, default={:?})",
self.name, self.var_type, self.count, self.default_value)
}
}
#[pyfunction]
fn class_name(class_type: u16) -> &'static str {
ClassIndex::class_name(class_type)
}
#[pymodule]
fn plasma_prp(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PrpFile>()?;
m.add_class::<PrpObject>()?;
m.add_class::<PageHeaderInfo>()?;
m.add_class::<SceneObject>()?;
m.add_class::<Mipmap>()?;
m.add_class::<Material>()?;
m.add_class::<Layer>()?;
m.add_class::<PythonFileMod>()?;
m.add_class::<PythonParam>()?;
m.add_class::<ResponderMod>()?;
m.add_class::<PhysicalData>()?;
m.add_class::<SoundInfo>()?;
m.add_class::<AgeFile>()?;
m.add_class::<PageEntry>()?;
m.add_class::<SdlFile>()?;
m.add_class::<SdlDescriptor>()?;
m.add_class::<SdlVariable>()?;
m.add_function(wrap_pyfunction!(class_name, m)?)?;
Ok(())
}