Skip to main content

rustapi/
utils.rs

1use crate::editor::{CONFIGEDITOR, PALETTE, SCENEMANAGER};
2use crate::prelude::*;
3use codegridfx::{Module, ModuleType};
4use rusteria::{RenderBuffer, Rusteria};
5use rusterix::{PixelSource, Value, ValueContainer, pixel_to_vec4};
6use std::str::FromStr;
7use toml::*;
8
9pub fn default_preview_rigging_toml() -> String {
10    if let Some(bytes) = crate::Embedded::get("toml/preview_rigging.toml")
11        && let Ok(source) = std::str::from_utf8(bytes.data.as_ref())
12    {
13        return source.to_string();
14    }
15    "# Editor-only preview. Runtime equipment/animation comes from server scripts.\nanimation = \"Idle\"\nperspective = \"Front\"\nplay = true\nspeed = 1.0\n\n[slots]\n# main_hand = \"Item Name\"\n# off_hand = \"Item Name\"\n".to_string()
16}
17
18pub fn update_region_settings(
19    project: &mut Project,
20    server_ctx: &ServerContext,
21) -> Result<bool, Box<dyn std::error::Error>> {
22    let mut changed = false;
23    if let Some(id) = server_ctx.pc.id() {
24        if server_ctx.pc.is_region() {
25            if let Some(region) = project.get_region_mut(&id) {
26                let parsed: toml::Value = toml::from_str(&region.config)?;
27                // Parse [render] section
28                if let Some(section) = parsed.get("terrain") {
29                    if let Some(enabled) = section.get("enabled") {
30                        let enabled = enabled.as_bool().unwrap_or(false);
31                        // let existing = region
32                        //     .map
33                        //     .properties
34                        //     .get_bool_default("terain_enabled", false);
35                        // if enabled != existing {
36                        region
37                            .map
38                            .properties
39                            .set("terrain_enabled", Value::Bool(enabled));
40                        changed = true;
41                        // }
42                    }
43
44                    let mut got_tile_id = false;
45                    if let Some(enabled) = section.get("tile_id") {
46                        if let Some(tile_id) = enabled.as_str() {
47                            if let Ok(id) = Uuid::from_str(tile_id) {
48                                region.map.properties.set(
49                                    "default_terrain_tile",
50                                    Value::Source(PixelSource::TileId(id)),
51                                );
52                                got_tile_id = true;
53                            }
54                        }
55                    }
56                    if !got_tile_id {
57                        region.map.properties.remove("default_terrain_tile");
58                    }
59                }
60
61                if let Some(section) = parsed.get("preview").and_then(toml::Value::as_table) {
62                    region.map.properties.remove("preview_hide");
63                    if let Some(values) = section.get("hide").and_then(toml::Value::as_array) {
64                        let hide: Vec<String> = values
65                            .iter()
66                            .filter_map(toml::Value::as_str)
67                            .map(str::trim)
68                            .filter(|s| !s.is_empty())
69                            .map(str::to_string)
70                            .collect();
71                        if !hide.is_empty() {
72                            region
73                                .map
74                                .properties
75                                .set("preview_hide", Value::StrArray(hide));
76                        }
77                    }
78                    changed = true;
79                } else {
80                    region.map.properties.remove("preview_hide");
81                }
82            }
83        }
84    }
85
86    Ok(changed)
87}
88
89/// Sets the code for the code editor based on the current editor mode
90pub fn set_code(
91    ui: &mut TheUI,
92    ctx: &mut TheContext,
93    project: &mut Project,
94    server_ctx: &ServerContext,
95) {
96    let mut success = false;
97
98    match server_ctx.cc {
99        ContentContext::CharacterInstance(uuid) => {
100            if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
101                if let Some(character_instance) = region.characters.get_mut(&uuid) {
102                    ui.set_widget_value(
103                        "CodeEdit",
104                        ctx,
105                        TheValue::Text(character_instance.source.clone()),
106                    );
107                    success = true;
108                }
109            }
110        }
111        ContentContext::Sector(uuid) => {
112            if let Some(map) = project.get_map_mut(server_ctx) {
113                for s in &map.sectors {
114                    if s.creator_id == uuid {
115                        if let Some(Value::Str(source)) = s.properties.get("source") {
116                            ui.set_widget_value("CodeEdit", ctx, TheValue::Text(source.clone()));
117                            success = true;
118                        }
119                        if let Some(Value::Str(data)) = s.properties.get("data") {
120                            ui.set_widget_value("DataEdit", ctx, TheValue::Text(data.clone()));
121                            success = true;
122                        }
123                        break;
124                    }
125                }
126            }
127        }
128        ContentContext::CharacterTemplate(uuid) => {
129            if let Some(character) = project.characters.get_mut(&uuid) {
130                ui.set_widget_value("CodeEdit", ctx, TheValue::Text(character.source.clone()));
131                ui.set_widget_value("DataEdit", ctx, TheValue::Text(character.data.clone()));
132                character
133                    .module
134                    .set_module_type(ModuleType::CharacterTemplate);
135                success = true;
136            }
137        }
138        ContentContext::ItemTemplate(uuid) => {
139            if let Some(item) = project.items.get_mut(&uuid) {
140                ui.set_widget_value("CodeEdit", ctx, TheValue::Text(item.source.clone()));
141                ui.set_widget_value("DataEdit", ctx, TheValue::Text(item.data.clone()));
142                item.module.set_module_type(ModuleType::ItemTemplate);
143                success = true;
144            }
145        }
146        ContentContext::ItemInstance(uuid) => {
147            let mut temp_id = None;
148
149            if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
150                if let Some(item_inst) = region.items.get_mut(&uuid) {
151                    temp_id = Some(item_inst.item_id);
152                }
153            }
154
155            if let Some(temp_id) = temp_id {
156                if let Some(item) = project.items.get_mut(&temp_id) {
157                    ui.set_widget_value("CodeEdit", ctx, TheValue::Text(item.source.clone()));
158                    ui.set_widget_value("DataEdit", ctx, TheValue::Text(item.data.clone()));
159                    item.module.set_module_type(ModuleType::ItemTemplate);
160                    success = true;
161                }
162            }
163        }
164        _ => {}
165    }
166
167    /*
168    let sidebarmode = SIDEBARMODE.read().unwrap();
169    if *sidebarmode == SidebarMode::Region {
170        if let Some(region_content_id) = server_ctx.curr_region_content {
171            if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
172                // Check for Character Instance
173                if let Some(character_instance) = region.characters.get_mut(&region_content_id) {
174                    ui.set_widget_value(
175                        "CodeEdit",
176                        ctx,
177                        TheValue::Text(character_instance.source.clone()),
178                    );
179                    success = true;
180                } else {
181                    // Check for Sector
182                    for s in &region.map.sectors {
183                        if s.creator_id == region_content_id {
184                            ui.set_widget_value("CodeEdit", ctx, TheValue::Text(String::new()));
185                        }
186                    }
187                }
188            }
189        }
190    } else if *sidebarmode == SidebarMode::Character {
191        if let Some(character_id) = &server_ctx.curr_character {
192            if let Some(character) = project.characters.get(character_id) {
193                ui.set_widget_value("CodeEdit", ctx, TheValue::Text(character.source.clone()));
194                success = true;
195            }
196        }
197    }*/
198
199    if !success {
200        ui.set_widget_value("CodeEdit", ctx, TheValue::Text(String::new()));
201    }
202}
203
204/// Returns the currently active source
205pub fn get_source(_ui: &mut TheUI, server_ctx: &ServerContext) -> Option<PixelSource> {
206    let mut source: Option<PixelSource> = None;
207
208    if let Some(id) = server_ctx.curr_tile_id {
209        source = Some(PixelSource::TileId(id));
210    }
211
212    source
213}
214
215pub fn extract_build_values_from_config(values: &mut ValueContainer) {
216    let config = CONFIGEDITOR.read().unwrap();
217    let sample_mode = config.get_string_default("render", "sample_mode", "nearest");
218    if sample_mode == "linear" {
219        values.set(
220            "sample_mode",
221            Value::SampleMode(rusterix::SampleMode::Linear),
222        );
223    } else {
224        values.set(
225            "sample_mode",
226            Value::SampleMode(rusterix::SampleMode::Nearest),
227        );
228    }
229}
230
231/// Reads map relevant region settings from the TOML config and stores it in the map.
232pub fn apply_region_config(map: &mut Map, config: String) {
233    // Reset editor-only preview filters and repopulate from config if present.
234    map.properties.remove("preview_hide");
235
236    if let Ok(table) = config.parse::<Table>() {
237        if let Some(rendering) = table.get("rendering").and_then(toml::Value::as_table) {
238            // Daylight
239            if let Some(value) = rendering.get("receives_daylight") {
240                if let Some(v) = value.as_bool() {
241                    map.properties.set("receives_daylight", Value::Bool(v));
242                }
243            }
244
245            // Fog
246            if let Some(value) = rendering.get("fog_enabled") {
247                if let Some(v) = value.as_bool() {
248                    map.properties.set("fog_enabled", Value::Bool(v));
249                }
250            }
251            if let Some(value) = rendering.get("fog_start_distance") {
252                if let Some(v) = value.as_float() {
253                    map.properties
254                        .set("fog_start_distance", Value::Float(v as f32));
255                }
256            }
257            if let Some(value) = rendering.get("fog_end_distance") {
258                if let Some(v) = value.as_float() {
259                    map.properties
260                        .set("fog_end_distance", Value::Float(v as f32));
261                }
262            }
263            let mut fog_color = Vec4::zero();
264            if let Some(value) = rendering.get("fog_color") {
265                if let Some(v) = value.as_str() {
266                    let c = hex_to_rgba_u8(v);
267                    fog_color = pixel_to_vec4(&c);
268                }
269            }
270            map.properties.set(
271                "fog_color",
272                Value::Vec4([fog_color.x, fog_color.y, fog_color.z, fog_color.w]),
273            );
274        }
275
276        if let Some(preview) = table.get("preview").and_then(toml::Value::as_table)
277            && let Some(hide_values) = preview.get("hide").and_then(toml::Value::as_array)
278        {
279            let hide: Vec<String> = hide_values
280                .iter()
281                .filter_map(toml::Value::as_str)
282                .map(str::to_string)
283                .filter(|s| !s.is_empty())
284                .collect();
285            if !hide.is_empty() {
286                map.properties.set("preview_hide", Value::StrArray(hide));
287            }
288        }
289    }
290}
291
292/// Converts an hex string to a vec4 color
293pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] {
294    let hex = hex.trim_start_matches('#');
295
296    match hex.len() {
297        6 => match (
298            u8::from_str_radix(&hex[0..2], 16),
299            u8::from_str_radix(&hex[2..4], 16),
300            u8::from_str_radix(&hex[4..6], 16),
301        ) {
302            (Ok(r), Ok(g), Ok(b)) => [r, g, b, 255],
303            _ => [255, 255, 255, 255],
304        },
305        8 => match (
306            u8::from_str_radix(&hex[0..2], 16),
307            u8::from_str_radix(&hex[2..4], 16),
308            u8::from_str_radix(&hex[4..6], 16),
309            u8::from_str_radix(&hex[6..8], 16),
310        ) {
311            (Ok(r), Ok(g), Ok(b), Ok(a)) => [r, g, b, a],
312            _ => [255, 255, 255, 255],
313        },
314        _ => [255, 255, 255, 255],
315    }
316}
317
318/// Checks if the string is a valid python variable name
319pub fn is_valid_python_variable(name: &str) -> bool {
320    // Must not be empty, must start with a letter or underscore, and only contain letters, digits, or underscores
321    let mut chars = name.chars();
322    match chars.next() {
323        Some(c) if c.is_ascii_alphabetic() || c == '_' => (),
324        _ => return false,
325    }
326    if name.is_empty() {
327        return false;
328    }
329    if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
330        // Python keywords are not valid variable names
331        const PYTHON_KEYWORDS: &[&str] = &[
332            "False", "None", "True", "and", "as", "assert", "break", "class", "continue", "def",
333            "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
334            "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try",
335            "while", "with", "yield",
336        ];
337        !PYTHON_KEYWORDS.contains(&name)
338    } else {
339        false
340    }
341}
342
343/// Draws the shader into the given buffer
344pub fn draw_shader_into(module: &Module, buffer: &mut TheRGBABuffer) {
345    use std::sync::{Arc, Mutex};
346
347    let mut rs = Rusteria::default();
348    let code = module.build_shader();
349
350    let _module = match rs.parse_str(&code) {
351        Ok(module) => match rs.compile(&module) {
352            Ok(()) => {
353                println!("Module '{}' compiled successfully.", module.name);
354            }
355            Err(e) => {
356                eprintln!("Error compiling module: {e}");
357                return;
358            }
359        },
360        Err(e) => {
361            eprintln!("Error parsing module: {e}");
362            return;
363        }
364    };
365
366    let width = buffer.dim().width as usize;
367    let height = buffer.dim().height as usize;
368
369    if let Some(shade_index) = rs.context.program.shade_index {
370        let mut rbuffer = Arc::new(Mutex::new(RenderBuffer::new(width, height)));
371        let t0 = rs.get_time();
372        rs.shade(&mut rbuffer, shade_index, &PALETTE.read().unwrap());
373        let t1 = rs.get_time();
374        println!("Rendered in {}ms", t1 - t0);
375
376        let b = rbuffer.lock().unwrap().as_rgba_bytes();
377        *buffer = TheRGBABuffer::from(b, width as u32, height as u32)
378    }
379}
380
381/// Renders the current map in the scenemanager depending on the current viewmode.
382pub fn scenemanager_render_map(project: &Project, server_ctx: &ServerContext) {
383    // Screens should not trigger SceneManager SetMap updates.
384    if server_ctx.get_map_context() == MapContext::Screen {
385        return;
386    }
387
388    if server_ctx.editor_view_mode == EditorViewMode::D2 {
389        // In 2D we render the current map (region/profile/character/item), but never screens.
390        if let Some(map) = project.get_map(server_ctx) {
391            SCENEMANAGER.write().unwrap().set_map(map.clone());
392        }
393    } else {
394        // In 3D, SceneManager builds region geometry. This includes profile/surface editing
395        // contexts, but excludes non-region contexts.
396        if server_ctx.get_map_context() != MapContext::Region {
397            return;
398        }
399
400        // In 3D region mode (including profiles), render the region map.
401        if let Some(id) = server_ctx.pc.id() {
402            if let Some(region) = project.get_region(&id) {
403                let mut map = region.map.clone();
404                // Keep editor-only preview filters in sync with region config for 3D builds.
405                apply_region_config(&mut map, region.config.clone());
406                SCENEMANAGER.write().unwrap().set_map(map);
407            }
408        }
409    }
410}