diff --git a/crates/dmm-tools/src/render_passes/icon_smoothing_2025.rs b/crates/dmm-tools/src/render_passes/icon_smoothing_2025.rs new file mode 100644 index 00000000..bcdcabab --- /dev/null +++ b/crates/dmm-tools/src/render_passes/icon_smoothing_2025.rs @@ -0,0 +1,284 @@ +//! Port of icon smoothing subsystem as of 2025. +//! +//! Based off of the [`icon_smoothing_2020`](crate::render_passes::icon_smoothing_2020) subsystem, +//! followed by https://github.com/tgstation/tgstation/pull/90002 + +use crate::dmi::Dir; +use crate::minimap::{Atom, GetVar, Neighborhood, Sprite}; +use dm::constants::Constant; +use dm::objtree::ObjectTree; +use foldhash::HashSet; + +use super::RenderPass; + +const NORTH_JUNCTION: i32 = 1 << 0; +const SOUTH_JUNCTION: i32 = 1 << 1; +const EAST_JUNCTION: i32 = 1 << 2; +const WEST_JUNCTION: i32 = 1 << 3; +const NORTHEAST_JUNCTION: i32 = 1 << 4; +const SOUTHEAST_JUNCTION: i32 = 1 << 5; +const SOUTHWEST_JUNCTION: i32 = 1 << 6; +const NORTHWEST_JUNCTION: i32 = 1 << 7; + +/// Smoothing system in where adjacencies are calculated and used to select a pre-baked icon_state, encoded by bitmasking. +const SMOOTH_BITMASK: i32 = 1 << 0; +/// Atom has diagonal corners, with underlays under them. +const SMOOTH_DIAGONAL_CORNERS: i32 = 1 << 2; +/// Atom will smooth with the borders of the map. +const SMOOTH_BORDER: i32 = 1 << 3; + +pub struct IconSmoothing { + pub mask: i32, +} + +impl Default for IconSmoothing { + fn default() -> Self { + IconSmoothing { mask: !0 } + } +} + +impl RenderPass for IconSmoothing { + fn adjust_sprite<'a>( + &self, + atom: &Atom<'a>, + sprite: &mut Sprite<'a>, + _objtree: &'a ObjectTree, + _bump: &'a bumpalo::Bump, + ) { + if atom.istype("/turf/closed/mineral/") { + sprite.ofs_x -= 4; + sprite.ofs_y -= 4; + } + } + + fn neighborhood_appearance<'a>( + &self, + atom: &Atom<'a>, + objtree: &'a ObjectTree, + neighborhood: &Neighborhood<'a, '_>, + output: &mut Vec>, + bump: &'a bumpalo::Bump, + ) -> bool { + let smooth_flags = self.mask + & atom + .get_var("smoothing_flags", objtree) + .to_int() + .unwrap_or(0); + if smooth_flags & SMOOTH_BITMASK != 0 { + let adjacencies = calculate_adjacencies(objtree, neighborhood, atom, smooth_flags); + bitmask_smooth( + output, + objtree, + bump, + neighborhood, + atom, + adjacencies, + smooth_flags, + ) + } else { + true + } + } +} + +// ---------------------------------------------------------------------------- +// Older cardinal smoothing system + +fn calculate_adjacencies( + objtree: &ObjectTree, + neighborhood: &Neighborhood, + atom: &Atom, + smooth_flags: i32, +) -> i32 { + // Easier to read as a nested conditional + #[allow(clippy::collapsible_if)] + if atom.istype("/atom/movable/") { + if atom.get_var("can_be_unanchored", objtree).to_bool() + && !atom.get_var("anchored", objtree).to_bool() + { + return 0; + } + } + + let mut adjacencies = 0; + let check_one = |direction, flag| { + if find_type_in_direction(objtree, neighborhood, atom, direction, smooth_flags) { + flag + } else { + 0 + } + }; + + adjacencies |= check_one(Dir::North, NORTH_JUNCTION); + adjacencies |= check_one(Dir::South, SOUTH_JUNCTION); + adjacencies |= check_one(Dir::East, EAST_JUNCTION); + adjacencies |= check_one(Dir::West, WEST_JUNCTION); + + if adjacencies & NORTH_JUNCTION != 0 { + if adjacencies & WEST_JUNCTION != 0 { + adjacencies |= check_one(Dir::Northwest, NORTHWEST_JUNCTION); + } + if adjacencies & EAST_JUNCTION != 0 { + adjacencies |= check_one(Dir::Northeast, NORTHEAST_JUNCTION); + } + } + if adjacencies & SOUTH_JUNCTION != 0 { + if adjacencies & WEST_JUNCTION != 0 { + adjacencies |= check_one(Dir::Southwest, SOUTHWEST_JUNCTION); + } + if adjacencies & EAST_JUNCTION != 0 { + adjacencies |= check_one(Dir::Southeast, SOUTHEAST_JUNCTION); + } + } + + adjacencies +} + +fn find_type_in_direction( + objtree: &ObjectTree, + adjacency: &Neighborhood, + source: &Atom, + direction: Dir, + smooth_flags: i32, +) -> bool { + let atom_list = adjacency.offset(direction); + if atom_list.is_empty() { + return smooth_flags & SMOOTH_BORDER != 0; + } + + match source.get_var("canSmoothWith", objtree) { + Constant::List(elements) => { + // smooth with anything for which their smoothing_groups overlaps our canSmoothWith + let set: HashSet<_> = elements.iter().map(|x| &x.0).collect(); + for atom in atom_list { + if let Constant::List(elements2) = atom.get_var("smoothing_groups", objtree) { + let set2: HashSet<_> = elements2.iter().map(|x| &x.0).collect(); + if set.intersection(&set2).next().is_some() { + return true; + } + } + } + }, + _ => { + // smooth only with the same type + for atom in atom_list { + if std::ptr::eq(atom.get_path(), source.get_path()) { + return true; + } + } + }, + } + false +} + +fn diagonal_underlay<'a>( + output: &mut Vec>, + objtree: &'a ObjectTree, + neighborhood: &Neighborhood<'a, '_>, + source: &Atom<'a>, + adjacencies: i32, +) { + // BYOND memes + if source + .get_var("fixed_underlay", objtree) + .index(&Constant::string("space")) + .is_some() + { + output.push(Sprite::from_vars( + objtree, + &objtree.expect("/turf/open/space/basic"), + )); + } else if let Some(dir) = reverse_ndir(adjacencies) { + let dir = dir.flip(); + let mut needs_plating = true; + // check direct, then 45deg left, then 45deg right + 'dirs: for &each in &[dir, dir.counterclockwise_45(), dir.clockwise_45()] { + let atom_list = neighborhood.offset(each); + for atom in atom_list { + if atom.istype("/turf/open/") { + output.push(Sprite::from_vars(objtree, atom)); + needs_plating = false; + break 'dirs; + } + } + } + if needs_plating { + output.push(Sprite::from_vars( + objtree, + &objtree.expect("/turf/open/floor/plating"), + )); + } + } +} + +fn reverse_ndir(ndir: i32) -> Option { + const NW1: i32 = NORTH_JUNCTION | WEST_JUNCTION; + const NW2: i32 = NW1 | NORTHWEST_JUNCTION; + const NE1: i32 = NORTH_JUNCTION | EAST_JUNCTION; + const NE2: i32 = NE1 | NORTHEAST_JUNCTION; + const SW1: i32 = SOUTH_JUNCTION | WEST_JUNCTION; + const SW2: i32 = SW1 | SOUTHWEST_JUNCTION; + const SE1: i32 = SOUTH_JUNCTION | EAST_JUNCTION; + const SE2: i32 = SE1 | SOUTHEAST_JUNCTION; + + match ndir { + NORTH_JUNCTION => Some(Dir::North), + SOUTH_JUNCTION => Some(Dir::South), + WEST_JUNCTION => Some(Dir::West), + EAST_JUNCTION => Some(Dir::East), + SOUTHEAST_JUNCTION | SE1 | SE2 => Some(Dir::Southeast), + SOUTHWEST_JUNCTION | SW1 | SW2 => Some(Dir::Southwest), + NORTHEAST_JUNCTION | NE1 | NE2 => Some(Dir::Northeast), + NORTHWEST_JUNCTION | NW1 | NW2 => Some(Dir::Northwest), + _ => None, + } +} + +// ---------------------------------------------------------------------------- +// Bitmask smoothing system + +fn bitmask_smooth<'a>( + output: &mut Vec>, + objtree: &'a ObjectTree, + bump: &'a bumpalo::Bump, + neighborhood: &Neighborhood<'a, '_>, + source: &Atom<'a>, + smoothing_junction: i32, + smooth_flags: i32, +) -> bool { + let mut diagonal = ""; + if source.istype("/turf/open/floor/") { + if source.get_var("broken", objtree).to_bool() || source.get_var("burnt", objtree).to_bool() + { + return true; // use original appearance + } + } else if source.istype("/turf/closed/") + && (smooth_flags & SMOOTH_DIAGONAL_CORNERS != 0) + && reverse_ndir(smoothing_junction).is_some() + { + diagonal_underlay(output, objtree, neighborhood, source, smoothing_junction); + diagonal = "-d"; + } + + // if it has post_init_icon_state, that means it has a pre-generated icon + if !source.get_var("post_init_icon_state", objtree).is_null() { + return true; + } + + let base_icon_state = source + .get_var("base_icon_state", objtree) + .as_str() + .unwrap_or(""); + let mut sprite = Sprite { + icon_state: + bumpalo::format!(in bump, "{}-{}{}", base_icon_state, smoothing_junction, diagonal) + .into_bump_str(), + ..source.sprite + }; + if let Some(icon) = source.get_var("smooth_icon", objtree).as_path_str() { + sprite.icon = icon; + } + output.push(sprite); + + false +} diff --git a/crates/dmm-tools/src/render_passes/mod.rs b/crates/dmm-tools/src/render_passes/mod.rs index a4119e8d..e1f3a49c 100644 --- a/crates/dmm-tools/src/render_passes/mod.rs +++ b/crates/dmm-tools/src/render_passes/mod.rs @@ -4,6 +4,7 @@ use dm::objtree::*; mod icon_smoothing; mod icon_smoothing_2020; +mod icon_smoothing_2025; mod random; mod smart_cables; mod structures; @@ -11,6 +12,7 @@ mod transit_tube; pub use self::icon_smoothing::IconSmoothing as IconSmoothing2016; pub use self::icon_smoothing_2020::IconSmoothing; +pub use self::icon_smoothing_2025::IconSmoothing as IconSmoothing2025; pub use self::random::Random; pub use self::smart_cables::SmartCables; pub use self::structures::{GravityGen, Spawners}; @@ -184,6 +186,12 @@ pub const RENDER_PASSES: &[RenderPassInfo] = &[ "Emulate the icon smoothing subsystem (Rohesie, 2020).", true ), + pass!( + IconSmoothing2025, + "icon-smoothing-2025", + "Emulate the tg icon smoothing subsystem without corner smoothing (LemonInTheDark, Rohesie, 2025).", + false + ), pass!( SmartCables, "smart-cables", @@ -381,42 +389,82 @@ impl RenderPass for Overlays { icon_state: "grille", ..atom.sprite }); - } else if atom.istype("/obj/structure/closet/") { - // closet doors - if atom.get_var("opened", objtree).to_bool() { - let var = if atom.get_var("icon_door_override", objtree).to_bool() { - "icon_door" - } else { - "icon_state" - }; - if let Constant::String(door) = atom.get_var(var, objtree) { - add_to( - overlays, - atom, - bumpalo::format!(in bump, "{}_open", door).into_bump_str(), - ); + } else if atom.istype("/obj/structure/closet/crate") { + if !atom.get_var("opened", objtree).to_bool() { + if atom.get_var("broken", objtree).to_bool() { + add_to(overlays, atom, "securecrateemag"); + } else if atom.get_var("locked", objtree).to_bool() { + add_to(overlays, atom, "securecrater"); + } else if atom.get_var("secure", objtree).to_bool() { + add_to(overlays, atom, "securecrateg"); } - } else { - if let Constant::String(door) = atom - .get_var_notnull("icon_door", objtree) - .unwrap_or_else(|| atom.get_var("icon_state", objtree)) - { - add_to( - overlays, - atom, - bumpalo::format!(in bump, "{}_door", door).into_bump_str(), - ); - } - if atom.get_var("welded", objtree).to_bool() { - add_to(overlays, atom, "welded"); + } + + if atom.get_var("welded", objtree).to_bool() { + let off_x = atom.get_var("weld_w", objtree).to_int().unwrap_or(0); + let off_y = atom.get_var("weld_z", objtree).to_int().unwrap_or(0); + overlays.push(Sprite { + icon_state: "welded", + ofs_x: atom.sprite.ofs_x + off_x, + ofs_y: atom.sprite.ofs_y + off_y, + ..atom.sprite + }); + } + + if atom.get_var("opened", objtree).to_bool() { + if let Constant::String(lid_icon_state) = atom.get_var("lid_icon_state", objtree) { + let lid_icon = atom.get_var_notnull("lid_icon", objtree).and_then(|lid_icon| lid_icon.as_path_str()); + let off_x = atom.get_var("lid_w", objtree).to_int().unwrap_or(0); + let off_y = atom.get_var("lid_z", objtree).to_int().unwrap_or(0); + overlays.push(Sprite { + icon: lid_icon.unwrap_or(atom.sprite.icon), + icon_state: lid_icon_state, + ofs_x: atom.sprite.ofs_x + off_x, + ofs_y: atom.sprite.ofs_y + off_y, + ..atom.sprite + }); } - if atom.get_var("secure", objtree).to_bool() - && !atom.get_var("broken", objtree).to_bool() - { - if atom.get_var("locked", objtree).to_bool() { - add_to(overlays, atom, "locked"); + } + } else if atom.istype("/obj/structure/closet/") { + if atom.get_var("enable_door_overlay", objtree).to_bool() { + // closet doors + if atom.get_var("opened", objtree).to_bool() { + let var = if atom.get_var("icon_door_override", objtree).to_bool() { + "icon_door" } else { - add_to(overlays, atom, "unlocked"); + "icon_state" + }; + if let Constant::String(door) = atom.get_var(var, objtree) { + add_to( + overlays, + atom, + bumpalo::format!(in bump, "{}_open", door).into_bump_str(), + ); + } + } else { + if atom.get_var("has_closed_overlay", objtree).to_bool() { + if let Constant::String(door) = atom + .get_var_notnull("icon_door", objtree) + .unwrap_or_else(|| atom.get_var("icon_state", objtree)) + { + add_to( + overlays, + atom, + bumpalo::format!(in bump, "{}_door", door).into_bump_str(), + ); + } + } + if atom.get_var("welded", objtree).to_bool() { + add_to(overlays, atom, "welded"); + } + if atom.get_var("secure", objtree).to_bool() + && !atom.get_var("broken", objtree).to_bool() + { + if atom.get_var("locked", objtree).to_bool() { + add_to(overlays, atom, "locked"); + } else { + add_to(overlays, atom, "unlocked"); + } } } } @@ -439,7 +487,7 @@ impl RenderPass for Overlays { ..atom.sprite }) } - } else { + } else if atom.get_var("can_be_glass", objtree).to_bool() { add_to(overlays, atom, "fill_closed"); } } else if atom.istype("/obj/machinery/power/apc/") { @@ -493,8 +541,7 @@ impl RenderPass for Pretty { } } else if atom.istype("/obj/machinery/firealarm/") { add_to(overlays, atom, "fire_overlay"); - add_to(overlays, atom, "fire_0"); - add_to(overlays, atom, "fire_off"); + add_to(overlays, atom, "fire_disabled"); } else if atom.istype("/obj/structure/tank_dispenser/") { if let &Constant::Float(oxygen) = atom.get_var("oxygentanks", objtree) { match oxygen as i32 { diff --git a/crates/dmm-tools/src/render_passes/random.rs b/crates/dmm-tools/src/render_passes/random.rs index ca8c0541..cd2354f4 100644 --- a/crates/dmm-tools/src/render_passes/random.rs +++ b/crates/dmm-tools/src/render_passes/random.rs @@ -98,31 +98,52 @@ impl RenderPass for Random { ) { let mut rng = rand::rng(); - const CONTRABAND_POSTERS: u32 = 44; - const LEGIT_POSTERS: u32 = 35; + // small selection of contraband and legit poster icon states + // in theory this could iterate through subtypes of /obj/structure/sign/poster, + // but i am far too lazy for that ~lucy + const CONTRABAND_POSTERS: &[&str] = &[ + "aspev_syndie", + "microwave", + "singletank_bomb", + "kudzu", + "free_drone", + "lusty_xenomorph", + ]; + const LEGIT_POSTERS: &[&str] = &[ + "aspev_hardhat", + "aspev_piping", + "aspev_meth", + "aspev_epi", + "aspev_delam", + "cleanliness", + "help_others", + "build", + "bless_this_spess", + "science", + "ue_no", + "safety_internals", + "safety_eye_protection", + ]; if atom.istype("/obj/structure/sign/poster/contraband/random/") { - sprite.icon_state = - bumpalo::format!(in bump, "poster{}", rng.random_range(1..=CONTRABAND_POSTERS)) - .into_bump_str(); + sprite.icon_state = CONTRABAND_POSTERS.choose(&mut rng).unwrap(); } else if atom.istype("/obj/structure/sign/poster/official/random/") { - sprite.icon_state = - bumpalo::format!(in bump, "poster{}_legit", rng.random_range(1..=LEGIT_POSTERS)) - .into_bump_str(); + sprite.icon_state = LEGIT_POSTERS.choose(&mut rng).unwrap(); } else if atom.istype("/obj/structure/sign/poster/random/") { - let i = 1 + rng.random_range(0..CONTRABAND_POSTERS + LEGIT_POSTERS); - if i <= CONTRABAND_POSTERS { - sprite.icon_state = bumpalo::format!(in bump, "poster{}", i).into_bump_str(); + let poster_type = if rng.random_ratio( + CONTRABAND_POSTERS.len() as u32, + (CONTRABAND_POSTERS.len() + LEGIT_POSTERS.len()) as u32, + ) { + CONTRABAND_POSTERS } else { - sprite.icon_state = - bumpalo::format!(in bump, "poster{}_legit", i - CONTRABAND_POSTERS) - .into_bump_str(); - } + LEGIT_POSTERS + }; + sprite.icon_state = poster_type.choose(&mut rng).unwrap(); } else if atom.istype("/obj/item/kirbyplants/random/") || atom.istype("/obj/item/twohanded/required/kirbyplants/random/") { - sprite.icon = "icons/obj/flora/plants.dmi"; - let random = rng.random_range(0..26); + sprite.icon = "icons/obj/fluff/flora/plants.dmi"; + let random = rng.random_range(0..=29); if random == 0 { sprite.icon_state = "applebush"; } else { diff --git a/crates/dmm-tools/src/render_passes/smart_cables.rs b/crates/dmm-tools/src/render_passes/smart_cables.rs index bc6c717a..e526f24a 100644 --- a/crates/dmm-tools/src/render_passes/smart_cables.rs +++ b/crates/dmm-tools/src/render_passes/smart_cables.rs @@ -14,7 +14,7 @@ impl RenderPass for SmartCables { output: &mut Vec>, bump: &'a bumpalo::Bump, ) -> bool { - if !atom.istype("/obj/structure/cable/") { + if !atom.istype("/obj/structure/cable/") || atom.istype("/obj/structure/cable/multilayer") { return true; }