1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
use super::{
brush::Brush,
color::Color,
layer::Layer,
novascript::{NovaScript, variable::Variable},
object::Object,
pattern::Pattern,
prefab::Prefab,
theme::Theme,
vec2::Vec2,
};
use crate::{Read, ReadVersioned, Write, error::Error};
use ordered_float::OrderedFloat;
use uuid::Uuid;
/// The level data for an Exoracer level.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct LevelData {
/// The UUID of the level.
pub level_id: Uuid,
/// The version of the level e.g. v1, v2, etc.
pub level_version: i32,
/// Whether this level is for the new level editor.
///
/// If this is true, the level can be opened in the new level editor. Otherwise it's for the "legacy" editor.
/// This Field is presumably only useful in .level files, not in .exolvl ones. A mismatch with the corresponding `LocalLevel` field should be avoided.
pub nova_level: bool,
/// The tile ids for the "under decoration" layer.
pub under_decoration_tiles: Vec<i32>,
/// The tile ids for the "background decoration" layer.
pub background_decoration_tiles: Vec<i32>,
/// The tile ids for the terrain layer.
pub terrain_tiles: Vec<i32>,
/// The tile ids for the floating zone layer.
pub floating_zone_tiles: Vec<i32>,
/// The tile ids for the "object" layer.
pub object_tiles: Vec<i32>,
/// The tile ids for the "foreground decoration" layer.
pub foreground_decoration_tiles: Vec<i32>,
/// The objects in the level.
pub objects: Vec<Object>,
/// The layers in the level.
pub layers: Vec<Layer>,
/// The prefabs in the level.
pub prefabs: Vec<Prefab>,
/// The brushes in the level.
pub brushes: Vec<Brush>,
/// The patterns in the level.
pub patterns: Vec<Pattern>,
/// The color palettes in the level.
///
/// This is only present in levels with version 17 or higher.
pub color_palette: Option<Vec<Color>>,
/// The author medal time for this level in milliseconds.
pub author_time: i64,
/// The author medal lap times for this level in milliseconds.
pub author_lap_times: Vec<i64>,
/// The silver medal time for this level in milliseconds.
pub silver_medal_time: i64,
/// The gold medal time for this level in milliseconds.
pub gold_medal_time: i64,
/// The number of laps in this level.
pub laps: i32,
/// Whether the camera should be centered while playing this level.
///
/// This is mostly deprecated and should stay set to false.
pub center_camera: bool,
/// The scripts in the level.
///
/// These are used in the legacy level editor.
pub scripts: Vec<i32>,
/// The "new" scripts in the level.
///
/// These are the scripts that are used in the new level editor. As opposed to the `scripts` field which is for the legacy editor.
pub nova_scripts: Vec<NovaScript>,
/// All the global variables in the level.
pub global_variables: Vec<Variable>,
/// The theme name of the level.
pub theme: Theme,
/// The custom background color of the level.
pub custom_background_color: Color,
/// Unknown data.
pub(crate) unknown1: [u8; 4],
/// The following terrain related fields are all used when explicitly copying certain terrain data.
///
/// The custom terrain pattern that can be pasted with the `color_paste` button if the recieving object has the `FillMode` set to `Pattern`.
pub custom_terrain_pattern_id: i32,
/// The tiling of that pattern.
pub custom_terrain_pattern_tiling: Vec2,
/// the offset of that pattern.
pub custom_terrain_pattern_offset: Vec2,
/// In the legacy editor: The custom terrain color of the level.
/// In the new editor: The color of the copied terrain.
pub custom_terrain_color: Color,
/// Not 100% sure of the use of this, presumably the replacement for the border color in the new editor.
/// Used when copying and pasting properties of terrain.
pub custom_terrain_secondary_color: Color,
/// The blend mode of the copied terrain.
pub custom_terrain_blend_mode: i32,
/// The custom terrain border color of the level.
pub custom_terrain_border_color: Color,
/// The thickness of the terrain border.
pub custom_terrain_border_thickness: OrderedFloat<f32>,
/// The corner radius of the terrain border.
pub custom_terrain_border_corner_radius: OrderedFloat<f32>,
/// Whether the copied terrain has round reflex angles or not (only visual).
pub custom_terrain_round_reflex_angles: bool,
/// Whether the copied terrain has a round collider or not (not visual).
pub custom_terrain_round_collider: bool,
/// The friction of the copied terrain.
pub custom_terrain_friction: OrderedFloat<f32>,
/// Whether the default music should be played or not.
pub default_music: bool,
/// The music ids for the level. The game randomly picks one of these to play each time.
pub music_ids: Vec<String>,
/// Whether the game lets the player change directions or not.
pub allow_direction_change: bool,
/// Whether replays are disabled or not.
///
/// If this is true, the game won't upload replays on this level.
/// (unless you explicitly upload blank shells from the history section, that only contain the time you set without any replay data. Could be a bug).
pub disable_replays: bool,
/// Whether revive pads are disabled or not.
pub disable_revive_pads: bool,
/// Whether the start animation is disabled or not.
pub disable_start_animation: bool,
/// The gravity vector for this level.
pub gravity: Vec2,
}
impl LevelData {
#[must_use]
pub fn default_with_id(level_id: Uuid) -> Self {
Self {
level_id,
level_version: 1,
nova_level: true,
color_palette: Some(Vec::default()),
laps: 1,
default_music: true,
gravity: Vec2 {
x: OrderedFloat(0.0),
y: OrderedFloat(-75.0),
},
..Default::default()
}
}
}
impl ReadVersioned for LevelData {
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "debug", name = "LevelData::read", skip(input))
)]
fn read(input: &mut impl std::io::Read, version: i32) -> Result<Self, Error> {
Ok(Self {
level_id: Read::read(input)?,
level_version: Read::read(input)?,
nova_level: Read::read(input)?,
under_decoration_tiles: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("under_decoration_tiles");
let _guard = s.enter();
}
Read::read(input)?
},
background_decoration_tiles: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("background_decoration_tiles");
let _guard = s.enter();
}
Read::read(input)?
},
terrain_tiles: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("terrain_tiles");
let _guard = s.enter();
}
Read::read(input)?
},
floating_zone_tiles: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("floating_zone_tiles");
let _guard = s.enter();
}
Read::read(input)?
},
object_tiles: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("object_tiles");
let _guard = s.enter();
}
Read::read(input)?
},
foreground_decoration_tiles: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("foreground_decoration_tiles");
let _guard = s.enter();
}
Read::read(input)?
},
objects: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("objects");
let _guard = s.enter();
}
Read::read(input)?
},
layers: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("layers");
let _guard = s.enter();
}
Read::read(input)?
},
prefabs: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("prefabs");
let _guard = s.enter();
}
Read::read(input)?
},
brushes: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("brushes");
let _guard = s.enter();
}
Read::read(input)?
},
patterns: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("patterns");
let _guard = s.enter();
}
Read::read(input)?
},
color_palette: if version >= 17 {
#[cfg(feature = "tracing")]
{
let s = debug_span!("color_palette");
let _guard = s.enter();
}
Some(Read::read(input)?)
} else {
None
},
author_time: Read::read(input)?,
author_lap_times: Read::read(input)?,
silver_medal_time: Read::read(input)?,
gold_medal_time: Read::read(input)?,
laps: Read::read(input)?,
center_camera: Read::read(input)?,
scripts: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("scripts");
let _guard = s.enter();
}
Read::read(input)?
},
nova_scripts: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("nova_scripts");
let _guard = s.enter();
}
Read::read(input)?
},
global_variables: {
#[cfg(feature = "tracing")]
{
let s = debug_span!("global_variables");
let _guard = s.enter();
}
Read::read(input)?
},
theme: Read::read(input)?,
custom_background_color: Read::read(input)?,
unknown1: Read::read(input)?,
custom_terrain_pattern_id: Read::read(input)?,
custom_terrain_pattern_tiling: Read::read(input)?,
custom_terrain_pattern_offset: Read::read(input)?,
custom_terrain_color: Read::read(input)?,
custom_terrain_secondary_color: Read::read(input)?,
custom_terrain_blend_mode: Read::read(input)?,
custom_terrain_border_color: Read::read(input)?,
custom_terrain_border_thickness: Read::read(input)?,
custom_terrain_border_corner_radius: Read::read(input)?,
custom_terrain_round_reflex_angles: Read::read(input)?,
custom_terrain_round_collider: Read::read(input)?,
custom_terrain_friction: Read::read(input)?,
default_music: Read::read(input)?,
music_ids: Read::read(input)?,
allow_direction_change: Read::read(input)?,
disable_replays: Read::read(input)?,
disable_revive_pads: Read::read(input)?,
disable_start_animation: Read::read(input)?,
gravity: Read::read(input)?,
})
}
}
impl Write for LevelData {
fn write(&self, output: &mut impl std::io::Write) -> Result<(), Error> {
self.level_id.write(output)?;
self.level_version.write(output)?;
self.nova_level.write(output)?;
self.under_decoration_tiles.write(output)?;
self.background_decoration_tiles.write(output)?;
self.terrain_tiles.write(output)?;
self.floating_zone_tiles.write(output)?;
self.object_tiles.write(output)?;
self.foreground_decoration_tiles.write(output)?;
self.objects.write(output)?;
self.layers.write(output)?;
self.prefabs.write(output)?;
self.brushes.write(output)?;
self.patterns.write(output)?;
if let Some(color_palette) = &self.color_palette {
color_palette.write(output)?;
}
self.author_time.write(output)?;
self.author_lap_times.write(output)?;
self.silver_medal_time.write(output)?;
self.gold_medal_time.write(output)?;
self.laps.write(output)?;
self.center_camera.write(output)?;
self.scripts.write(output)?;
self.nova_scripts.write(output)?;
self.global_variables.write(output)?;
self.theme.write(output)?;
self.custom_background_color.write(output)?;
self.unknown1.write(output)?;
self.custom_terrain_pattern_id.write(output)?;
self.custom_terrain_pattern_tiling.write(output)?;
self.custom_terrain_pattern_offset.write(output)?;
self.custom_terrain_color.write(output)?;
self.custom_terrain_secondary_color.write(output)?;
self.custom_terrain_blend_mode.write(output)?;
self.custom_terrain_border_color.write(output)?;
self.custom_terrain_border_thickness.write(output)?;
self.custom_terrain_border_corner_radius.write(output)?;
self.custom_terrain_round_reflex_angles.write(output)?;
self.custom_terrain_round_collider.write(output)?;
self.custom_terrain_friction.write(output)?;
self.default_music.write(output)?;
self.music_ids.write(output)?;
self.allow_direction_change.write(output)?;
self.disable_replays.write(output)?;
self.disable_revive_pads.write(output)?;
self.disable_start_animation.write(output)?;
self.gravity.write(output)
}
}