Skip to main content

rustapi/
minimap.rs

1use crate::prelude::*;
2use rusterix::{Surface, ValueContainer};
3use std::collections::hash_map::DefaultHasher;
4use vek::Vec2;
5
6use crate::editor::{PALETTE, RUSTERIX, SIDEBARMODE};
7use std::hash::{Hash, Hasher};
8
9pub static MINIMAPBUFFER: LazyLock<RwLock<TheRGBABuffer>> =
10    LazyLock::new(|| RwLock::new(TheRGBABuffer::default()));
11
12pub static MINIMAPBOX: LazyLock<RwLock<Vec4<f32>>> = LazyLock::new(|| RwLock::new(Vec4::one()));
13pub static MINIMAPCACHEKEY: LazyLock<RwLock<u64>> = LazyLock::new(|| RwLock::new(0));
14
15fn minimap_context_key(server_ctx: &ServerContext) -> u64 {
16    let mut hasher = DefaultHasher::default();
17    match server_ctx.get_map_context() {
18        MapContext::Region => 0_u8.hash(&mut hasher),
19        MapContext::Screen => 1_u8.hash(&mut hasher),
20        MapContext::Character => 2_u8.hash(&mut hasher),
21        MapContext::Item => 3_u8.hash(&mut hasher),
22    }
23    server_ctx.curr_region.hash(&mut hasher);
24    server_ctx.curr_screen.hash(&mut hasher);
25    if let Some(surface) = &server_ctx.editing_surface {
26        surface.id.hash(&mut hasher);
27    }
28    hasher.finish()
29}
30
31pub fn minimap_bbox_for_map(map: &Map) -> Option<Vec4<f32>> {
32    let mut bbox = map.bounding_box()?;
33    if let Some(tbbox) = map.terrain.compute_bounds() {
34        let bbox_min = Vec2::new(bbox.x, bbox.y);
35        let bbox_max = bbox_min + Vec2::new(bbox.z, bbox.w);
36
37        let new_min = bbox_min.map2(tbbox.min, f32::min);
38        let new_max = bbox_max.map2(tbbox.max, f32::max);
39
40        bbox.x = new_min.x;
41        bbox.y = new_min.y;
42        bbox.z = new_max.x - new_min.x;
43        bbox.w = new_max.y - new_min.y;
44    }
45
46    bbox.x -= 0.5;
47    bbox.y -= 0.5;
48    bbox.z += 1.0;
49    bbox.w += 1.0;
50    Some(bbox)
51}
52
53fn surface_uv_outline(surface: &Surface) -> Option<Vec<Vec2<f32>>> {
54    if surface.world_vertices.len() < 2 {
55        return None;
56    }
57    let mut points = Vec::with_capacity(surface.world_vertices.len());
58    for p in &surface.world_vertices {
59        let mut uv = surface.world_to_uv(*p);
60        uv.y = -uv.y;
61        points.push(uv);
62    }
63    Some(points)
64}
65
66fn minimap_bbox_for_surface(surface: &Surface) -> Option<Vec4<f32>> {
67    let points = surface_uv_outline(surface)?;
68    let mut min = points[0];
69    let mut max = points[0];
70    for p in points.iter().skip(1) {
71        min = min.map2(*p, f32::min);
72        max = max.map2(*p, f32::max);
73    }
74    let mut bbox = Vec4::new(min.x, min.y, max.x - min.x, max.y - min.y);
75    bbox.x -= 0.5;
76    bbox.y -= 0.5;
77    bbox.z += 1.0;
78    bbox.w += 1.0;
79    Some(bbox)
80}
81
82fn draw_surface_outline_on_minimap(buffer: &mut TheRGBABuffer, surface: &Surface, bbox: Vec4<f32>) {
83    let Some(points) = surface_uv_outline(surface) else {
84        return;
85    };
86    if points.len() < 2 {
87        return;
88    }
89    let dim = *buffer.dim();
90    let render_dim = Vec2::new(dim.width as f32, dim.height as f32);
91    let line_color = [235, 235, 235, 255];
92    for i in 0..points.len() {
93        let p0 = world_to_minimap_pixel(points[i], render_dim, bbox);
94        let p1 = world_to_minimap_pixel(points[(i + 1) % points.len()], render_dim, bbox);
95        buffer.draw_line(
96            p0.x.round() as i32,
97            p0.y.round() as i32,
98            p1.x.round() as i32,
99            p1.y.round() as i32,
100            line_color,
101        );
102    }
103}
104
105pub fn draw_camera_marker(
106    map: &Map,
107    region: Option<&Region>,
108    buffer: &mut TheRGBABuffer,
109    server_ctx: &ServerContext,
110) {
111    let camera_pos = if server_ctx.editor_view_mode == EditorViewMode::D2 || region.is_none() {
112        // In 2D (region/screen/profile), marker follows the current map view center.
113        Vec2::new(-map.offset.x / map.grid_size, map.offset.y / map.grid_size)
114    } else if let Some(region) = region {
115        // In 3D region mode, marker follows the active 3D editing anchor.
116        Vec2::new(region.editing_position_3d.x, region.editing_position_3d.z)
117    } else {
118        Vec2::zero()
119    };
120
121    let dim = *buffer.dim();
122    let bbox = *MINIMAPBOX.read().unwrap();
123
124    let pos = world_to_minimap_pixel(
125        camera_pos,
126        Vec2::new(dim.width as f32, dim.height as f32),
127        bbox,
128    );
129
130    let w = 4;
131    buffer.draw_rect_outline(
132        &TheDim::rect(pos.x as i32 - w, pos.y as i32 - w, w * 2, w * 2),
133        &vek::Rgba::red().into_array(),
134    );
135
136    if server_ctx.editor_view_mode == EditorViewMode::FirstP
137        && let Some(region) = region
138    {
139        let look_at_pos = Vec2::new(region.editing_look_at_3d.x, region.editing_look_at_3d.z);
140
141        let pos = world_to_minimap_pixel(
142            look_at_pos,
143            Vec2::new(dim.width as f32, dim.height as f32),
144            bbox,
145        );
146
147        buffer.draw_rect_outline(
148            &TheDim::rect(pos.x as i32 - w, pos.y as i32 - w, w * 2, w * 2),
149            &vek::Rgba::yellow().into_array(),
150        );
151    }
152}
153
154pub fn draw_minimap_context_label(
155    buffer: &mut TheRGBABuffer,
156    ctx: &mut TheContext,
157    server_ctx: &ServerContext,
158) {
159    let label = if server_ctx.get_map_context() == MapContext::Region {
160        if server_ctx.editing_surface.is_some() {
161            "Profile"
162        } else {
163            "Region"
164        }
165    } else if server_ctx.get_map_context() == MapContext::Screen {
166        "Screen"
167    } else if server_ctx.get_map_context() == MapContext::Character {
168        "Character"
169    } else if server_ctx.get_map_context() == MapContext::Item {
170        "Item"
171    } else {
172        "Map"
173    };
174
175    let stride = buffer.stride();
176    let bg = [36, 36, 36, 180];
177    let fg = [215, 215, 215, 255];
178    let text_pad_left = 4;
179    let approx_char_w = 7;
180    let label_w = (label.len() as i32 * approx_char_w + text_pad_left * 2).max(24);
181    ctx.draw.rect(
182        buffer.pixels_mut(),
183        &(0, 0, label_w as usize, 16),
184        stride,
185        &bg,
186    );
187    ctx.draw.text_rect(
188        buffer.pixels_mut(),
189        &(
190            text_pad_left as usize,
191            0,
192            (label_w - text_pad_left) as usize,
193            16,
194        ),
195        stride,
196        label,
197        TheFontSettings {
198            size: 11.0,
199            ..Default::default()
200        },
201        &fg,
202        &bg,
203        TheHorizontalAlign::Left,
204        TheVerticalAlign::Center,
205    );
206}
207
208pub fn draw_minimap(
209    project: &Project,
210    buffer: &mut TheRGBABuffer,
211    server_ctx: &ServerContext,
212    hard: bool,
213) {
214    if *SIDEBARMODE.read().unwrap() == SidebarMode::Palette {
215        buffer.render_hsl_hue_waveform();
216
217        if let Some(color) = PALETTE.read().unwrap().get_current_color() {
218            if let Some(pos) = buffer.find_closest_color_position(color.to_u8_array_3()) {
219                let w = 4;
220                buffer.draw_rect_outline(
221                    &TheDim::rect(pos.x - w, pos.y - w, w * 2, w * 2),
222                    &vek::Rgba::white().into_array(),
223                );
224            }
225        }
226
227        return;
228    }
229
230    let cache_key = minimap_context_key(server_ctx);
231    let mut hard = hard;
232    if !hard {
233        let cached_key = *MINIMAPCACHEKEY.read().unwrap();
234        let cached = MINIMAPBUFFER.read().unwrap();
235        let cached_dim = *cached.dim();
236        let dim = *buffer.dim();
237        if cached_key != cache_key
238            || cached_dim.width != dim.width
239            || cached_dim.height != dim.height
240        {
241            hard = true;
242        }
243    }
244
245    if !hard {
246        buffer.copy_into(0, 0, &MINIMAPBUFFER.read().unwrap());
247        if let Some(map) = project.get_map(server_ctx) {
248            let region_marker = if server_ctx.get_map_context() == MapContext::Region {
249                project.get_region(&server_ctx.curr_region)
250            } else {
251                None
252            };
253            draw_camera_marker(map, region_marker, buffer, server_ctx);
254        }
255        return;
256    }
257
258    let dim = buffer.dim();
259
260    let width = dim.width as f32;
261    let height = dim.height as f32;
262    let background = [42, 42, 42, 255];
263
264    if let Some(map) = project.get_map(server_ctx) {
265        let bbox_from_surface = server_ctx
266            .editing_surface
267            .as_ref()
268            .and_then(minimap_bbox_for_surface);
269        let Some(bbox) = minimap_bbox_for_map(map).or(bbox_from_surface) else {
270            buffer.fill(background);
271            return;
272        };
273
274        *MINIMAPBOX.write().unwrap() = bbox;
275
276        let scale_x = width / bbox.z;
277        let scale_y = height / bbox.w;
278
279        let mut rusterix = RUSTERIX.write().unwrap();
280        let mut map_copy = map.clone();
281        map_copy.selected_linedefs.clear();
282        map_copy.selected_sectors.clear();
283        map_copy.grid_size = scale_x.min(scale_y);
284        map_copy.camera = MapCamera::TwoD;
285
286        let bbox_center_x = bbox.x + bbox.z / 2.0;
287        let bbox_center_y = bbox.y + bbox.w / 2.0;
288        map_copy.offset.x = -bbox_center_x * scale_x;
289        map_copy.offset.y = bbox_center_y * scale_y;
290
291        let translation_matrix = Mat3::<f32>::translation_2d(Vec2::new(
292            map_copy.offset.x + width / 2.0,
293            -map_copy.offset.y + height / 2.0,
294        ));
295        let scale_matrix = Mat3::new(scale_x, 0.0, 0.0, 0.0, scale_y, 0.0, 0.0, 0.0, 1.0);
296        let transform = translation_matrix * scale_matrix;
297
298        let use_scenevm_region = server_ctx.get_map_context() == MapContext::Region
299            && server_ctx.editing_surface.is_none();
300
301        if use_scenevm_region {
302            // Minimap readability: render with stable daytime lighting.
303            let hour = 12.0;
304            let anim_counter = rusterix.client.animation_frame;
305            let scenevm_mode_2d = rusterix.scene_handler.settings.scenevm_mode_2d();
306            let scene_handler = &mut rusterix.scene_handler;
307            let layer_count = scene_handler.vm.vm_layer_count();
308            let mut layer_enabled_before = Vec::with_capacity(layer_count);
309            for i in 0..layer_count {
310                layer_enabled_before.push(scene_handler.vm.is_layer_enabled(i).unwrap_or(true));
311            }
312            // Minimap should show base scene only, never editor/game overlays.
313            for i in 1..layer_count {
314                scene_handler.vm.set_layer_enabled(i, false);
315            }
316            if matches!(scenevm_mode_2d, scenevm::RenderMode::Compute2D) {
317                scene_handler.vm.execute(scenevm::Atom::SetGP0(Vec4::new(
318                    map_copy.grid_size,
319                    map_copy.subdivisions,
320                    map_copy.offset.x,
321                    -map_copy.offset.y,
322                )));
323            }
324            scene_handler
325                .vm
326                .execute(scenevm::Atom::SetRenderMode(scenevm_mode_2d));
327            scene_handler.settings.apply_hour(hour);
328            scene_handler.settings.apply_2d(&mut scene_handler.vm);
329            // Minimap 2D background should stay black regardless of project sky settings.
330            scene_handler
331                .vm
332                .execute(scenevm::Atom::SetGP0(Vec4::zero()));
333            scene_handler
334                .vm
335                .execute(scenevm::Atom::SetTransform2D(transform));
336            scene_handler
337                .vm
338                .execute(scenevm::Atom::SetAnimationCounter(anim_counter));
339            scene_handler
340                .vm
341                .execute(scenevm::Atom::SetBackground(Vec4::zero()));
342            scene_handler
343                .vm
344                .render_frame(buffer.pixels_mut(), width as u32, height as u32);
345            for (i, enabled) in layer_enabled_before.into_iter().enumerate() {
346                scene_handler.vm.set_layer_enabled(i, enabled);
347            }
348        } else {
349            let mut builder = rusterix::D2PreviewBuilder::new();
350            let mut scene = builder.build(
351                &map_copy,
352                &rusterix.assets,
353                Vec2::new(width, height),
354                &ValueContainer::default(),
355            );
356
357            rusterix::Rasterizer::setup(Some(transform), Mat4::identity(), Mat4::identity())
358                .background(background)
359                .ambient(Vec4::one())
360                .render_mode(rusterix::RenderMode::render_2d().ignore_background_shader(true))
361                .rasterize(
362                    &mut scene,
363                    buffer.pixels_mut(),
364                    width as usize,
365                    height as usize,
366                    40,
367                    &rusterix.assets,
368                );
369        }
370
371        // Only overlay linedefs while editing a profile surface in D2 mode.
372        let show_profile_lines = server_ctx.get_map_context() == MapContext::Region
373            && server_ctx.editor_view_mode == EditorViewMode::D2
374            && server_ctx.editing_surface.is_some();
375        if show_profile_lines && !map_copy.linedefs.is_empty() {
376            let dim = Vec2::new(width, height);
377            let line_color = [235, 235, 235, 255];
378            for linedef in &map_copy.linedefs {
379                if let Some(start) = map_copy.get_vertex(linedef.start_vertex)
380                    && let Some(end) = map_copy.get_vertex(linedef.end_vertex)
381                {
382                    let p0 = world_to_minimap_pixel(start, dim, bbox);
383                    let p1 = world_to_minimap_pixel(end, dim, bbox);
384                    buffer.draw_line(
385                        p0.x.round() as i32,
386                        p0.y.round() as i32,
387                        p1.x.round() as i32,
388                        p1.y.round() as i32,
389                        line_color,
390                    );
391                }
392            }
393        }
394        if map_copy.sectors.is_empty()
395            && map_copy.linedefs.is_empty()
396            && let Some(surface) = server_ctx.editing_surface.as_ref()
397        {
398            draw_surface_outline_on_minimap(buffer, surface, bbox);
399        }
400
401        MINIMAPBUFFER
402            .write()
403            .unwrap()
404            .resize(buffer.dim().width, buffer.dim().height);
405
406        MINIMAPBUFFER.write().unwrap().copy_into(0, 0, buffer);
407        *MINIMAPCACHEKEY.write().unwrap() = cache_key;
408        let region_marker = if server_ctx.get_map_context() == MapContext::Region {
409            project.get_region(&server_ctx.curr_region)
410        } else {
411            None
412        };
413        draw_camera_marker(map, region_marker, buffer, server_ctx);
414    }
415}
416
417fn world_to_minimap_pixel(
418    world_pos: Vec2<f32>,
419    render_dim: Vec2<f32>,
420    bbox: Vec4<f32>, // x, y, w, h
421) -> Vec2<f32> {
422    let width = render_dim.x;
423    let height = render_dim.y;
424
425    let scale_x = width / bbox.z;
426    let scale_y = height / bbox.w;
427
428    let bbox_center_x = bbox.x + bbox.z / 2.0;
429    let bbox_center_y = bbox.y + bbox.w / 2.0;
430
431    let offset_x = -bbox_center_x * scale_x;
432    let offset_y = bbox_center_y * scale_y;
433
434    let pixel_x = (world_pos.x * scale_x) + offset_x + (width / 2.0);
435    let pixel_y = (-world_pos.y * scale_y) + offset_y + (height / 2.0);
436
437    Vec2::new(pixel_x, render_dim.y - pixel_y)
438}