use std::fmt::{Debug, Display, Write};
use crate::cclocallevels::{
gdobj::{
ids::properties::*,
meta::{GDObjAttributes, GDObjConfig},
structs::{ColourChannel, Event, GDObjPropType, GDValue, Group, MoveEasing, ZLayer},
},
properties::{self, OBJECT_NAMES, get_obj_property_type},
};
pub mod defaults;
pub mod ids {
#![allow(missing_docs)]
include!(concat!(env!("OUT_DIR"), "/ids.rs"));
}
pub mod constructors;
pub mod meta;
pub mod structs;
macro_rules! parse {
($v:expr => $t:ty) => {
$v.parse::<$t>().unwrap_or_default()
};
}
#[derive(Clone, PartialEq)]
#[must_use]
pub struct GDObject {
pub id: i32,
pub config: GDObjConfig,
pub properties: Vec<(u16, GDValue)>,
}
impl Display for GDObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut group_str = String::new();
if !self.config.groups.is_empty() {
group_str += " with groups: ";
for (idx, g) in self.config.groups.iter().enumerate() {
if idx != 0 {
group_str.push_str(", ");
}
let _ = write!(group_str, "{}", g.id());
}
}
let mut trigger_conf_str = String::new();
if self.config.trigger_cfg.spawnable || self.config.trigger_cfg.touchable {
if self.config.trigger_cfg.multitriggerable {
trigger_conf_str += "Multi";
}
if self.config.trigger_cfg.touchable {
trigger_conf_str += "touchable ";
} else if self.config.trigger_cfg.spawnable {
trigger_conf_str += "spawnable ";
}
}
write!(
f,
"{trigger_conf_str}{} @ ({}, {}) scaled to ({}, {}){} angled to {}°",
self.get_name(),
self.config.pos.0,
self.config.pos.1,
self.config.scale.0,
self.config.scale.1,
group_str,
self.config.angle
)
}
}
impl Debug for GDObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut property_str = String::with_capacity(self.properties.len() * 32);
for (property, value) in &self.properties {
let desc = properties::PROPERTY_TABLE.get(property).map(|p| p.0);
if let Some(d) = desc {
write!(property_str, "\n - {d}: {value:?}")
} else {
write!(property_str, "\n - {property}: {value:?}")
}
.unwrap();
}
write!(
f,
"{} with properties:{property_str}",
<Self as ToString>::to_string(self),
)
}
}
impl GDObject {
pub fn parse_str<T: AsRef<str>>(s: T) -> GDObject {
let s = s.as_ref();
let mut obj = GDObject {
id: 1,
config: GDObjConfig::default(),
properties: vec![],
};
let mut iter = s.trim_end_matches(';').split(',');
while let (Some(idx), Some(val)) = (iter.next(), iter.next()) {
let idx_u16 = match idx.parse::<u16>() {
Ok(n) => n,
Err(_) => match idx[2..].parse::<u16>() {
Ok(n) => n + 10_000,
Err(_) => 65535,
},
};
match idx_u16 {
OBJECT_ID => obj.id = parse!(val => i32),
X_POS => obj.config.pos.0 = val.parse().unwrap_or(0.0),
Y_POS => obj.config.pos.1 = val.parse().unwrap_or(0.0),
ROTATION => obj.config.angle = val.parse().unwrap_or(0.0),
TOUCH_TRIGGERABLE => obj.config.trigger_cfg.touchable = parse!(val => bool),
SPAWN_TRIGGERABLE => obj.config.trigger_cfg.spawnable = parse!(val => bool),
MULTITRIGGERABLE => obj.config.trigger_cfg.multitriggerable = parse!(val => bool),
GROUPS => {
obj.config.add_groups(
val.trim_matches('"')
.split('.')
.filter_map(|g| g.parse::<i16>().ok())
.map(Group::Regular)
.collect::<Vec<Group>>(),
);
}
X_SCALE => obj.config.scale.0 = val.parse().unwrap_or(1.0),
Y_SCALE => obj.config.scale.1 = val.parse().unwrap_or(1.0),
EDITOR_LAYER_1 => obj.config.editor_layers.0 = parse!(val => i16),
EDITOR_LAYER_2 => obj.config.editor_layers.1 = parse!(val => i16),
OBJECT_COLOUR => {
obj.config.colour_channels.0 = ColourChannel::from(parse!(val => i16));
}
SECONDARY_COLOUR => {
obj.config.colour_channels.1 = ColourChannel::from(parse!(val => i16));
}
Z_LAYER => obj.config.z_layer = ZLayer::from(parse!(val => i32)),
Z_ORDER => obj.config.z_order = parse!(val => i32),
ENTER_EFFECT_CHANNEL => obj.config.enter_effect_channel = parse!(val => i16),
OBJECT_MATERIAL => obj.config.material_id = parse!(val => i16),
DONT_FADE => obj
.config
.attributes
.set(GDObjAttributes::dont_fade, parse!(val => bool)),
DONT_ENTER => obj
.config
.attributes
.set(GDObjAttributes::dont_enter, parse!(val => bool)),
NO_OBJECT_EFFECTS => obj
.config
.attributes
.set(GDObjAttributes::no_effects, parse!(val => bool)),
IS_GROUP_PARENT => obj
.config
.attributes
.set(GDObjAttributes::is_group_parent, parse!(val => bool)),
IS_AREA_PARENT => obj
.config
.attributes
.set(GDObjAttributes::is_area_parent, parse!(val => bool)),
DONT_BOOST_X => obj
.config
.attributes
.set(GDObjAttributes::dont_boost_x, parse!(val => bool)),
DONT_BOOST_Y => obj
.config
.attributes
.set(GDObjAttributes::dont_boost_y, parse!(val => bool)),
IS_HIGH_DETAIL => obj
.config
.attributes
.set(GDObjAttributes::high_detail, parse!(val => bool)),
NO_TOUCH => obj
.config
.attributes
.set(GDObjAttributes::no_touch, parse!(val => bool)),
PASSABLE => obj
.config
.attributes
.set(GDObjAttributes::passable, parse!(val => bool)),
HIDDEN => obj
.config
.attributes
.set(GDObjAttributes::hidden, parse!(val => bool)),
NONSTICK_X => obj
.config
.attributes
.set(GDObjAttributes::non_stick_x, parse!(val => bool)),
NONSTICK_Y => obj
.config
.attributes
.set(GDObjAttributes::non_stick_y, parse!(val => bool)),
EXTRA_STICKY => obj
.config
.attributes
.set(GDObjAttributes::extra_sticky, parse!(val => bool)),
HAS_EXTENDED_COLLISION => obj
.config
.attributes
.set(GDObjAttributes::extended_collision, parse!(val => bool)),
IS_ICE_BLOCK => obj
.config
.attributes
.set(GDObjAttributes::is_ice_block, parse!(val => bool)),
GRIP_SLOPE => obj
.config
.attributes
.set(GDObjAttributes::grip_slope, parse!(val => bool)),
NO_GLOW => obj
.config
.attributes
.set(GDObjAttributes::no_glow, parse!(val => bool)),
NO_PARTICLES => obj
.config
.attributes
.set(GDObjAttributes::no_particles, parse!(val => bool)),
SCALE_STICK => obj
.config
.attributes
.set(GDObjAttributes::scale_stick, parse!(val => bool)),
NO_AUDIO_SCALE => obj
.config
.attributes
.set(GDObjAttributes::no_audio_scale, parse!(val => bool)),
SINGLE_PLAYER_TOUCH => obj
.config
.attributes
.set(GDObjAttributes::single_ptouch, parse!(val => bool)),
CENTER_EFFECT => obj
.config
.attributes
.set(GDObjAttributes::center_effect, parse!(val => bool)),
REVERSES_GAMEPLAY => obj
.config
.attributes
.set(GDObjAttributes::reverse, parse!(val => bool)),
MATERIAL_CONTROL_ID => obj.config.control_id = parse!(val => i16),
PARENT_GROUPS => {
obj.config.add_groups(
val.trim_matches('"')
.split('.')
.filter_map(|g| g.parse::<i16>().ok())
.map(Group::Parent)
.collect::<Vec<Group>>(),
);
}
n => obj.set_property_raw(n, val),
}
}
obj
}
fn set_property_raw(&mut self, p: u16, value: &str) {
self.set_property(
p,
GDValue::from(
get_obj_property_type(p).unwrap_or(GDObjPropType::Unknown),
value,
),
);
}
pub fn set_property(&mut self, p: u16, val: GDValue) {
match self.properties.binary_search_by_key(&p, |t| t.0) {
Ok(idx) => self.properties[idx].1 = val,
Err(idx) => self.properties.insert(idx, (p, val)),
}
}
pub fn del_property(&mut self, p: u16) {
if let Ok(idx) = self.properties.binary_search_by_key(&p, |t| t.0) {
let _ = self.properties.remove(idx);
}
}
#[must_use]
pub fn serialise_to_string(&self) -> String {
let mut properties_string = String::with_capacity(self.properties.len() * 8);
for (idx, val) in &self.properties {
let (pref, id) = if *idx < 10_000 {
("", *idx)
} else {
("kA", idx - 10_000) };
write!(properties_string, ",{pref}{id},{val}").unwrap();
}
let config_str = self.config.serialise_to_string();
let mut raw_str = format!("1,{}{config_str}{properties_string}", self.id);
raw_str.retain(|c| c != '"');
raw_str.push(';');
raw_str
}
#[inline]
#[must_use]
pub fn get_name(&self) -> String {
OBJECT_NAMES
.iter()
.find(|&o| o.0 == self.id)
.unwrap_or(&(0, format!("Object {}", self.id).as_str()))
.1
.to_string()
}
#[inline]
pub fn new(id: i32, config: &GDObjConfig, properties: Vec<(u16, GDValue)>) -> Self {
GDObject {
id,
config: config.clone(),
properties,
}
}
#[inline]
pub fn default_from_id(id: i32) -> Self {
defaults::default_object(id)
}
#[inline]
fn get_attr_as_gdvalue(&self, attr: GDObjAttributes) -> GDValue {
GDValue::Bool(self.config.get_attribute_flag(attr))
}
pub fn get_property(&self, p: u16) -> Option<GDValue> {
match p {
1 => Some(GDValue::Int(self.id)),
2 => Some(GDValue::Float(self.config.pos.0)),
3 => Some(GDValue::Float(self.config.pos.1)),
6 => Some(GDValue::Float(self.config.angle)),
11 => Some(GDValue::Bool(self.config.trigger_cfg.touchable)),
57 => Some(GDValue::from_group_list(&self.config.groups)),
62 => Some(GDValue::Bool(self.config.trigger_cfg.spawnable)),
87 => Some(GDValue::Bool(self.config.trigger_cfg.multitriggerable)),
128 => Some(GDValue::Float(self.config.scale.0)),
129 => Some(GDValue::Float(self.config.scale.1)),
20 => Some(GDValue::Short(self.config.editor_layers.0)),
61 => Some(GDValue::Short(self.config.editor_layers.1)),
21 => Some(GDValue::Short(self.config.colour_channels.0.into())),
22 => Some(GDValue::Short(self.config.colour_channels.1.into())),
24 => Some(GDValue::ZLayer(self.config.z_layer)),
25 => Some(GDValue::Int(self.config.z_order)),
343 => Some(GDValue::Short(self.config.enter_effect_channel)),
446 => Some(GDValue::Short(self.config.material_id)),
534 => Some(GDValue::Short(self.config.control_id)),
64 => Some(self.get_attr_as_gdvalue(GDObjAttributes::dont_fade)),
67 => Some(self.get_attr_as_gdvalue(GDObjAttributes::dont_enter)),
116 => Some(self.get_attr_as_gdvalue(GDObjAttributes::no_effects)),
34 => Some(self.get_attr_as_gdvalue(GDObjAttributes::is_group_parent)),
279 => Some(self.get_attr_as_gdvalue(GDObjAttributes::is_area_parent)),
509 => Some(self.get_attr_as_gdvalue(GDObjAttributes::dont_boost_x)),
496 => Some(self.get_attr_as_gdvalue(GDObjAttributes::dont_boost_y)),
103 => Some(self.get_attr_as_gdvalue(GDObjAttributes::high_detail)),
121 => Some(self.get_attr_as_gdvalue(GDObjAttributes::no_touch)),
134 => Some(self.get_attr_as_gdvalue(GDObjAttributes::passable)),
135 => Some(self.get_attr_as_gdvalue(GDObjAttributes::hidden)),
136 => Some(self.get_attr_as_gdvalue(GDObjAttributes::non_stick_x)),
289 => Some(self.get_attr_as_gdvalue(GDObjAttributes::non_stick_y)),
495 => Some(self.get_attr_as_gdvalue(GDObjAttributes::extra_sticky)),
511 => Some(self.get_attr_as_gdvalue(GDObjAttributes::extended_collision)),
137 => Some(self.get_attr_as_gdvalue(GDObjAttributes::is_ice_block)),
193 => Some(self.get_attr_as_gdvalue(GDObjAttributes::grip_slope)),
96 => Some(self.get_attr_as_gdvalue(GDObjAttributes::no_glow)),
507 => Some(self.get_attr_as_gdvalue(GDObjAttributes::no_particles)),
356 => Some(self.get_attr_as_gdvalue(GDObjAttributes::scale_stick)),
372 => Some(self.get_attr_as_gdvalue(GDObjAttributes::no_audio_scale)),
284 => Some(self.get_attr_as_gdvalue(GDObjAttributes::single_ptouch)),
369 => Some(self.get_attr_as_gdvalue(GDObjAttributes::center_effect)),
117 => Some(self.get_attr_as_gdvalue(GDObjAttributes::reverse)),
_ => self
.properties
.binary_search_by_key(&p, |(key, _)| *key)
.ok()
.map(|idx| self.properties[idx].1.clone()),
}
}
pub fn set_config(&mut self, config: GDObjConfig) {
self.config = config;
}
}