use std::{
fmt::{Debug, Display, Write},
str::FromStr,
};
use crate::gdobj::{
ids::properties::{
CENTER_EFFECT, DONT_BOOST_X, DONT_BOOST_Y, DONT_ENTER, DONT_FADE, EDITOR_LAYER_1,
EDITOR_LAYER_2, ENTER_EFFECT_CHANNEL, EXTRA_STICKY, GRIP_SLOPE, GROUPS,
HAS_EXTENDED_COLLISION, HIDDEN, IS_AREA_PARENT, IS_GROUP_PARENT, IS_HIGH_DETAIL,
IS_ICE_BLOCK, MATERIAL_CONTROL_ID, MULTITRIGGERABLE, NO_AUDIO_SCALE, NO_GLOW,
NO_OBJECT_EFFECTS, NO_PARTICLES, NO_TOUCH, NONSTICK_X, NONSTICK_Y, OBJECT_COLOUR,
OBJECT_ID, OBJECT_MATERIAL, PARENT_GROUPS, PASSABLE, REVERSES_GAMEPLAY, ROTATION,
SCALE_STICK, SECONDARY_COLOUR, SINGLE_PLAYER_TOUCH, SPAWN_TRIGGERABLE, TOUCH_TRIGGERABLE,
X_POS, X_SCALE, Y_POS, Y_SCALE, Z_LAYER, Z_ORDER,
},
lookup::get_property_type,
};
use bitflags::bitflags;
use itoa;
use smallvec::SmallVec;
pub mod defaults;
pub mod ids {
#![allow(missing_docs)]
include!(concat!(env!("OUT_DIR"), "/ids.rs"));
}
pub mod lookup;
pub mod misc;
pub mod triggers;
pub mod animation_ids {
#![allow(missing_docs)]
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BigBeast {
Bite = 0,
Attack01 = 1,
Attack01End = 2,
Idle01 = 3,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Bat {
Idle01 = 0,
Idle02 = 1,
Idle03 = 2,
Attack01 = 3,
Attack02 = 4,
Attack02End = 5,
Sleep = 6,
SleepLoop = 7,
SleepEnd = 8,
Attack02Loop = 9,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Spikeball {
Idle01 = 0,
Idle02 = 1,
ToAttack01 = 2,
Attack01 = 3,
Attack02 = 4,
ToAttack03 = 5,
Attack03 = 6,
Idle03 = 7,
FromAttack03 = 8,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Anim {
Other(i32),
BigBeast(animation_ids::BigBeast),
Bat(animation_ids::Bat),
Spikeball(animation_ids::Spikeball),
}
impl From<Anim> for i32 {
fn from(value: Anim) -> i32 {
match value {
Anim::Bat(b) => b as i32,
Anim::BigBeast(b) => b as i32,
Anim::Spikeball(s) => s as i32,
Anim::Other(i) => i,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum Item {
Counter(i16),
Timer(i16),
Points,
Attempts,
MainTime,
}
impl Item {
pub fn get_type(&self) -> ItemType {
match self {
Self::Attempts => ItemType::Attempts,
Self::Counter(_) => ItemType::Counter,
Self::MainTime => ItemType::MainTime,
Self::Points => ItemType::Points,
Self::Timer(_) => ItemType::Timer,
}
}
#[inline(always)]
pub fn get_type_as_i32(&self) -> i32 {
self.get_type() as i32
}
pub fn as_special_mode(&self) -> Option<CounterMode> {
match self {
Self::Attempts => Some(CounterMode::Attempts),
Self::MainTime => Some(CounterMode::MainTime),
Self::Points => Some(CounterMode::Points),
_ => None,
}
}
#[inline(always)]
pub fn as_special_mode_i32(&self) -> i32 {
self.as_special_mode().unwrap() as i32
}
pub fn id(&self) -> i16 {
match self {
Self::Counter(c) => *c,
Self::Timer(t) => *t,
_ => 0,
}
}
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum ItemType {
Counter = 1,
Timer = 2,
Points = 3,
MainTime = 4,
Attempts = 5,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum CounterMode {
Attempts = -3,
Points = -2,
MainTime = -1,
}
#[repr(u8)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Copy)]
#[allow(missing_docs)]
pub enum GDObjPropType {
Int,
Float,
Text,
Bool,
Group,
Item,
Easing,
EventsList,
ColourChannel,
ProbabilitiesList,
SpawnRemapsList,
Toggle,
Unknown,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[allow(missing_docs)]
pub enum ZLayer {
B5 = -5,
B4 = -3,
B3 = -1,
B2 = 1,
B1 = 3,
#[default]
Default = 0,
T1 = 5,
T2 = 7,
T3 = 9,
T4 = 11,
}
impl From<i32> for ZLayer {
fn from(int: i32) -> Self {
match int {
-5 => Self::B5,
-3 => Self::B4,
-1 => Self::B3,
1 => Self::B2,
3 => Self::B1,
5 => Self::T1,
7 => Self::T2,
9 => Self::T3,
11 => Self::T4,
_ => Self::Default,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[allow(missing_docs)]
pub enum ColourChannel {
Channel(i16),
Background,
Ground1,
Ground2,
Line,
#[default]
Object,
ThreeDLine,
MiddleGround,
MiddleGround2,
P1,
P2,
}
impl From<i16> for ColourChannel {
fn from(c: i16) -> Self {
match c {
1000 => Self::Background,
1001 => Self::Ground1,
1009 => Self::Ground2,
1002 => Self::Line,
1004 => Self::Object,
1003 => Self::ThreeDLine,
1013 => Self::MiddleGround,
1014 => Self::MiddleGround2,
1005 => Self::P1,
1006 => Self::P2,
n => Self::Channel(n),
}
}
}
impl From<ColourChannel> for i16 {
fn from(value: ColourChannel) -> Self {
match value {
ColourChannel::Channel(n) => n,
ColourChannel::Background => 1000,
ColourChannel::Ground1 => 1001,
ColourChannel::Ground2 => 1009,
ColourChannel::Line => 1002,
ColourChannel::Object => 1004,
ColourChannel::ThreeDLine => 1003,
ColourChannel::MiddleGround => 1013,
ColourChannel::MiddleGround2 => 1014,
ColourChannel::P1 => 1005,
ColourChannel::P2 => 1006,
}
}
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[allow(missing_docs)]
pub enum MoveEasing {
#[default]
None = 0,
EaseInOut = 1,
EaseIn = 2,
EaseOut = 3,
ElasticInOut = 4,
ElasticIn = 5,
ElasticOut = 6,
BounceInOut = 7,
BounceIn = 8,
BounceOut = 9,
ExponentialInOut = 10,
ExponentialIn = 11,
ExponentialOut = 12,
SineInOut = 13,
SineIn = 14,
SineOut = 15,
BackInOut = 16,
BackIn = 17,
BackOut = 18,
}
impl From<i32> for MoveEasing {
fn from(i: i32) -> Self {
match i {
1 => Self::EaseInOut,
2 => Self::EaseIn,
3 => Self::EaseOut,
4 => Self::ElasticInOut,
5 => Self::ElasticIn,
6 => Self::ElasticOut,
7 => Self::BounceInOut,
8 => Self::BounceIn,
9 => Self::BounceOut,
10 => Self::ExponentialInOut,
11 => Self::ExponentialIn,
12 => Self::ExponentialOut,
13 => Self::SineInOut,
14 => Self::SineIn,
15 => Self::SineOut,
16 => Self::BackInOut,
17 => Self::BackIn,
18 => Self::BackOut,
_ => Self::None,
}
}
}
const LIST_ALLOCSIZE: usize = 5;
#[derive(Debug, Clone, PartialEq)]
pub enum GDValue {
Int(i32),
Short(i16),
Float(f64),
Bool(bool),
Toggle(bool),
Group(i16),
Item(i16),
GroupList(smallvec::SmallVec<[i16; LIST_ALLOCSIZE]>),
ProbabilitiesList(smallvec::SmallVec<[(i16, i32); LIST_ALLOCSIZE]>),
SpawnRemapsList(smallvec::SmallVec<[(i16, i16); LIST_ALLOCSIZE]>),
Easing(MoveEasing),
ColourChannel(ColourChannel),
ZLayer(ZLayer),
Events(Vec<Event>),
String(String), }
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum Event {
Unknown = 0,
TinyLanding = 1,
FeatherLanding = 2,
SoftLanding = 3,
NormalLanding = 4,
HardLanding = 5,
HitHead = 6,
OrbTouched = 7,
OrbActivated = 8,
PadActivated = 9,
GravityInverted = 10,
GravityRestored = 11,
NormalJump = 12,
RobotBoostStart = 13,
RobotBoostStop = 14,
UFOJump = 15,
ShipBoostStart = 16,
ShipBoostEnd = 17,
SpiderTeleport = 18,
BallSwitch = 19,
SwingSwitch = 20,
WavePush = 21,
WaveRelease = 22,
DashStart = 23,
DashStop = 24,
Teleported = 25,
PortalNormal = 26,
PortalShip = 27,
PortalBall = 28,
PortalUFO = 29,
PortalWave = 30,
PortalRobot = 31,
PortalSpider = 32,
PortalSwing = 33,
YellowOrb = 34,
PinkOrb = 35,
RedOrb = 36,
GravityOrb = 37,
GreenOrb = 38,
DropOrb = 39,
CustomOrb = 40,
DashOrb = 41,
GravityDashOrb = 42,
SpiderOrb = 43,
TeleportOrb = 44,
YellowPad = 45,
PinkPad = 46,
RedPad = 47,
GravityPad = 48,
SpiderPad = 49,
PortalGravityFlip = 50,
PortalGravityNormal = 51,
PortalGravityInvert = 52,
PoratlFlip = 53,
PortalUnflip = 54,
PortalNormalScale = 55,
PortalMiniScale = 56,
PortalDualOn = 57,
PortalDualOff = 58,
PortalTeleport = 59,
Checkpoint = 60,
DestroyBlock = 61,
UserCoin = 62,
PickupItem = 63,
FallLow = 65,
FallMed = 66,
FallHigh = 67,
FallVHigh = 68,
JumpPush = 69,
JumpRelease = 70,
LeftPush = 71,
LeftRelease = 72,
RightPush = 73,
RightRelease = 74,
PlayerReversed = 75,
CheckpointRespawn = 64, FallSpeedLow = 76,
FallSpeedMed = 77,
FallSpeedHigh = 78,
}
impl From<i32> for Event {
fn from(i: i32) -> Self {
match i {
1 => Self::TinyLanding,
2 => Self::FeatherLanding,
3 => Self::SoftLanding,
4 => Self::NormalLanding,
5 => Self::HardLanding,
6 => Self::HitHead,
7 => Self::OrbTouched,
8 => Self::OrbActivated,
9 => Self::PadActivated,
10 => Self::GravityInverted,
11 => Self::GravityRestored,
12 => Self::NormalJump,
13 => Self::RobotBoostStart,
14 => Self::RobotBoostStop,
15 => Self::UFOJump,
16 => Self::ShipBoostStart,
17 => Self::ShipBoostEnd,
18 => Self::SpiderTeleport,
19 => Self::BallSwitch,
20 => Self::SwingSwitch,
21 => Self::WavePush,
22 => Self::WaveRelease,
23 => Self::DashStart,
24 => Self::DashStop,
25 => Self::Teleported,
26 => Self::PortalNormal,
27 => Self::PortalShip,
28 => Self::PortalBall,
29 => Self::PortalUFO,
30 => Self::PortalWave,
31 => Self::PortalRobot,
32 => Self::PortalSpider,
33 => Self::PortalSwing,
34 => Self::YellowOrb,
35 => Self::PinkOrb,
36 => Self::RedOrb,
37 => Self::GravityOrb,
38 => Self::GreenOrb,
39 => Self::DropOrb,
40 => Self::CustomOrb,
41 => Self::DashOrb,
42 => Self::GravityDashOrb,
43 => Self::SpiderOrb,
44 => Self::TeleportOrb,
45 => Self::YellowPad,
46 => Self::PinkPad,
47 => Self::RedPad,
48 => Self::GravityPad,
49 => Self::SpiderPad,
50 => Self::PortalGravityFlip,
51 => Self::PortalGravityNormal,
52 => Self::PortalGravityInvert,
53 => Self::PoratlFlip,
54 => Self::PortalUnflip,
55 => Self::PortalNormalScale,
56 => Self::PortalMiniScale,
57 => Self::PortalDualOn,
58 => Self::PortalDualOff,
59 => Self::PortalTeleport,
60 => Self::Checkpoint,
61 => Self::DestroyBlock,
62 => Self::UserCoin,
63 => Self::PickupItem,
65 => Self::FallLow,
66 => Self::FallMed,
67 => Self::FallHigh,
68 => Self::FallVHigh,
69 => Self::JumpPush,
70 => Self::JumpRelease,
71 => Self::LeftPush,
72 => Self::LeftRelease,
73 => Self::RightPush,
74 => Self::RightRelease,
75 => Self::PlayerReversed,
64 => Self::CheckpointRespawn,
76 => Self::FallSpeedLow,
77 => Self::FallSpeedMed,
78 => Self::FallSpeedHigh,
_ => Self::Unknown,
}
}
}
macro_rules! parse {
($v:expr => $t:ty) => {
$v.parse::<$t>().unwrap_or_default()
};
}
fn parse_sibling_items<T, S>(s: &str) -> Vec<(T, S)>
where
T: Default + FromStr + Copy + Clone,
S: Default + FromStr + Copy + Clone,
{
let mut curr_group: T = T::default();
let mut idx = 0;
let mut tuples = vec![];
s.split('.').for_each(|c| {
match idx % 2 == 0 {
true => {
curr_group = parse!(c => T)
}
false => {
tuples.push((curr_group, parse!(c => S)));
}
};
idx += 1
});
tuples
}
impl GDValue {
pub fn from(t: GDObjPropType, s: &str) -> Self {
match t {
GDObjPropType::Bool => Self::Bool(s == "1"),
GDObjPropType::Toggle => Self::Toggle(s == "1"),
GDObjPropType::ColourChannel => {
Self::ColourChannel(ColourChannel::from(parse!(s => i16)))
}
GDObjPropType::Easing => Self::Easing(MoveEasing::from(parse!(s => i32))),
GDObjPropType::Float => Self::Float(parse!(s => f64)),
GDObjPropType::Int => Self::Int(parse!(s => i32)),
GDObjPropType::EventsList => Self::Events(
s.split('.')
.map(|i| Event::from(parse!(i => i32)))
.collect(),
),
GDObjPropType::ProbabilitiesList => {
let tuples = parse_sibling_items::<i16, i32>(s);
Self::ProbabilitiesList(SmallVec::from_vec(tuples))
}
GDObjPropType::SpawnRemapsList => {
let tuples = parse_sibling_items::<i16, i16>(s);
Self::SpawnRemapsList(SmallVec::from_vec(tuples))
}
GDObjPropType::Group => Self::Group(parse!(s => i16)),
GDObjPropType::Item => Self::Item(parse!(s => i16)),
GDObjPropType::Text | GDObjPropType::Unknown => Self::String(s.to_owned()),
}
}
#[inline(always)]
pub fn from_group_list(g: Vec<Group>) -> Self {
Self::GroupList(SmallVec::from_vec(g.iter().map(|&g| g.id()).collect()))
}
#[inline(always)]
pub fn parents_group_list(g: Vec<Group>) -> Self {
Self::GroupList(SmallVec::from_vec(
g.iter()
.filter_map(|g| match g {
Group::Parent(p) => Some(*p),
Group::Regular(_) => None,
})
.collect(),
))
}
#[inline(always)]
pub fn from_prob_list(g: Vec<(i16, i32)>) -> Self {
Self::ProbabilitiesList(SmallVec::from_vec(g))
}
#[inline(always)]
pub fn from_spawn_remaps(g: Vec<(i16, i16)>) -> Self {
Self::SpawnRemapsList(SmallVec::from_vec(g))
}
#[inline(always)]
pub fn colour_channel(s: &str) -> Self {
Self::ColourChannel(ColourChannel::from(s.parse().unwrap_or(0)))
}
#[inline(always)]
pub fn zlayer(s: &str) -> Self {
Self::ZLayer(ZLayer::from(s.parse().unwrap_or(0)))
}
}
macro_rules! fmt_intlist {
($vals:expr, $i_buf:expr) => {{
let mut items_str = String::with_capacity($vals.len() * 4);
for (idx, item) in $vals.iter().enumerate() {
if idx != 0 {
items_str.push('.');
}
items_str.push_str($i_buf.format(*item as i32));
}
items_str
}};
}
macro_rules! fmt_inttuples {
($vals:expr, $i_buf:expr) => {{
let mut items_str = String::with_capacity($vals.len() * 8);
for (idx, item) in $vals.iter().enumerate() {
if idx != 0 {
items_str.push('.');
}
items_str.push_str($i_buf.format(item.0));
items_str.push('.');
items_str.push_str($i_buf.format(item.1));
}
items_str
}};
}
impl Display for GDValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut i_buf = itoa::Buffer::new();
let mut d_buf = dtoa::Buffer::new();
match self {
GDValue::Bool(b) => write!(f, "{}", if *b { '1' } else { '0' }),
GDValue::Toggle(b) => write!(
f,
"{}",
match b {
true => "1",
false => "-1",
}
),
GDValue::ColourChannel(v) => write!(f, "{}", i_buf.format(Into::<i16>::into(*v))),
GDValue::Easing(v) => write!(f, "{}", i_buf.format(*v as i32)),
GDValue::Float(v) => write!(f, "{}", d_buf.format(*v)),
GDValue::Group(v) | GDValue::Item(v) => write!(f, "{}", i_buf.format(*v)),
GDValue::GroupList(v) => write!(f, "{}", fmt_intlist!(v, i_buf)),
GDValue::ProbabilitiesList(v) => write!(f, "{}", fmt_inttuples!(v, i_buf)),
GDValue::SpawnRemapsList(v) => write!(f, "{}", fmt_inttuples!(v, i_buf)),
GDValue::Int(v) => write!(f, "{}", i_buf.format(*v)),
GDValue::Short(v) => write!(f, "{}", i_buf.format(*v)),
GDValue::String(v) => write!(f, "{v}"),
GDValue::ZLayer(v) => write!(f, "{}", i_buf.format(*v as i32)),
GDValue::Events(evts) => write!(f, "{}", fmt_intlist!(evts, i_buf)),
}
}
}
const OBJECT_NAMES: &[(i32, &str)] = &[
(1, "Default block"),
(2, "Waffle block floor"),
(3, "Waffle block corner"),
(4, "Waffle block inner corner"),
(5, "Waffle block filler"),
(6, "Waffle block no bottom"),
(7, "Waffle block straight"),
(8, "Spike"),
(9, "Ground spikes"),
(10, "Normal gravity portal"),
(11, "Flipped gravity portal"),
(12, "Cube portal"),
(13, "Ship portal"),
(15, "Pulse pole tall"),
(16, "Pulse pole medium"),
(17, "Pulse pole short"),
(18, "Transparent spikes huge"),
(19, "Transparent spikes big"),
(20, "Transparent spikes medium"),
(21, "Transparent spikes small"),
(22, "No block transition object"),
(23, "Blocks from top transition object"),
(24, "Blocks from bottom transition object"),
(25, "Blocks from left transition object"),
(26, "Blocks from right transition object"),
(27, "Scale in transition object"),
(28, "Scale out transition object"),
(31, "Start pos"),
(32, "Enable player trail"),
(33, "Disable player trail"),
(34, "Solid startpos"),
(35, "Yellow pad"),
(36, "Yellow orb"),
(39, "Small spike"),
(40, "Half block default"),
(41, "Chain tall"),
(45, "Mirror portal reverse"),
(46, "Mirror portal normal"),
(47, "Ball portal"),
(48, "Transparent clouds big"),
(49, "Transparent clouds small"),
(50, "Pulse circle"),
(51, "Pulse ring"),
(52, "Pulse heart"),
(53, "Pulse diamond"),
(54, "Pulse star"),
(55, "Random direction transition object"),
(56, "Away to left transition object"),
(57, "Away to right transition object"),
(58, "Away from middle transition object"),
(59, "Away to middle transition object"),
(60, "Pulse music note"),
(61, "Ground spikes wavy"),
(62, "Wavy block floor"),
(67, "Blue pad"),
(83, "Waffle block"),
(84, "Blue orb"),
(88, "Buzzsaw big"),
(89, "Buzzsaw medium"),
(98, "Buzzsaw small"),
(99, "Size portal normal"),
(101, "Size portal small"),
(111, "UFO portal"),
(140, "Pink pad"),
(141, "Pink orb"),
(200, "Speed portal 0.5x"),
(201, "Speed portal 1x"),
(202, "Speed portal 2x"),
(203, "Speed portal 3x"),
(286, "Dual portal double"),
(287, "Dual portal single"),
(899, "Trigger Colour"),
(901, "Trigger Move"),
(914, "Text object"),
(1006, "Trigger Pulse"),
(1007, "Trigger Alpha"),
(1049, "Trigger Toggle"),
(1268, "Trigger Spawn"),
(1346, "Trigger Rotation"),
(1347, "Trigger Follow"),
(1520, "Trigger Shake"),
(1585, "Trigger Animate"),
(1595, "Trigger Touch"),
(1611, "Trigger Count"),
(1615, "Counter"),
(1616, "Trigger Stop"),
(1812, "Trigger On death"),
(1812, "Trigger follow player y"),
(1815, "Trigger Collision"),
(1816, "Collision block"),
(1818, "BG effect on"),
(1819, "BG effect off"),
(1912, "Trigger Random"),
(1913, "Trigger Camera zoom"),
(1915, "Don't fade + don't enter transition object"),
(1917, "Trigger Reverse gameplay"),
(1932, "Trigger Player control"),
(1934, "Trigger Song"),
(1935, "Trigger Time warp"),
(2016, "Camera guide"),
(2066, "Trigger Gravity"),
(2067, "Trigger Scale"),
(2068, "Trigger Advanced random"),
(2900, "Trigger rotate gameplay"),
(2900, "Trigger Middleground config"),
(3024, "Trigger Area stop"),
(3031, "Trigger Middleground change"),
(3600, "Trigger End"),
(3604, "Trigger Event"),
(3606, "BG speed config"),
(3608, "Trigger Spawn particle"),
(3609, "Trigger Instant collision"),
(3612, "MG speed config"),
(3613, "UI config"),
(3614, "Trigger Time"),
(3615, "Trigger Time event"),
(3617, "Trigger Time control"),
(3618, "Trigger Reset group"),
(3619, "Trigger Item edit"),
(3620, "Trigger Item compare"),
(3640, "Collision state block"),
(3641, "Trigger Persistent item"),
(3643, "Toggle block"),
(3662, "Trigger Link visible"),
];
#[derive(Clone, PartialEq)]
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 group_str = match !self.config.groups.is_empty() {
true => &format!(
" with groups: {}",
self.config
.groups
.iter()
.map(|g| format!("{}", g.id()))
.collect::<Vec<String>>()
.join(", ")
),
false => "",
};
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.iter() {
let desc = lookup::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(s: &str) -> GDObject {
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_property_type(p).unwrap_or(GDObjPropType::Unknown),
value,
),
);
}
pub fn set_property(&mut self, p: u16, val: GDValue) {
if let Some(v) = self.properties.iter_mut().find(|(k, _)| *k == p) {
v.1 = val;
} else {
let new_idx = self.properties.partition_point(|(k, _)| k < &p);
self.properties.insert(new_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) {
self.properties.remove(idx);
}
}
pub fn serialise_to_string(&self) -> String {
let mut properties_string = String::with_capacity(self.properties.len() * 8);
for (idx, val) in self.properties.iter() {
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 raw_str = format!("1,{}{config_str}{properties_string}", self.id);
raw_str.replace("\"", "") + ";"
}
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(always)]
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(always)]
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.clone())),
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
.iter()
.find(|pair| pair.0 == p)
.map(|p| p.1.clone()),
}
}
pub fn set_config(&mut self, config: GDObjConfig) {
self.config = config;
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct TriggerConfig {
pub touchable: bool,
pub spawnable: bool,
pub multitriggerable: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum Group {
Regular(i16),
Parent(i16),
}
impl Ord for Group {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.id().cmp(&other.id()) {
std::cmp::Ordering::Equal => self.get_type().cmp(&other.get_type()),
o => o,
}
}
}
impl PartialOrd for Group {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum GroupType {
Regular,
Parent,
}
impl Ord for GroupType {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self == other {
std::cmp::Ordering::Equal
} else if *self == Self::Regular {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less
}
}
}
impl PartialOrd for GroupType {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Group {
pub fn id(&self) -> i16 {
match self {
Self::Regular(id) => *id,
Self::Parent(id) => *id,
}
}
pub fn get_type(&self) -> GroupType {
match self {
Group::Parent(_) => GroupType::Parent,
Group::Regular(_) => GroupType::Regular,
}
}
}
impl From<i16> for Group {
fn from(value: i16) -> Self {
Self::Regular(value)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct GDObjConfig {
pub pos: (f64, f64),
pub scale: (f64, f64),
pub angle: f64,
pub groups: Vec<Group>,
pub trigger_cfg: TriggerConfig,
pub z_order: i32,
pub z_layer: ZLayer,
pub editor_layers: (i16, i16),
pub colour_channels: (ColourChannel, ColourChannel),
pub enter_effect_channel: i16,
pub material_id: i16,
pub control_id: i16,
pub attributes: GDObjAttributes,
}
impl Default for GDObjConfig {
fn default() -> Self {
GDObjConfig {
pos: (0.0, 0.0),
scale: (1.0, 1.0),
angle: 0.0,
groups: vec![],
trigger_cfg: TriggerConfig {
touchable: false,
spawnable: false,
multitriggerable: false,
},
z_layer: ZLayer::T1,
z_order: 0,
editor_layers: (0, 0),
colour_channels: (ColourChannel::Object, ColourChannel::Channel(1)),
enter_effect_channel: 0,
material_id: 0,
control_id: 0,
attributes: GDObjAttributes::new(),
}
}
}
impl GDObjConfig {
#[inline(always)]
pub fn new() -> Self {
Self::default()
}
pub fn serialise_to_string(&self) -> String {
let mut properties = String::with_capacity(64);
let _ = write!(
properties,
",2,{},3,{}{}",
self.pos.0,
self.pos.1,
self.attributes.get_property_str()
);
serialise_bools(
&[
("11", self.trigger_cfg.touchable),
("62", self.trigger_cfg.spawnable),
("87", self.trigger_cfg.multitriggerable),
],
&mut properties,
);
serialise_fields(
&[
("6", self.angle, 0.0),
("128", self.scale.0, 1.0),
("129", self.scale.1, 1.0),
],
&mut properties,
);
serialise_fields(
&[
("20", self.editor_layers.0, 0),
("61", self.editor_layers.1, 0),
(
"21",
self.colour_channels.0.into(),
ColourChannel::Object.into(),
),
("22", self.colour_channels.1.into(), 1),
("24", self.z_layer as i16, ZLayer::T1 as i16),
("343", self.enter_effect_channel, 0),
("446", self.material_id, 0),
("534", self.control_id, 0),
],
&mut properties,
);
serialise_fields(&[("25", self.z_order, 0)], &mut properties);
if !self.groups.is_empty() {
properties.push_str(",57,");
let group_str = &self
.groups
.iter()
.map(|g| g.id().to_string())
.collect::<Vec<String>>()
.join(".");
properties.push_str(group_str);
};
properties
}
fn dedup_groups(&mut self) {
self.groups.sort();
self.groups.dedup_by(|a, b| a.id() == b.id());
}
#[inline(always)]
pub fn groups<T: IntoIterator<Item = I>, I: Into<Group>>(mut self, groups: T) -> Self {
self.groups = groups.into_iter().map(|g| g.into()).collect();
self.dedup_groups();
self
}
#[inline(always)]
pub fn add_groups<T: AsRef<[Group]>>(&mut self, groups: T) {
self.groups.extend_from_slice(groups.as_ref());
self.dedup_groups();
}
#[inline(always)]
pub fn add_group(&mut self, group: Group) {
self.groups.push(group);
self.dedup_groups();
}
#[inline(always)]
pub fn remove_group(&mut self, group: Group) {
if let Some(idx) = self.groups.iter().position(|&g| g == group) {
self.groups.swap_remove(idx);
}
}
#[inline(always)]
pub fn clear_groups(&mut self) {
self.groups.clear();
}
#[inline(always)]
pub fn x(mut self, x: f64) -> Self {
self.pos.0 = x;
self
}
#[inline(always)]
pub fn y(mut self, y: f64) -> Self {
self.pos.1 = y;
self
}
pub fn translate(mut self, x: f64, y: f64) -> Self {
self.pos.0 += x;
self.pos.1 += y;
self
}
#[inline(always)]
pub fn pos(mut self, x: f64, y: f64) -> Self {
self.pos = (x, y);
self
}
#[inline(always)]
pub fn xscale(mut self, xscale: f64) -> Self {
self.scale.0 = xscale;
self
}
#[inline(always)]
pub fn yscale(mut self, yscale: f64) -> Self {
self.scale.1 = yscale;
self
}
#[inline(always)]
pub fn scale(mut self, x: f64, y: f64) -> Self {
self.scale = (x, y);
self
}
#[inline(always)]
pub fn angle(mut self, angle: f64) -> Self {
self.angle = angle;
self
}
#[inline(always)]
pub fn touchable(mut self, touchable: bool) -> Self {
self.trigger_cfg.touchable = touchable;
self
}
#[inline(always)]
pub fn spawnable(mut self, spawnable: bool) -> Self {
self.trigger_cfg.spawnable = spawnable;
self
}
#[inline(always)]
pub fn multitrigger(mut self, multi: bool) -> Self {
self.trigger_cfg.multitriggerable = multi;
self
}
#[inline(always)]
pub fn set_base_colour(mut self, channel: ColourChannel) -> Self {
self.colour_channels.0 = channel;
self
}
#[inline(always)]
pub fn set_detail_colour(mut self, channel: ColourChannel) -> Self {
self.colour_channels.1 = channel;
self
}
#[inline(always)]
pub fn set_z_layer(mut self, z: ZLayer) -> Self {
self.z_layer = z;
self
}
#[inline(always)]
pub fn set_z_order(mut self, z: i32) -> Self {
self.z_order = z;
self
}
#[inline(always)]
pub fn editor_layer_1(mut self, l: i16) -> Self {
self.editor_layers.0 = l;
self
}
#[inline(always)]
pub fn editor_layer_2(mut self, l: i16) -> Self {
self.editor_layers.1 = l;
self
}
#[inline(always)]
pub fn set_material_id(mut self, material_id: i16) -> Self {
self.material_id = material_id;
self
}
#[inline(always)]
pub fn set_enter_channel(mut self, channel: i16) -> Self {
self.enter_effect_channel = channel;
self
}
#[inline(always)]
pub fn set_control_id(mut self, id: i16) -> Self {
self.control_id = id;
self
}
pub fn get_attribute_flag(&self, flag: GDObjAttributes) -> bool {
self.attributes.contains(flag)
}
pub fn set_attribute_flag(mut self, flag: GDObjAttributes, toggle: bool) -> Self {
self.attributes.set(flag, toggle);
self
}
}
bitflags! {
#[derive(Debug, Clone, PartialEq, Default, Eq, Hash)]
pub struct GDObjAttributes: u32 {
const dont_fade = 1;
const dont_enter = 1 << 1;
const no_effects = 1 << 2;
const is_group_parent = 1 << 3;
const is_area_parent = 1 << 4;
const dont_boost_x = 1 << 5;
const dont_boost_y = 1 << 6;
const high_detail = 1 << 7;
const no_touch = 1 << 8;
const passable = 1 << 9;
const hidden = 1 << 10;
const non_stick_x = 1 << 11;
const non_stick_y = 1 << 12;
const extra_sticky = 1 << 13;
const extended_collision = 1 << 14;
const is_ice_block = 1 << 15;
const grip_slope = 1 << 16;
const no_glow = 1 << 17;
const no_particles = 1 << 18;
const scale_stick = 1 << 19;
const no_audio_scale = 1 << 20;
const single_ptouch = 1 << 21;
const center_effect = 1 << 22;
const reverse = 1 << 23;
}
}
impl GDObjAttributes {
#[inline(always)]
pub fn new() -> Self {
Self::default()
}
pub fn get_property_str(&self) -> String {
let fields = [
(DONT_FADE, Self::dont_fade),
(DONT_ENTER, Self::dont_enter),
(NO_OBJECT_EFFECTS, Self::no_effects),
(IS_GROUP_PARENT, Self::is_group_parent),
(IS_AREA_PARENT, Self::is_area_parent),
(DONT_BOOST_X, Self::dont_boost_x),
(DONT_BOOST_Y, Self::dont_boost_y),
(IS_HIGH_DETAIL, Self::high_detail),
(NO_TOUCH, Self::no_touch),
(PASSABLE, Self::passable),
(HIDDEN, Self::hidden),
(NONSTICK_X, Self::non_stick_x),
(NONSTICK_Y, Self::non_stick_y),
(EXTRA_STICKY, Self::extra_sticky),
(HAS_EXTENDED_COLLISION, Self::extended_collision),
(IS_ICE_BLOCK, Self::is_ice_block),
(GRIP_SLOPE, Self::grip_slope),
(NO_GLOW, Self::no_glow),
(NO_PARTICLES, Self::no_particles),
(SCALE_STICK, Self::scale_stick),
(NO_AUDIO_SCALE, Self::no_audio_scale),
(SINGLE_PLAYER_TOUCH, Self::single_ptouch),
(CENTER_EFFECT, Self::center_effect),
(REVERSES_GAMEPLAY, Self::reverse),
];
let mut properties_str = String::with_capacity(6 * fields.len());
for (id, flag) in fields {
if self.contains(flag) {
let _ = write!(properties_str, ",{id},1");
}
}
properties_str
}
}
fn serialise_fields<T: PartialEq + Display>(fields: &[(&str, T, T)], buf: &mut String) {
for (id, field, default) in fields {
if field != default {
let _ = write!(buf, ",{id},{field}");
}
}
}
fn serialise_bools(fields: &[(&str, bool)], buf: &mut String) {
for (id, field) in fields {
if *field {
let _ = write!(buf, ",{id},1");
}
}
}