use crate::editor::{CONFIGEDITOR, PALETTE, SCENEMANAGER};
use crate::prelude::*;
use codegridfx::{Module, ModuleType};
use rusteria::{RenderBuffer, Rusteria};
use rusterix::{PixelSource, Value, ValueContainer, pixel_to_vec4};
use std::str::FromStr;
use toml::*;
pub fn default_preview_rigging_toml() -> String {
if let Some(bytes) = crate::Embedded::get("toml/preview_rigging.toml")
&& let Ok(source) = std::str::from_utf8(bytes.data.as_ref())
{
return source.to_string();
}
"# 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()
}
pub fn update_region_settings(
project: &mut Project,
server_ctx: &ServerContext,
) -> Result<bool, Box<dyn std::error::Error>> {
let mut changed = false;
if let Some(id) = server_ctx.pc.id() {
if server_ctx.pc.is_region() {
if let Some(region) = project.get_region_mut(&id) {
let parsed: toml::Value = toml::from_str(®ion.config)?;
if let Some(section) = parsed.get("terrain") {
if let Some(enabled) = section.get("enabled") {
let enabled = enabled.as_bool().unwrap_or(false);
region
.map
.properties
.set("terrain_enabled", Value::Bool(enabled));
changed = true;
}
let mut got_tile_id = false;
if let Some(enabled) = section.get("tile_id") {
if let Some(tile_id) = enabled.as_str() {
if let Ok(id) = Uuid::from_str(tile_id) {
region.map.properties.set(
"default_terrain_tile",
Value::Source(PixelSource::TileId(id)),
);
got_tile_id = true;
}
}
}
if !got_tile_id {
region.map.properties.remove("default_terrain_tile");
}
}
if let Some(section) = parsed.get("preview").and_then(toml::Value::as_table) {
region.map.properties.remove("preview_hide");
if let Some(values) = section.get("hide").and_then(toml::Value::as_array) {
let hide: Vec<String> = values
.iter()
.filter_map(toml::Value::as_str)
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if !hide.is_empty() {
region
.map
.properties
.set("preview_hide", Value::StrArray(hide));
}
}
changed = true;
} else {
region.map.properties.remove("preview_hide");
}
}
}
}
Ok(changed)
}
pub fn set_code(
ui: &mut TheUI,
ctx: &mut TheContext,
project: &mut Project,
server_ctx: &ServerContext,
) {
let mut success = false;
match server_ctx.cc {
ContentContext::CharacterInstance(uuid) => {
if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
if let Some(character_instance) = region.characters.get_mut(&uuid) {
ui.set_widget_value(
"CodeEdit",
ctx,
TheValue::Text(character_instance.source.clone()),
);
success = true;
}
}
}
ContentContext::Sector(uuid) => {
if let Some(map) = project.get_map_mut(server_ctx) {
for s in &map.sectors {
if s.creator_id == uuid {
if let Some(Value::Str(source)) = s.properties.get("source") {
ui.set_widget_value("CodeEdit", ctx, TheValue::Text(source.clone()));
success = true;
}
if let Some(Value::Str(data)) = s.properties.get("data") {
ui.set_widget_value("DataEdit", ctx, TheValue::Text(data.clone()));
success = true;
}
break;
}
}
}
}
ContentContext::CharacterTemplate(uuid) => {
if let Some(character) = project.characters.get_mut(&uuid) {
ui.set_widget_value("CodeEdit", ctx, TheValue::Text(character.source.clone()));
ui.set_widget_value("DataEdit", ctx, TheValue::Text(character.data.clone()));
character
.module
.set_module_type(ModuleType::CharacterTemplate);
success = true;
}
}
ContentContext::ItemTemplate(uuid) => {
if let Some(item) = project.items.get_mut(&uuid) {
ui.set_widget_value("CodeEdit", ctx, TheValue::Text(item.source.clone()));
ui.set_widget_value("DataEdit", ctx, TheValue::Text(item.data.clone()));
item.module.set_module_type(ModuleType::ItemTemplate);
success = true;
}
}
ContentContext::ItemInstance(uuid) => {
let mut temp_id = None;
if let Some(region) = project.get_region_mut(&server_ctx.curr_region) {
if let Some(item_inst) = region.items.get_mut(&uuid) {
temp_id = Some(item_inst.item_id);
}
}
if let Some(temp_id) = temp_id {
if let Some(item) = project.items.get_mut(&temp_id) {
ui.set_widget_value("CodeEdit", ctx, TheValue::Text(item.source.clone()));
ui.set_widget_value("DataEdit", ctx, TheValue::Text(item.data.clone()));
item.module.set_module_type(ModuleType::ItemTemplate);
success = true;
}
}
}
_ => {}
}
if !success {
ui.set_widget_value("CodeEdit", ctx, TheValue::Text(String::new()));
}
}
pub fn get_source(_ui: &mut TheUI, server_ctx: &ServerContext) -> Option<PixelSource> {
if let Some(source) = server_ctx.curr_tile_source {
return Some(match source {
TileSource::SingleTile(id) => PixelSource::TileId(id),
TileSource::TileGroup(id) => server_ctx
.curr_tile_id
.map(PixelSource::TileId)
.unwrap_or(PixelSource::TileGroup(id)),
TileSource::TileGroupMember {
group_id,
member_index,
} => server_ctx.curr_tile_id.map(PixelSource::TileId).unwrap_or(
PixelSource::TileGroupMember {
group_id,
member_index,
},
),
TileSource::Procedural(id) => server_ctx
.curr_tile_id
.map(PixelSource::TileId)
.unwrap_or(PixelSource::ProceduralTile(id)),
});
}
server_ctx.curr_tile_id.map(PixelSource::TileId)
}
#[derive(Clone)]
pub enum SurfaceApplySource {
Direct(PixelSource),
TileGroup {
group: rusterix::TileGroup,
flip_y: bool,
},
}
pub fn get_surface_apply_source(
project: &Project,
server_ctx: &ServerContext,
) -> Option<SurfaceApplySource> {
if let Some(source) = server_ctx.curr_tile_source {
return match source {
TileSource::SingleTile(id) => Some(SurfaceApplySource::Direct(PixelSource::TileId(id))),
TileSource::TileGroup(id) => {
project
.tile_groups
.get(&id)
.cloned()
.map(|group| SurfaceApplySource::TileGroup {
group,
flip_y: project.tile_node_groups.contains_key(&id),
})
}
TileSource::TileGroupMember { .. } => server_ctx
.curr_tile_id
.map(PixelSource::TileId)
.map(SurfaceApplySource::Direct),
TileSource::Procedural(_) => server_ctx
.curr_tile_id
.map(PixelSource::TileId)
.map(SurfaceApplySource::Direct),
};
}
server_ctx
.curr_tile_id
.map(PixelSource::TileId)
.map(SurfaceApplySource::Direct)
}
fn ensure_sector_surface(map: &mut Map, sector_id: u32) -> Option<rusterix::Surface> {
if map.get_surface_for_sector_id(sector_id).is_none() {
let mut surface = rusterix::Surface::new(sector_id);
surface.calculate_geometry(map);
let id = surface.id;
map.surfaces.insert(id, surface);
}
map.get_surface_for_sector_id(sector_id).cloned()
}
fn clear_sector_tile_overrides(map: &mut Map, sector_id: u32) {
if let Some(sector) = map.find_sector_mut(sector_id) {
sector.properties.remove("tiles");
sector.properties.remove("blend_tiles");
}
}
fn point_in_polygon_2d(point: Vec2<f32>, polygon: &[Vec2<f32>]) -> bool {
if polygon.len() < 3 {
return false;
}
let mut inside = false;
let mut j = polygon.len() - 1;
for i in 0..polygon.len() {
let pi = polygon[i];
let pj = polygon[j];
let crosses = (pi.y > point.y) != (pj.y > point.y);
if crosses {
let denom = (pj.y - pi.y).abs().max(1e-6);
let x = (pj.x - pi.x) * (point.y - pi.y) / denom + pi.x;
if point.x < x {
inside = !inside;
}
}
j = i;
}
inside
}
fn sector_local_tile_cells(map: &Map, surface: &rusterix::Surface) -> Vec<(i32, i32)> {
let Some(loop_uv) = surface.sector_loop_uv(map) else {
return Vec::new();
};
let local_loop: Vec<Vec2<f32>> = loop_uv
.into_iter()
.map(|uv| surface.uv_to_tile_local(uv, map))
.collect();
if local_loop.len() < 3 {
return Vec::new();
}
let mut min = Vec2::new(f32::INFINITY, f32::INFINITY);
let mut max = Vec2::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
for point in &local_loop {
min.x = min.x.min(point.x);
min.y = min.y.min(point.y);
max.x = max.x.max(point.x);
max.y = max.y.max(point.y);
}
let mut cells = Vec::new();
for y in min.y.floor() as i32..max.y.ceil() as i32 {
for x in min.x.floor() as i32..max.x.ceil() as i32 {
let center = Vec2::new(x as f32 + 0.5, y as f32 + 0.5);
if point_in_polygon_2d(center, &local_loop) {
cells.push((x, y));
}
}
}
cells
}
pub fn apply_surface_source_to_sector(
map: &mut Map,
sector_id: u32,
source_key: &str,
source: &SurfaceApplySource,
tile_mode: Option<i32>,
) -> bool {
let target_sector_ids: Vec<u32> = if let Some(sector) = map.find_sector(sector_id) {
let generated_by = sector
.properties
.get_str_default("generated_by", String::new());
let dungeon_part = sector
.properties
.get_str_default("dungeon_part", String::new());
if generated_by == "dungeon_tool"
&& (dungeon_part == "stair_tread"
|| dungeon_part == "stair_riser"
|| dungeon_part == "stair_ceiling")
&& let (Some(layer_id), Some(cell_x), Some(cell_y)) = (
sector.properties.get_id("dungeon_layer_id"),
sector.properties.get_int("dungeon_cell_x"),
sector.properties.get_int("dungeon_cell_y"),
)
{
map.sectors
.iter()
.filter(|candidate| {
candidate
.properties
.get_str_default("generated_by", String::new())
== "dungeon_tool"
&& candidate.properties.get_id("dungeon_layer_id") == Some(layer_id)
&& candidate.properties.get_int("dungeon_cell_x") == Some(cell_x)
&& candidate.properties.get_int("dungeon_cell_y") == Some(cell_y)
&& matches!(
candidate
.properties
.get_str_default("dungeon_part", String::new())
.as_str(),
"stair_tread" | "stair_riser" | "stair_ceiling"
)
})
.map(|sector| sector.id)
.collect()
} else {
vec![sector_id]
}
} else {
vec![sector_id]
};
match source {
SurfaceApplySource::Direct(pixel_source) => {
let mut changed = false;
for sector_id in target_sector_ids {
clear_sector_tile_overrides(map, sector_id);
if let Some(sector) = map.find_sector_mut(sector_id) {
sector
.properties
.set(source_key, Value::Source(pixel_source.clone()));
let dungeon_part = sector
.properties
.get_str_default("dungeon_part", String::new());
if source_key == "source"
&& matches!(
dungeon_part.as_str(),
"stair_tread" | "stair_riser" | "stair_ceiling"
)
{
sector
.properties
.set("source", Value::Source(pixel_source.clone()));
sector
.properties
.set("floor_source", Value::Source(pixel_source.clone()));
sector
.properties
.set("ceiling_source", Value::Source(pixel_source.clone()));
}
if let Some(tile_mode) = tile_mode {
sector.properties.set("tile_mode", Value::Int(tile_mode));
}
changed = true;
}
}
changed
}
SurfaceApplySource::TileGroup { group, flip_y } => {
if group.width == 0 || group.height == 0 || group.members.is_empty() {
return false;
}
if source_key != "source" {
let mut changed = false;
for sector_id in target_sector_ids {
if let Some(member) = group.members.first()
&& let Some(sector) = map.find_sector_mut(sector_id)
{
sector.properties.set(
source_key,
Value::Source(PixelSource::TileId(member.tile_id)),
);
if let Some(tile_mode) = tile_mode {
sector.properties.set("tile_mode", Value::Int(tile_mode));
}
changed = true;
}
}
return changed;
}
clear_sector_tile_overrides(map, sector_id);
let Some(surface) = ensure_sector_surface(map, sector_id) else {
return false;
};
let cells = sector_local_tile_cells(map, &surface);
if cells.is_empty() {
return false;
}
let member_lookup: FxHashMap<(i32, i32), Uuid> = group
.members
.iter()
.map(|member| ((member.x as i32, member.y as i32), member.tile_id))
.collect();
let mut tiles: FxHashMap<(i32, i32), PixelSource> = FxHashMap::default();
for (x, y) in cells {
let gx = x.rem_euclid(group.width as i32);
let gy = if *flip_y {
(group.height as i32 - 1) - y.rem_euclid(group.height as i32)
} else {
y.rem_euclid(group.height as i32)
};
if let Some(tile_id) = member_lookup.get(&(gx, gy)) {
tiles.insert((x, y), PixelSource::TileId(*tile_id));
}
}
if let Some(sector) = map.find_sector_mut(sector_id) {
if let Some(first) = group.members.first() {
sector
.properties
.set("source", Value::Source(PixelSource::TileId(first.tile_id)));
}
if let Some(tile_mode) = tile_mode {
sector.properties.set("tile_mode", Value::Int(tile_mode));
}
if tiles.is_empty() {
sector.properties.remove("tiles");
} else {
sector.properties.set("tiles", Value::TileOverrides(tiles));
}
sector.properties.remove("blend_tiles");
return true;
}
false
}
}
}
pub fn clear_surface_source_on_sector(map: &mut Map, sector_id: u32, source_key: &str) -> bool {
if source_key == "source" {
clear_sector_tile_overrides(map, sector_id);
}
if let Some(sector) = map.find_sector_mut(sector_id) {
sector
.properties
.set(source_key, Value::Source(PixelSource::Off));
return true;
}
false
}
pub fn extract_build_values_from_config(values: &mut ValueContainer) {
let config = CONFIGEDITOR.read().unwrap();
let sample_mode = config.get_string_default("render", "sample_mode", "nearest");
if sample_mode == "linear" {
values.set(
"sample_mode",
Value::SampleMode(rusterix::SampleMode::Linear),
);
} else {
values.set(
"sample_mode",
Value::SampleMode(rusterix::SampleMode::Nearest),
);
}
}
pub fn apply_region_config(map: &mut Map, config: String) {
map.properties.remove("preview_hide");
if let Ok(table) = config.parse::<Table>() {
if let Some(rendering) = table.get("rendering").and_then(toml::Value::as_table) {
if let Some(value) = rendering.get("receives_daylight") {
if let Some(v) = value.as_bool() {
map.properties.set("receives_daylight", Value::Bool(v));
}
}
if let Some(value) = rendering.get("fog_enabled") {
if let Some(v) = value.as_bool() {
map.properties.set("fog_enabled", Value::Bool(v));
}
}
if let Some(value) = rendering.get("fog_start_distance") {
if let Some(v) = value.as_float() {
map.properties
.set("fog_start_distance", Value::Float(v as f32));
}
}
if let Some(value) = rendering.get("fog_end_distance") {
if let Some(v) = value.as_float() {
map.properties
.set("fog_end_distance", Value::Float(v as f32));
}
}
let mut fog_color = Vec4::zero();
if let Some(value) = rendering.get("fog_color") {
if let Some(v) = value.as_str() {
let c = hex_to_rgba_u8(v);
fog_color = pixel_to_vec4(&c);
}
}
map.properties.set(
"fog_color",
Value::Vec4([fog_color.x, fog_color.y, fog_color.z, fog_color.w]),
);
}
if let Some(preview) = table.get("preview").and_then(toml::Value::as_table)
&& let Some(hide_values) = preview.get("hide").and_then(toml::Value::as_array)
{
let hide: Vec<String> = hide_values
.iter()
.filter_map(toml::Value::as_str)
.map(str::to_string)
.filter(|s| !s.is_empty())
.collect();
if !hide.is_empty() {
map.properties.set("preview_hide", Value::StrArray(hide));
}
}
}
}
pub fn hex_to_rgba_u8(hex: &str) -> [u8; 4] {
let hex = hex.trim_start_matches('#');
match hex.len() {
6 => match (
u8::from_str_radix(&hex[0..2], 16),
u8::from_str_radix(&hex[2..4], 16),
u8::from_str_radix(&hex[4..6], 16),
) {
(Ok(r), Ok(g), Ok(b)) => [r, g, b, 255],
_ => [255, 255, 255, 255],
},
8 => match (
u8::from_str_radix(&hex[0..2], 16),
u8::from_str_radix(&hex[2..4], 16),
u8::from_str_radix(&hex[4..6], 16),
u8::from_str_radix(&hex[6..8], 16),
) {
(Ok(r), Ok(g), Ok(b), Ok(a)) => [r, g, b, a],
_ => [255, 255, 255, 255],
},
_ => [255, 255, 255, 255],
}
}
pub fn is_valid_python_variable(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => (),
_ => return false,
}
if name.is_empty() {
return false;
}
if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
const PYTHON_KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "break", "class", "continue", "def",
"del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
"in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try",
"while", "with", "yield",
];
!PYTHON_KEYWORDS.contains(&name)
} else {
false
}
}
pub fn draw_shader_into(module: &Module, buffer: &mut TheRGBABuffer) {
use std::sync::{Arc, Mutex};
let mut rs = Rusteria::default();
let code = module.build_shader();
let _module = match rs.parse_str(&code) {
Ok(module) => match rs.compile(&module) {
Ok(()) => {
println!("Module '{}' compiled successfully.", module.name);
}
Err(e) => {
eprintln!("Error compiling module: {e}");
return;
}
},
Err(e) => {
eprintln!("Error parsing module: {e}");
return;
}
};
let width = buffer.dim().width as usize;
let height = buffer.dim().height as usize;
if let Some(shade_index) = rs.context.program.shade_index {
let mut rbuffer = Arc::new(Mutex::new(RenderBuffer::new(width, height)));
let t0 = rs.get_time();
rs.shade(&mut rbuffer, shade_index, &PALETTE.read().unwrap());
let t1 = rs.get_time();
println!("Rendered in {}ms", t1 - t0);
let b = rbuffer.lock().unwrap().as_rgba_bytes();
*buffer = TheRGBABuffer::from(b, width as u32, height as u32)
}
}
pub fn scenemanager_render_map(project: &Project, server_ctx: &ServerContext) {
if server_ctx.get_map_context() == MapContext::Screen {
return;
}
if server_ctx.editor_view_mode == EditorViewMode::D2 {
if let Some(map) = project.get_map(server_ctx) {
let mut map = map.clone();
map.properties.set(
"editing_filter_dungeon",
Value::Bool(server_ctx.editing_geo_filter == EditingGeoFilter::DungeonOnly),
);
map.properties.set(
"dungeon_no_ceiling",
Value::Bool(server_ctx.dungeon_no_ceiling),
);
SCENEMANAGER.write().unwrap().set_map(map);
}
} else {
if server_ctx.get_map_context() != MapContext::Region {
return;
}
if let Some(id) = server_ctx.pc.id() {
if let Some(region) = project.get_region(&id) {
let mut map = region.map.clone();
apply_region_config(&mut map, region.config.clone());
map.properties.set(
"editing_filter_dungeon",
Value::Bool(server_ctx.editing_geo_filter == EditingGeoFilter::DungeonOnly),
);
map.properties.set(
"dungeon_no_ceiling",
Value::Bool(server_ctx.dungeon_no_ceiling),
);
SCENEMANAGER.write().unwrap().set_map(map);
}
}
}
}