1pub mod entity;
2pub mod tileset;
3
4use std::{
5 collections::{BTreeMap, HashMap},
6 fs::File,
7 marker::PhantomData,
8 ops::{BitOr, Sub},
9 path::Path,
10};
11
12use anyhow::{anyhow, ensure, Context, Result};
13use celesteloader::{
14 archive::ModArchive,
15 atlas::Sprite,
16 map::{utils::parse_map_name, Bounds, Decal, Map, Pos, Room},
17 CelesteInstallation,
18};
19use tiny_skia::{
20 BlendMode, Color, IntSize, Paint, PathBuilder, Pattern, Pixmap, PixmapRef,
21 PremultipliedColorU8, Rect, Shader, Stroke, Transform,
22};
23use tracing::instrument;
24
25use crate::asset::{AssetDb, LookupAsset, SpriteLocation};
26
27use self::tileset::{tiles_to_matrix, tiles_to_matrix_scenery, Matrix, ParsedTileset};
28
29#[derive(Clone, Copy)]
30pub struct Layer(u8);
31impl Layer {
32 pub const NONE: Layer = Layer(0b00000000);
33 pub const ALL: Layer = Layer(0b00111111);
34 pub const TILES_BG: Layer = Layer(1 << 0);
35 pub const DECALS_BG: Layer = Layer(1 << 1);
36 pub const ENTITIES: Layer = Layer(1 << 2);
37 pub const TILES_FG: Layer = Layer(1 << 3);
38 pub const DECALS_FG: Layer = Layer(1 << 4);
39 pub const TRIGGERS: Layer = Layer(1 << 5);
40
41 pub fn has(self, other: Layer) -> bool {
42 self.0 & other.0 == other.0
43 }
44}
45impl BitOr for Layer {
46 type Output = Layer;
47
48 fn bitor(self, rhs: Self) -> Self::Output {
49 Layer(self.0.bitor(rhs.0))
50 }
51}
52impl Sub for Layer {
53 type Output = Layer;
54
55 fn sub(self, rhs: Self) -> Self::Output {
56 Layer(self.0 & !rhs.0)
57 }
58}
59
60pub struct MapTileset {
61 pub tileset_fg: HashMap<char, ParsedTileset>,
62 pub tileset_bg: HashMap<char, ParsedTileset>,
63}
64
65impl MapTileset {
66 pub fn vanilla(celeste: &CelesteInstallation) -> Result<Self> {
67 let fgtiles_xml = celeste.read_to_string("Content/Graphics/ForegroundTiles.xml")?;
68 let bgtiles_xml = celeste.read_to_string("Content/Graphics/BackgroundTiles.xml")?;
69 Self::parse(&fgtiles_xml, &bgtiles_xml)
70 }
71
72 pub fn parse(fgtiles_xml: &str, bgtiles_xml: &str) -> Result<Self> {
73 let tileset_fg =
74 celesteloader::tileset::parse_tilesets(fgtiles_xml).context("error parsing fgtiles")?;
75 let tileset_bg =
76 celesteloader::tileset::parse_tilesets(bgtiles_xml).context("error parsing bgtiles")?;
77
78 Ok(MapTileset {
79 tileset_fg: ParsedTileset::parse(&tileset_fg)?,
80 tileset_bg: ParsedTileset::parse(&tileset_bg)?,
81 })
82 }
83}
84
85pub struct CelesteRenderData {
86 pub gameplay_sprites: HashMap<String, Sprite>,
87 pub map_tileset: MapTileset,
88 pub gameplay_atlas: Pixmap,
89 pub scenery: Sprite,
90}
91
92impl CelesteRenderData {
93 pub fn for_map(
94 celeste: &CelesteInstallation,
95 archive: &mut ModArchive,
96 map: &Map,
97 ) -> Result<Self> {
98 let mut base = CelesteRenderData::base(celeste)?;
99 base.load_map_tileset(celeste, archive, map)?;
100 Ok(base)
101 }
102
103 pub fn vanilla(celeste: &CelesteInstallation) -> Result<Self> {
104 let mut base = CelesteRenderData::base(celeste)?;
105 base.map_tileset = MapTileset::vanilla(celeste)?;
106 Ok(base)
107 }
108
109 pub fn base(celeste: &CelesteInstallation) -> Result<Self> {
110 let gameplay_atlas_meta = celeste.gameplay_atlas()?;
111 let gameplay_atlas_image = celeste.decode_atlas_image(&gameplay_atlas_meta)?;
112 let gameplay_atlas = Pixmap::from_vec(
113 gameplay_atlas_image.2,
114 IntSize::from_wh(gameplay_atlas_image.0, gameplay_atlas_image.1).expect("atlas size"),
115 )
116 .expect("atlas size");
117
118 let scenery = gameplay_atlas_meta
119 .sprites
120 .iter()
121 .find(|i| i.path == "tilesets/scenery")
122 .expect("no scenery sprite")
123 .clone();
124
125 let gameplay_sprites = gameplay_atlas_meta
126 .sprites
127 .into_iter()
128 .map(|sprite| (sprite.path.clone(), sprite))
129 .collect::<HashMap<_, _>>();
130
131 Ok(CelesteRenderData {
132 map_tileset: MapTileset {
133 tileset_fg: HashMap::new(),
134 tileset_bg: HashMap::new(),
135 },
136 gameplay_atlas,
137 gameplay_sprites,
138 scenery,
139 })
140 }
141
142 pub fn load_map_tileset(
143 &mut self,
144 celeste: &CelesteInstallation,
145 archive: &mut ModArchive,
146 map: &Map,
147 ) -> Result<()> {
148 let (fgtiles, bgtiles) = archive.map_fgtiles_bgtiles(map)?;
149
150 let fgtiles = match fgtiles {
151 Some(fgtiles) => fgtiles,
152 None => celeste.read_to_string("Content/Graphics/ForegroundTiles.xml")?,
153 };
154 let bgtiles = match bgtiles {
155 Some(bgtiles) => bgtiles,
156 None => celeste.read_to_string("Content/Graphics/BackgroundTiles.xml")?,
157 };
158
159 self.map_tileset = MapTileset::parse(&fgtiles, &bgtiles)?;
160
161 Ok(())
162 }
163}
164
165pub struct RenderResult {
166 pub image: Pixmap,
167 pub bounds: Bounds,
168 pub unknown_entities: BTreeMap<String, u32>,
169}
170impl RenderResult {
171 #[tracing::instrument(skip_all, fields(path = path.as_ref().to_str().unwrap_or("")))]
173 pub fn save_png(
174 &mut self,
175 path: impl AsRef<Path>,
176 compression: png::Compression,
177 ) -> Result<(), png::EncodingError> {
178 let file = File::create(path)?;
179 self.encode_png(file, compression)
180 }
181
182 pub fn encode_png(
183 &mut self,
184 w: impl std::io::Write,
185 compression: png::Compression,
186 ) -> Result<(), png::EncodingError> {
187 let mut image = std::mem::replace(&mut self.image, Pixmap::new(1, 1).unwrap());
188
189 for pixel in image.pixels_mut() {
190 let c = pixel.demultiply();
191 *pixel = unsafe {
193 PremultipliedColorU8::from_rgba(c.red(), c.green(), c.blue(), c.alpha())
194 .unwrap_unchecked()
195 };
196 }
197
198 let mut encoder = png::Encoder::new(w, image.width(), image.height());
199 encoder.set_color(png::ColorType::Rgba);
200 encoder.set_depth(png::BitDepth::Eight);
201 encoder.set_compression(compression);
202 encoder.set_filter(png::Filter::Adaptive);
203 let mut writer = encoder.write_header()?;
204 writer.write_image_data(image.data())?;
205
206 Ok(())
207 }
208}
209
210pub struct RenderMapSettings<'a> {
211 pub layer: Layer,
212 pub include_room: &'a dyn Fn(&Room) -> bool,
213 pub status_update: &'a dyn Fn(usize, usize),
214}
215impl<'a> Default for RenderMapSettings<'a> {
216 fn default() -> Self {
217 Self {
218 layer: Layer::ALL,
219 include_room: &|_| true,
220 status_update: &|_, _| {},
221 }
222 }
223}
224impl<'a> RenderMapSettings<'a> {
225 pub fn include_room(self, f: &'a dyn Fn(&Room) -> bool) -> Self {
226 RenderMapSettings {
227 layer: self.layer,
228 include_room: f,
229 status_update: self.status_update,
230 }
231 }
232
233 pub fn status_update(self, f: &'a dyn Fn(usize, usize)) -> Self {
234 RenderMapSettings {
235 layer: self.layer,
236 include_room: self.include_room,
237 status_update: f,
238 }
239 }
240}
241
242#[instrument(skip_all, fields(name = map.package))]
243pub fn render<L: LookupAsset>(
244 render_data: &CelesteRenderData,
245 asset_db: &mut AssetDb<L>,
246 map: &Map,
247 settings: RenderMapSettings,
248) -> Result<RenderResult> {
249 fastrand::seed(2);
250
251 let parsed_map_name = parse_map_name(&map.package);
252
253 let mut map_bounds = Bounds::empty();
254 let mut rooms = Vec::new();
255 for room in &map.rooms {
256 if (settings.include_room)(room) {
257 map_bounds = map_bounds.join(room.bounds);
258 rooms.push(room);
259 }
260 }
261
262 ensure!(!rooms.is_empty(), "No rooms to render");
263
264 let pixmap = {
265 let size_pixels = map_bounds.size.0 as usize * map_bounds.size.1 as usize;
266
267 let data = {
268 let _span = tracing::info_span!("allocate_pixmap").entered();
269 allocate_data(size_pixels, [50, 50, 50, 255]).map_err(|_| {
270 anyhow!(
271 "could not allocate {:.02}GiB",
272 size_pixels as f32 * 4.0 / (1024.0 * 1024.0 * 1024.0)
273 )
274 })?
275 };
276
277 Pixmap::from_vec(
278 data,
279 IntSize::from_wh(map_bounds.size.0, map_bounds.size.1).unwrap(),
280 )
281 .context("failed to create pixmap")?
282 };
283
284 let mut cx = RenderContext {
285 map_bounds,
286 pixmap,
287 unknown_entities: Default::default(),
288 area_id: parsed_map_name.order,
289 _marker: PhantomData::<L>,
290 };
291 if parsed_map_name.name == "LostLevels" {
292 cx.area_id = Some(10);
293 }
294
295 for (i, room) in rooms.iter().enumerate() {
296 (settings.status_update)(i, rooms.len());
297 cx.render_room(room, render_data, asset_db, settings.layer)?;
298 }
299
300 Ok(RenderResult {
301 image: cx.pixmap,
302 bounds: map_bounds,
303 unknown_entities: cx.unknown_entities,
304 })
305}
306
307struct RenderContext<L> {
308 map_bounds: Bounds,
309 pixmap: Pixmap,
310 unknown_entities: BTreeMap<String, u32>,
311 area_id: Option<u32>,
312 _marker: PhantomData<L>,
313}
314
315impl<L: LookupAsset> RenderContext<L> {
316 fn transform_pos(&self, pos: Pos) -> (i32, i32) {
318 let top_left = self.map_bounds.position;
319 (pos.x - top_left.x, pos.y - top_left.y)
320 }
321 fn transform_pos_f32(&self, (x, y): (f32, f32)) -> (f32, f32) {
322 let top_left = self.map_bounds.position;
323 (x - top_left.x as f32, y - top_left.y as f32)
324 }
325
326 fn transform_bounds(&self, bounds: Bounds) -> Rect {
328 let pos = self.transform_pos(bounds.position);
329 Rect::from_xywh(
330 pos.0 as f32,
331 pos.1 as f32,
332 bounds.size.0 as f32,
333 bounds.size.1 as f32,
334 )
335 .unwrap()
336 }
337}
338
339struct SpriteDesc {
340 scale: (f32, f32),
341 justify: (f32, f32),
342 quad: Option<(i16, i16, i16, i16)>,
343 tint: Option<Color>,
344 rotation: f32,
345}
346
347impl Default for SpriteDesc {
348 fn default() -> Self {
349 Self {
350 justify: (0.5, 0.5),
351 scale: (1.0, 1.0),
352 quad: None,
353 tint: None,
354 rotation: 0.0,
355 }
356 }
357}
358
359impl<L: LookupAsset> RenderContext<L> {
360 pub fn circle(&mut self, pos: (f32, f32), radius: f32, color: Color) {
361 let (x, y) = self.transform_pos_f32(pos);
362
363 let mut pb = PathBuilder::new();
364 pb.push_circle(x, y, radius);
365
366 self.pixmap.stroke_path(
367 &pb.finish().unwrap(),
368 &Paint {
369 shader: tiny_skia::Shader::SolidColor(color),
370 anti_alias: false,
371 blend_mode: tiny_skia::BlendMode::Plus,
372
373 ..Default::default()
374 },
375 &Stroke::default(),
376 Transform::identity(),
377 None,
378 );
379 }
380
381 fn rect_inset(&mut self, inset: f32, map_pos: (f32, f32), size: (f32, f32), color: Color) {
382 let (rect_x, rect_y) = self.transform_pos_f32((map_pos.0 + inset, map_pos.1 + inset));
383 self.rect(
384 Rect::from_xywh(rect_x, rect_y, size.0 - (2. * inset), size.1 - (2. * inset)).unwrap(),
385 color,
386 BlendMode::SourceOver,
387 );
388 }
389
390 fn rect(&mut self, rect: Rect, color: Color, blend_mode: BlendMode) {
391 self.pixmap.fill_rect(
392 rect,
393 &Paint {
394 shader: tiny_skia::Shader::SolidColor(color),
395 anti_alias: false,
396 blend_mode,
397
398 ..Default::default()
399 },
400 Transform::identity(),
401 None,
402 );
403 }
404 fn stroke_rect(&mut self, rect: Rect, color: Color) {
405 let rect = Rect::from_ltrb(
406 rect.left(),
407 rect.top(),
408 rect.right() - 1.0,
409 rect.bottom() - 1.0,
410 )
411 .unwrap_or(rect);
412
413 let mut pb = PathBuilder::new();
414 pb.push_rect(rect);
415
416 self.pixmap.stroke_path(
417 &pb.finish().unwrap(),
418 &Paint {
419 shader: tiny_skia::Shader::SolidColor(color),
420 anti_alias: false,
421 ..Default::default()
422 },
423 &Stroke::default(),
424 Transform::identity(),
425 None,
426 );
427 }
428
429 fn sprite(
430 &mut self,
431 cx: &CelesteRenderData,
432 map_pos: (f32, f32),
433 sprite: SpriteLocation,
434 desc: SpriteDesc,
435 ) -> Result<()> {
436 let SpriteDesc {
437 scale,
438 justify,
439 quad,
440 tint,
441 rotation,
442 } = desc;
443
444 let (x, y) = self.transform_pos_f32(map_pos);
445
446 let (
447 mut real_w,
448 mut real_h,
449 mut sprite_w,
450 mut sprite_h,
451 sprite_offset_x,
452 sprite_offset_y,
453 atlas,
454 ) = match &sprite {
455 SpriteLocation::Atlas(sprite) => (
456 sprite.real_w,
457 sprite.real_h,
458 sprite.w,
459 sprite.h,
460 sprite.offset_x,
461 sprite.offset_y,
462 cx.gameplay_atlas.as_ref(),
463 ),
464 SpriteLocation::Raw(pixmap) => (
465 pixmap.width() as i16,
466 pixmap.height() as i16,
467 pixmap.width() as i16,
468 pixmap.height() as i16,
469 0,
470 0,
471 pixmap.as_ref(),
472 ),
473 };
474
475 let (quad_x, quad_y) = if let Some((quad_x, quad_y, quad_w, quad_h)) = quad {
476 real_w = quad_w;
477 sprite_w = quad_w;
478 real_h = quad_h;
479 sprite_h = quad_h;
480
481 (quad_x, quad_y)
482 } else {
483 (0, 0)
484 };
485
486 let justify_offset_x = (real_w as f32 * justify.0 + sprite_offset_x as f32) * scale.0;
487 let justify_offset_y = (real_h as f32 * justify.1 + sprite_offset_y as f32) * scale.1;
488 let draw_x = (x - justify_offset_x).floor();
489 let draw_y = (y - justify_offset_y).floor();
490
491 let pattern_transform = match sprite {
492 SpriteLocation::Atlas(sprite) => Transform::from_translate(
493 draw_x - sprite.x as f32 - quad_x as f32,
494 draw_y - sprite.y as f32 - quad_y as f32,
495 ),
496 SpriteLocation::Raw(_) => Transform::from_translate(draw_x, draw_y),
497 };
498
499 let scale_transform = Transform::from_translate(-draw_x, -draw_y)
500 .post_translate(-justify_offset_x, -justify_offset_y)
501 .post_rotate(rotation.to_degrees())
502 .post_translate(justify_offset_x, justify_offset_y)
503 .post_scale(scale.0, scale.1)
504 .post_translate(draw_x, draw_y);
505
506 let rect = Rect::from_xywh(draw_x, draw_y, sprite_w as f32, sprite_h as f32).unwrap();
507
508 self.pixmap.fill_rect(
509 rect,
510 &Paint {
511 shader: Pattern::new(
512 atlas,
513 tiny_skia::SpreadMode::Pad,
514 tiny_skia::FilterQuality::Nearest,
515 1.0,
516 pattern_transform,
517 ),
518 anti_alias: false,
519 ..Default::default()
520 },
521 scale_transform,
522 None,
523 );
524
525 if let Some(tint) = tint {
527 self.pixmap.fill_rect(
528 rect,
529 &Paint {
530 shader: Shader::SolidColor(tint),
531 blend_mode: tiny_skia::BlendMode::Multiply,
532 anti_alias: false,
533 ..Default::default()
534 },
535 scale_transform,
536 None,
537 );
538 }
539
540 Ok(())
541 }
542
543 fn tile_sprite(
544 &mut self,
545 atlas: PixmapRef,
546 pos: Pos,
547 atlas_position: (i16, i16),
548 tint: Option<Color>,
549 ) {
550 let (x, y) = self.transform_pos(pos);
551
552 let rect = Rect::from_xywh(x as f32, y as f32, 8.0, 8.0).unwrap();
553
554 let pattern_transform = Transform::from_translate(
555 (x - atlas_position.0 as i32) as f32,
556 (y - atlas_position.1 as i32) as f32,
557 );
558 self.pixmap.fill_rect(
559 rect,
560 &Paint {
561 shader: Pattern::new(
562 atlas,
563 tiny_skia::SpreadMode::Pad,
564 tiny_skia::FilterQuality::Nearest,
565 1.0,
566 pattern_transform,
567 ),
568 anti_alias: false,
569 ..Default::default()
570 },
571 Transform::identity(),
572 None,
573 );
574
575 if let Some(tint) = tint {
576 self.pixmap.fill_rect(
577 rect,
578 &Paint {
579 shader: Shader::SolidColor(tint),
580 blend_mode: tiny_skia::BlendMode::Multiply,
581 anti_alias: false,
582 ..Default::default()
583 },
584 Transform::identity(),
585 None,
586 );
587 }
588 }
589
590 #[instrument(skip_all, fields(name = room.name))]
591 fn render_room(
592 &mut self,
593 room: &Room,
594 cx: &CelesteRenderData,
595 asset_db: &mut AssetDb<L>,
596 layer: Layer,
597 ) -> Result<()> {
598 if false {
599 let mut pb = tiny_skia::PathBuilder::new();
600 pb.push_rect(self.transform_bounds(room.bounds));
601 let path = pb.finish().unwrap();
602 self.pixmap.stroke_path(
603 &path,
604 &Paint::default(),
605 &tiny_skia::Stroke::default(),
606 Transform::identity(),
607 None,
608 );
609 }
610
611 let bgtiles = tiles_to_matrix(room.bounds.size_tiles(), &room.bg_tiles_raw)?;
612 let fgtiles = tiles_to_matrix(room.bounds.size_tiles(), &room.fg_tiles_raw)?;
613
614 if layer.has(Layer::TILES_BG) {
615 self.render_tileset(room, &bgtiles, &cx.map_tileset.tileset_bg, cx, asset_db)?;
616 self.render_tileset_scenery(room, &room.scenery_bg_raw, cx)?;
617 }
618 if layer.has(Layer::DECALS_BG) {
619 self.render_decals(room, &room.decals_bg, cx, asset_db)?;
620 }
621 if layer.has(Layer::ENTITIES) {
622 self.render_entities(room, &fgtiles, cx, asset_db)?;
624 }
625 if layer.has(Layer::TILES_FG) {
626 self.render_tileset(room, &fgtiles, &cx.map_tileset.tileset_fg, cx, asset_db)?;
627 self.render_tileset_scenery(room, &room.scenery_fg_raw, cx)?;
628 }
629 if layer.has(Layer::DECALS_FG) {
630 self.render_decals(room, &room.decals_fg, cx, asset_db)?;
631 }
632 if layer.has(Layer::TRIGGERS) {
633 }
635
636 Ok(())
637 }
638
639 fn render_tileset(
640 &mut self,
641 room: &Room,
642 tiles: &Matrix<char>,
643 tilesets: &HashMap<char, ParsedTileset>,
644 cx: &CelesteRenderData,
645 asset_db: &mut AssetDb<L>,
646 ) -> Result<()> {
647 let tile_pos = room.bounds.position;
648 self.render_tileset_inner(
649 room.bounds.size_tiles(),
650 tile_pos,
651 tiles,
652 tilesets,
653 cx,
654 asset_db,
655 )
656 }
657
658 #[instrument(skip_all)]
659 fn render_tileset_inner(
660 &mut self,
661 size: (u32, u32),
662 tile_pos: Pos,
663 tiles: &Matrix<char>,
664 tilesets: &HashMap<char, ParsedTileset>,
665 cx: &CelesteRenderData,
666 asset_db: &mut AssetDb<L>,
667 ) -> Result<()> {
668 let (w, h) = size;
669
670 for x in 0..w {
671 for y in 0..h {
672 let c = tiles.get(x, y);
673
674 if c == '0' {
675 continue;
676 }
677
678 let tileset = tilesets
679 .get(&c)
680 .ok_or_else(|| anyhow!("tileset for '{}' not found", c))?;
681
682 let random_tiles = tileset::choose_tile(tileset, x, y, tiles)?.unwrap();
683 let sprite_tile_offset = fastrand::choice(random_tiles).unwrap();
684
685 let sprite = asset_db.lookup_gameplay(cx, &format!("tilesets/{}", tileset.path))?;
686
687 let (sprite_x, sprite_y, sprite_offset_x, sprite_offset_y, atlas) = match &sprite {
688 SpriteLocation::Atlas(sprite) => (
689 sprite.x,
690 sprite.y,
691 sprite.offset_x,
692 sprite.offset_y,
693 cx.gameplay_atlas.as_ref(),
694 ),
695 SpriteLocation::Raw(pixmap) => {
696 (0, 0, 0, 0, pixmap.as_ref())
698 }
699 };
700
701 let sprite_pos = (
702 sprite_x + sprite_tile_offset.0 as i16 * 8,
703 sprite_y + sprite_tile_offset.1 as i16 * 8,
704 );
705
706 if sprite_offset_x != 0 {
707 panic!();
708 }
709 if sprite_offset_y != 0 {
710 panic!();
711 }
712
713 self.tile_sprite(
714 atlas,
715 tile_pos.offset_tile(x as i32, y as i32),
716 sprite_pos,
717 None,
718 );
719 }
720 }
721
722 Ok(())
723 }
724
725 #[instrument(skip_all)]
726 fn render_tileset_scenery(
727 &mut self,
728 room: &Room,
729 tiles: &str,
730 cx: &CelesteRenderData,
731 ) -> Result<()> {
732 let (w, h) = room.bounds.size_tiles();
733
734 let matrix = tiles_to_matrix_scenery(room.bounds.size_tiles(), tiles);
735
736 for x in 0..w {
737 for y in 0..h {
738 let index = matrix.get(x, y);
739
740 if index == -1 {
741 continue;
742 }
743
744 let scenery_width = cx.scenery.real_w / 8;
745 let _scenery_height = cx.scenery.real_h / 8;
746 let quad_x = index % scenery_width;
747 let quad_y = index / scenery_width;
748
749 let sprite_x = cx.scenery.x - cx.scenery.offset_x + quad_x * 8;
750 let sprite_y = cx.scenery.y - cx.scenery.offset_y + quad_y * 8;
751 let _w = 8;
752 let _h = 8;
753
754 let tile_pos = room.bounds.position.offset_tile(x as i32, y as i32);
755 self.tile_sprite(
756 cx.gameplay_atlas.as_ref(),
757 tile_pos,
758 (sprite_x, sprite_y),
759 None,
760 );
761 }
762 }
763
764 Ok(())
765 }
766
767 #[instrument(skip_all)]
768 fn render_decals(
769 &mut self,
770 room: &Room,
771 decals: &[Decal],
772 cx: &CelesteRenderData,
773 asset_db: &mut AssetDb<L>,
774 ) -> Result<()> {
775 for decal in decals {
776 let map_pos = (
777 room.bounds.position.x as f32 + decal.x,
778 room.bounds.position.y as f32 + decal.y,
779 );
780
781 let sprite = asset_db.lookup_gameplay(cx, &format!("decals/{}", decal.texture))?;
782 self.sprite(
783 cx,
784 map_pos,
785 sprite,
786 SpriteDesc {
787 scale: (decal.scale_x, decal.scale_y),
788 ..Default::default()
789 },
790 )?;
791 }
792
793 Ok(())
794 }
795
796 fn render_entities(
797 &mut self,
798 room: &Room,
799 fgtiles: &Matrix<char>,
800 cx: &CelesteRenderData,
801 asset_db: &mut AssetDb<L>,
802 ) -> Result<()> {
803 {
804 let _span = tracing::info_span!("render_entities_pre").entered();
805 for e in &room.entities {
806 entity::pre_render_entity(self, cx, asset_db, room, e)?;
807 }
808 }
809 {
810 let _span = tracing::info_span!("render_entities").entered();
811 for e in &room.entities {
812 if !entity::render_entity(self, fgtiles, cx, asset_db, room, e)
813 .with_context(|| format!("couldn't render entity {}", e.name))?
814 {
815 *self.unknown_entities.entry(e.name.clone()).or_default() += 1;
816 }
817 }
818 }
819
820 Ok(())
821 }
822}
823
824pub fn allocate_data(
825 size_pixels: usize,
826 default_color_premultiplied: [u8; 4],
827) -> Result<Vec<u8>, ()> {
828 use std::alloc::{alloc, Layout};
829
830 let size_bytes = size_pixels * 4;
831
832 assert!(size_bytes > 0);
833 unsafe {
834 let allocation = alloc(Layout::from_size_align(size_bytes, 4).unwrap()) as *mut [u8; 4];
836 if allocation.is_null() {
837 return Err(());
838 }
839
840 for i in 0..size_pixels {
841 allocation.add(i).write(default_color_premultiplied);
843 }
844
845 Ok(Vec::from_raw_parts(
847 allocation.cast::<u8>(),
848 size_bytes,
849 size_bytes,
850 ))
851 }
852}