diff --git a/_maps/map_files/dun_world/dun_world.dmm b/_maps/map_files/dun_world/dun_world.dmm index 36cf52a08d..bd5511ef50 100644 --- a/_maps/map_files/dun_world/dun_world.dmm +++ b/_maps/map_files/dun_world/dun_world.dmm @@ -17653,8 +17653,8 @@ /turf/open/floor/rogue/tile/masonic/inverted, /area/provincial/underground/dungeon/vulnafir) "fUg" = ( -/obj/item/roguegem, -/obj/item/roguegem, +/obj/item/roguegem/ruby, +/obj/item/roguegem/ruby, /obj/item/roguegem/violet, /obj/item/roguegem/yellow, /obj/item/roguegem/green, @@ -34692,6 +34692,10 @@ icon_state = "greenstone" }, /area/provincial/indoors/town/province_keep/wine_cellar) +"lnB" = ( +/obj/effect/landmark/map_load_mark/south_mine_road, +/turf/closed/mineral/random/rogue/high, +/area/provincial/underground/field) "lnC" = ( /obj/structure/chair/wood/rogue/chair3, /obj/effect/decal/cleanable/dirt/cobweb/cobweb2, @@ -72667,7 +72671,7 @@ /area/provincial/underground/dungeon/old_ruin) "xUK" = ( /obj/structure/table/vtable, -/obj/item/roguegem{ +/obj/item/roguegem/ruby{ pixel_y = 10 }, /obj/item/roguegem/blue, @@ -240180,7 +240184,7 @@ ojJ nXg nXg nXg -nXg +lnB nXg nXg nXg diff --git a/_maps/map_files/otherz/dungeon.json b/_maps/map_files/otherz/dungeon.json new file mode 100644 index 0000000000..d7c4e4a890 --- /dev/null +++ b/_maps/map_files/otherz/dungeon.json @@ -0,0 +1,6 @@ +{ + "map_name": "Dungeon Map", + "map_path": "map_files/otherz", + "map_file": "dungeon.dmm", + "traits": [{ "Up": true }, { "Down": true }] +} diff --git a/_maps/map_files/templates/mining/south_mine_01.dmm b/_maps/map_files/templates/mining/south_mine_01.dmm new file mode 100644 index 0000000000..c63cf0e08c --- /dev/null +++ b/_maps/map_files/templates/mining/south_mine_01.dmm @@ -0,0 +1,18 @@ +"a" = (/turf/closed/mineral/random/rogue/high,/area/provincial) +"g" = (/obj/item/rogueweapon/pick,/obj/effect/decal/remains/human,/turf/open/floor/rogue/cobblerock,/area/provincial) +"n" = (/turf/open/floor/rogue/cobblerock,/area/provincial) +"p" = (/turf/closed/mineral/random/rogue/med,/area/provincial) +"A" = (/turf/closed/mineral/random/rogue,/area/provincial) + +(1,1,1) = {" +ppppppAAAA +appppppAAA +aapppppAAA +aappppppAA +naganpppAA +ngaganppAA +aaaaappppA +aaaaaapppp +aaaaaapppp +aaaaaaappp +"} diff --git a/_maps/map_files/templates/mining/south_mine_02.dmm b/_maps/map_files/templates/mining/south_mine_02.dmm new file mode 100644 index 0000000000..d7edb2c0cf --- /dev/null +++ b/_maps/map_files/templates/mining/south_mine_02.dmm @@ -0,0 +1,19 @@ +"a" = (/turf/closed/mineral/random/rogue/high,/area/provincial) +"g" = (/obj/item/rogueweapon/pick,/mob/living/simple_animal/hostile/rogue/skeleton/axe,/turf/open/floor/rogue/cobblerock,/area/provincial) +"n" = (/turf/open/floor/rogue/cobblerock,/area/provincial) +"p" = (/turf/closed/mineral/random/rogue/med,/area/provincial) +"A" = (/turf/closed/mineral/random/rogue,/area/provincial) +"X" = (/mob/living/simple_animal/hostile/rogue/skeleton/axe,/turf/open/floor/rogue/cobblerock,/area/provincial) + +(1,1,1) = {" +ppppppAAAA +appppppAAA +aapppppAAA +aappppppAA +naganpppAA +ngaXanppAA +aaaaappppA +aaaaaapppp +aaaaaapppp +aaaaaaappp +"} diff --git a/_maps/map_files/templates/mining/south_mine_03.dmm b/_maps/map_files/templates/mining/south_mine_03.dmm new file mode 100644 index 0000000000..5e629b5bd4 --- /dev/null +++ b/_maps/map_files/templates/mining/south_mine_03.dmm @@ -0,0 +1,19 @@ +"a" = (/turf/closed/mineral/random/rogue/high,/area/provincial) +"g" = (/obj/item/rogueweapon/pick,/obj/effect/decal/remains/human,/turf/open/floor/rogue/cobblerock,/area/provincial) +"n" = (/turf/open/floor/rogue/cobblerock,/area/provincial) +"p" = (/turf/closed/mineral/random/rogue/med,/area/provincial) +"A" = (/turf/closed/mineral/random/rogue,/area/provincial) +"O" = (/obj/item/rogueweapon/pick,/obj/effect/decal/remains/human,/mob/living/simple_animal/hostile/retaliate/rogue/bigrat,/turf/open/floor/rogue/cobblerock,/area/provincial) + +(1,1,1) = {" +ppppppAAAA +appppppAAA +aapppppAAA +aappppppAA +naOanpppAA +nOaganppAA +aaaaappppA +aaaaaapppp +aaaaaapppp +aaaaaaappp +"} diff --git a/_maps/map_files/templates/mining/south_mine_default.dmm b/_maps/map_files/templates/mining/south_mine_default.dmm new file mode 100644 index 0000000000..adc3bf9fd0 --- /dev/null +++ b/_maps/map_files/templates/mining/south_mine_default.dmm @@ -0,0 +1,17 @@ +"a" = (/turf/closed/mineral/random/rogue/high,/area/provincial) +"n" = (/turf/open/floor/rogue/cobblerock,/area/provincial) +"p" = (/turf/closed/mineral/random/rogue/med,/area/provincial) +"A" = (/turf/closed/mineral/random/rogue,/area/provincial) + +(1,1,1) = {" +ppppppAAAA +appppppAAA +aapppppAAA +aappppppAA +nananpppAA +nnananppAA +aaaaappppA +aaaaaapppp +aaaaaapppp +aaaaaaappp +"} diff --git a/_maps/templates/mining.dm b/_maps/templates/mining.dm new file mode 100644 index 0000000000..0bda92035e --- /dev/null +++ b/_maps/templates/mining.dm @@ -0,0 +1,31 @@ +/obj/effect/spawner/lootdrop/roguetown/miningtunnels + name = "sewer spawner" + loot = list() + lootcount = 0 + +/////////////////////// +/// Southern Road /// +/// Size: X:10 Y:10 /// +/////////////////////// + +/obj/effect/landmark/map_load_mark/south_mine_road + name = "South Mines Road" + templates = list("south_mine_road_1", "south_mine_road_2", "south_mine_road_3") + +/// just corpses +/datum/map_template/south_mine_road_1 + name = "South Mines Road Variant 1" + id = "south_mine_road_1" + mappath = "_maps/map_files/templates/mining/south_mine_01.dmm" + +/// skeletons +/datum/map_template/south_mine_road_2 + name = "South Mines Road Variant 2" + id = "south_mine_road_2" + mappath = "_maps/map_files/templates/mining/south_mine_02.dmm" + +/// big rats +/datum/map_template/south_mine_road_3 + name = "South Mines Road Variant 3" + id = "south_mine_road_3" + mappath = "_maps/map_files/templates/mining/south_mine_03.dmm" diff --git a/code/__DEFINES/_tick.dm b/code/__DEFINES/_tick.dm index 0419468a0e..05b3a5717c 100644 --- a/code/__DEFINES/_tick.dm +++ b/code/__DEFINES/_tick.dm @@ -26,3 +26,8 @@ #define RUNNING_BEFORE_MASTER ( Master.last_run != null && Master.last_run != world.time ) /// Returns true if a verb ought to yield to the MC (IE: queue up to be processed by a subsystem) #define VERB_SHOULD_YIELD ( TICK_CHECK || RUNNING_BEFORE_MASTER ) + +/// runs stoplag if tick_usage is above the limit +#define CHECK_TICK_LOW ( TICK_CHECK_LOW ? stoplag() : 0 ) +///like TICK_CHECK but for half the budget +#define TICK_CHECK_LOW ( TICK_USAGE > (Master.current_ticklimit * 0.5)) diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index 82df00623a..66de9d5f4d 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -16,8 +16,32 @@ /// Return value to cancel attaching #define ELEMENT_INCOMPATIBLE 1 -/// /datum/element flags -#define ELEMENT_DETACH (1 << 0) +/// fires on the target datum when an element is attached to it (/datum/element) +#define COMSIG_ELEMENT_ATTACH "element_attach" +/// fires on the target datum when an element is detached from it (/datum/element) +#define COMSIG_ELEMENT_DETACH "element_detach" + +/// before a datum's Destroy() is called: (force), returning a nonzero value will cancel the qdel operation +/// you should only be using this if you want to block deletion +/// that's the only functional difference between it and COMSIG_QDELETING, outside setting QDELETING to detect +#define COMSIG_PREQDELETED "parent_preqdeleted" +/// just before a datum's Destroy() is called: (force), at this point none of the other components chose to interrupt qdel and Destroy will be called +#define COMSIG_QDELETING "parent_qdeleting" + +// /datum/element flags +/// Causes the detach proc to be called when the host object is being deleted. +/// Should only be used if you need to perform cleanup not related to the host object. +/// You do not need this if you are only unregistering signals, for instance. +/// You would need it if you are doing something like removing the target from a processing list. +#define ELEMENT_DETACH_ON_HOST_DESTROY (1 << 0) +/** + * Only elements created with the same arguments given after `id_arg_index` share an element instance + * The arguments are the same when the text and number values are the same and all other values have the same ref + */ +#define ELEMENT_BESPOKE (1 << 1) +/// Causes all detach arguments to be passed to detach instead of only being used to identify the element +/// When this is used your Detach proc should have the same signature as your Attach proc +#define ELEMENT_COMPLEX_DETACH (1 << 2) // How multiple components of the exact same type are handled in the same datum /// old component is deleted (default) @@ -26,8 +50,17 @@ #define COMPONENT_DUPE_ALLOWED 1 /// new component is deleted #define COMPONENT_DUPE_UNIQUE 2 +/** + * Component uses source tracking to manage adding and removal logic. + * Add a source/spawn to/the component by using AddComponentFrom(source, component_type, args...) + * Removing the last source will automatically remove the component from the parent. + * Arguments will be passed to on_source_add(source, args...); ensure that Initialize and on_source_add have the same signature. + */ +#define COMPONENT_DUPE_SOURCES 3 /// old component is given the initialization args of the new #define COMPONENT_DUPE_UNIQUE_PASSARGS 4 +/// each component of the same type is consulted as to whether the duplicate should be allowed +#define COMPONENT_DUPE_SELECTIVE 5 // All signals. Format: // When the signal is called: (signal arguments) @@ -38,6 +71,8 @@ // start global signals with "!", this used to be necessary but now it's just a formatting choice /// from base of datum/controller/subsystem/mapping/proc/add_new_zlevel(): (list/args) #define COMSIG_GLOB_NEW_Z "!new_z" +/// sent after world.maxx and/or world.maxy are expanded: (has_exapnded_world_maxx, has_expanded_world_maxy) +#define COMSIG_GLOB_EXPANDED_WORLD_BOUNDS "!expanded_world_bounds" /// called after a successful var edit somewhere in the world: (list/args) #define COMSIG_GLOB_VAR_EDIT "!var_edit" /// mob was created somewhere : (mob) diff --git a/code/__DEFINES/dcs/flags.dm b/code/__DEFINES/dcs/flags.dm index 763c561498..a3072e32dd 100644 --- a/code/__DEFINES/dcs/flags.dm +++ b/code/__DEFINES/dcs/flags.dm @@ -18,10 +18,10 @@ * Only elements created with the same arguments given after `id_arg_index` share an element instance * The arguments are the same when the text and number values are the same and all other values have the same ref */ -#define ELEMENT_BESPOKE (1 << 1) +//#define ELEMENT_BESPOKE (1 << 1) // Causes all detach arguments to be passed to detach instead of only being used to identify the element /// When this is used your Detach proc should have the same signature as your Attach proc -#define ELEMENT_COMPLEX_DETACH (1 << 2) +//#define ELEMENT_COMPLEX_DETACH (1 << 2) /* Dup commend out // How multiple components of the exact same type are handled in the same datum diff --git a/code/__DEFINES/dcs/helpers.dm b/code/__DEFINES/dcs/helpers.dm new file mode 100644 index 0000000000..f231ff0e64 --- /dev/null +++ b/code/__DEFINES/dcs/helpers.dm @@ -0,0 +1,14 @@ +/// A wrapper for _AddElement that allows us to pretend we're using normal named arguments +#define AddElement(arguments...) _AddElement(list(##arguments)) +/// A wrapper for _RemoveElement that allows us to pretend we're using normal named arguments +#define RemoveElement(arguments...) _RemoveElement(list(##arguments)) + +/// A wrapper for _AddComponent that allows us to pretend we're using normal named arguments +#define AddComponent(arguments...) _AddComponent(list(##arguments)) + +/// A wrapper for _AddComonent that passes in a source. +/// Necessary if dupe_mode is set to COMPONENT_DUPE_SOURCES. +#define AddComponentFrom(source, arguments...) _AddComponent(list(##arguments), source) + +/// A wrapper for _LoadComponent that allows us to pretend we're using normal named arguments +#define LoadComponent(arguments...) _LoadComponent(list(##arguments)) diff --git a/code/__DEFINES/dcs/signals/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum.dm deleted file mode 100644 index 534b51e801..0000000000 --- a/code/__DEFINES/dcs/signals/signals_datum.dm +++ /dev/null @@ -1,6 +0,0 @@ -/// before a datum's Destroy() is called: (force), returning a nonzero value will cancel the qdel operation -/// you should only be using this if you want to block deletion -/// that's the only functional difference between it and COMSIG_QDELETING, outside setting QDELETING to detect -#define COMSIG_PREQDELETED "parent_preqdeleted" -/// just before a datum's Destroy() is called: (force), at this point none of the other components chose to interrupt qdel and Destroy will be called -#define COMSIG_QDELETING "parent_qdeleting" diff --git a/code/__DEFINES/dcs/signals/signals_mob.dm b/code/__DEFINES/dcs/signals/signals_mob.dm index 0a04df81eb..6865105f0c 100644 --- a/code/__DEFINES/dcs/signals/signals_mob.dm +++ b/code/__DEFINES/dcs/signals/signals_mob.dm @@ -8,6 +8,7 @@ ///before attackingtarget has happened, source is the attacker and target is the attacked #define COMSIG_HOSTILE_PRE_ATTACKINGTARGET "hostile_pre_attackingtarget" +#define COMPONENT_HOSTILE_NO_PREATTACK (1<<0) //cancel the attack, only works before attack happens //#define COMPONENT_HOSTILE_NO_ATTACK COMPONENT_CANCEL_ATTACK_CHAIN //cancel the attack, only works before attack happens ///after attackingtarget has happened, source is the attacker and target is the attacked, extra argument for if the attackingtarget was successful #define COMSIG_HOSTILE_POST_ATTACKINGTARGET "hostile_post_attackingtarget" diff --git a/code/__DEFINES/dcs/signals/signals_spatial_grid.dm b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm new file mode 100644 index 0000000000..82e69dfcdf --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm @@ -0,0 +1,6 @@ +//spatial grid signals + +///Called from base of /datum/controller/subsystem/spatial_grid/proc/enter_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_ENTERED(contents_type) "spatial_grid_cell_entered_[contents_type]" +///Called from base of /datum/controller/subsystem/spatial_grid/proc/exit_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_EXITED(contents_type) "spatial_grid_cell_exited_[contents_type]" diff --git a/code/__DEFINES/dcs/signals_atoms/signals_atom.dm b/code/__DEFINES/dcs/signals_atoms/signals_atom.dm index 9b32abf0c9..79d6ee06d1 100644 --- a/code/__DEFINES/dcs/signals_atoms/signals_atom.dm +++ b/code/__DEFINES/dcs/signals_atoms/signals_atom.dm @@ -1,2 +1,10 @@ ///from base of atom/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) #define COMSIG_ATOM_HITBY "atom_hitby" +///from base of atom/attackby(): (/obj/item, /mob/living, params) +#define COMSIG_ATOM_ATTACKBY "atom_attackby" +///from base of atom/examine(): (/mob, list/examine_text) +#define COMSIG_ATOM_EXAMINE "atom_examine" +///from base of atom/examine_tags(): (/mob, list/examine_tags) +#define COMSIG_ATOM_EXAMINE_TAGS "atom_examine_tags" +///from base of atom/examine_more(): (/mob, examine_list) +#define COMSIG_ATOM_EXAMINE_MORE "atom_examine_more" diff --git a/code/__DEFINES/important_recursive_contents.dm b/code/__DEFINES/important_recursive_contents.dm new file mode 100644 index 0000000000..79abb67d18 --- /dev/null +++ b/code/__DEFINES/important_recursive_contents.dm @@ -0,0 +1,9 @@ +///the area channel of the important_recursive_contents list, everything in here will be sent a signal when their last holding object changes areas +#define RECURSIVE_CONTENTS_AREA_SENSITIVE "recursive_contents_area_sensitive" +///the hearing channel of the important_recursive_contents list, everything in here will count as a hearing atom +#define RECURSIVE_CONTENTS_HEARING_SENSITIVE "recursive_contents_hearing_sensitive" +///the client mobs channel of the important_recursive_contents list, everything in here will be a mob with an attached client +///this is given to both a clients mob, and a clients eye, both point to the clients mob +#define RECURSIVE_CONTENTS_CLIENT_MOBS "recursive_contents_client_mobs" +///the parent of storage components currently shown to some client mob get this. gets removed when nothing is viewing the parent +#define RECURSIVE_CONTENTS_ACTIVE_STORAGE "recursive_contents_active_storage" diff --git a/code/__DEFINES/maps.dm b/code/__DEFINES/maps.dm index 2ca422f633..0b478278e4 100644 --- a/code/__DEFINES/maps.dm +++ b/code/__DEFINES/maps.dm @@ -20,6 +20,9 @@ Multi-Z stations are supported and multi-Z mining and away missions would require only minor tweaks. */ +/// A map key that corresponds to being one exclusively for Space. +#define SPACE_KEY "space" + // helpers for modifying jobs, used in various job_changes.dm files #define MAP_JOB_CHECK if(SSmapping.config.map_name != JOB_MODIFICATION_MAP_NAME) { return; } #define MAP_JOB_CHECK_BASE if(SSmapping.config.map_name != JOB_MODIFICATION_MAP_NAME) { return ..(); } diff --git a/code/__DEFINES/qdel.dm b/code/__DEFINES/qdel.dm index c55b761298..4723e0b2d4 100644 --- a/code/__DEFINES/qdel.dm +++ b/code/__DEFINES/qdel.dm @@ -31,7 +31,7 @@ #define GC_DEL_QUEUE (10 SECONDS) #define QDELING(X) (X.gc_destroyed) -#define QDELETED(X) (!X || QDELING(X)) +#define QDELETED(X) (isnull(X) || QDELING(X)) #define QDESTROYING(X) (!X || X.gc_destroyed == GC_CURRENTLY_BEING_QDELETED) //Theses were taken from Vanderlin, some are commented out if they are dupped elsewhere diff --git a/code/__DEFINES/sight.dm b/code/__DEFINES/sight.dm index 5cac290082..875625f993 100644 --- a/code/__DEFINES/sight.dm +++ b/code/__DEFINES/sight.dm @@ -1,3 +1,5 @@ +#define INVISIBILITY_NONE 0 + #define SEE_INVISIBLE_MINIMUM 5 #define INVISIBILITY_LIGHTING 20 diff --git a/code/__DEFINES/spatial_gridmap.dm b/code/__DEFINES/spatial_gridmap.dm new file mode 100644 index 0000000000..97a6f99153 --- /dev/null +++ b/code/__DEFINES/spatial_gridmap.dm @@ -0,0 +1,55 @@ +/// each cell in a spatial_grid is this many turfs in length and width (with world.max(x or y) being 255, 15 of these fit on each side of a z level) +#define SPATIAL_GRID_CELLSIZE 17 +/// Takes a coordinate, and spits out the spatial grid index (x or y) it's inside +#define GET_SPATIAL_INDEX(coord) ROUND_UP((coord) / SPATIAL_GRID_CELLSIZE) +/// changes the cell_(x or y) vars on /datum/spatial_grid_cell to the x or y coordinate on the map for the LOWER LEFT CORNER of the grid cell. +/// index is from 1 to SPATIAL_GRID_CELLS_PER_SIDE +#define GRID_INDEX_TO_COORDS(index) ((((index) - 1) * SPATIAL_GRID_CELLSIZE) + 1) +/// number of grid cells per x or y side of all z levels. pass in world.maxx or world.maxy +#define SPATIAL_GRID_CELLS_PER_SIDE(world_bounds) GET_SPATIAL_INDEX(world_bounds) + +//grid contents channels + +///everything that is hearing sensitive is stored in this channel +#define SPATIAL_GRID_CONTENTS_TYPE_HEARING RECURSIVE_CONTENTS_HEARING_SENSITIVE +///every movable that has a client in it is stored in this channel +#define SPATIAL_GRID_CONTENTS_TYPE_CLIENTS RECURSIVE_CONTENTS_CLIENT_MOBS +///all atmos machines are stored in this channel (I'm sorry kyler) +#define SPATIAL_GRID_CONTENTS_TYPE_ATMOS "spatial_grid_contents_type_atmos" + +#define ALL_CONTENTS_OF_CELL(cell) (cell.hearing_contents | cell.client_contents | cell.atmos_contents) + +///whether movable is itself or containing something which should be in one of the spatial grid channels. +#define HAS_SPATIAL_GRID_CONTENTS(movable) (movable.spatial_grid_key) + +// macros meant specifically to add/remove movables from the internal lists of /datum/spatial_grid_cell, +// when empty they become references to a single list in SSspatial_grid and when filled they become their own list +// this is to save memory without making them lazylists as that slows down iteration through them +#define GRID_CELL_ADD(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list += movable_or_list; \ + }; + +#define GRID_CELL_SET(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list |= movable_or_list; \ + }; + +//dont use these outside of SSspatial_grid's scope use the procs it has for this purpose +#define GRID_CELL_REMOVE(cell_contents_list, movable_or_list) \ + cell_contents_list -= movable_or_list; \ + if(!length(cell_contents_list)) {\ + cell_contents_list = dummy_list; \ + }; + +///remove from every list +#define GRID_CELL_REMOVE_ALL(cell, movable) \ + GRID_CELL_REMOVE(cell.hearing_contents, movable) \ + GRID_CELL_REMOVE(cell.client_contents, movable) \ + GRID_CELL_REMOVE(cell.atmos_contents, movable) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 0ae3c61b2e..409550cd76 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -119,7 +119,9 @@ #define INIT_ORDER_AI_CONTROLLERS 55 //So the controller can get the ref #define INIT_ORDER_TICKER 55 #define INIT_ORDER_MAPPING 50 +#define INIT_ORDER_DUNGEON 49 #define INIT_ORDER_NETWORKS 45 +#define INIT_ORDER_SPATIAL_GRID 43 #define INIT_ORDER_ECONOMY 40 #define INIT_ORDER_OUTPUTS 35 #define INIT_ORDER_ATOMS 30 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index f58ec6857f..4e4250f536 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -1,3 +1,6 @@ +#define SIGNAL_ADDTRAIT(trait_ref) ("addtrait " + trait_ref) +#define SIGNAL_REMOVETRAIT(trait_ref) ("removetrait " + trait_ref) + // ROGUETRAITS (description when rmb skills button) #define TRAIT_WEBWALK "Webwalker" #define TRAIT_NOSTINK "Dead Nose" @@ -377,6 +380,8 @@ GLOBAL_LIST_INIT(roguetraits, list( } while (0) #define HAS_TRAIT(target, trait) (target.status_traits ? (target.status_traits[trait] ? TRUE : FALSE) : FALSE) #define HAS_TRAIT_FROM(target, trait, source) (target.status_traits ? (target.status_traits[trait] ? (source in target.status_traits[trait]) : FALSE) : FALSE) +#define HAS_TRAIT_FROM_ONLY(target, trait, source) (HAS_TRAIT(target, trait) && (source in target._status_traits[trait]) && (length(target.status_traits[trait]) == 1)) +#define HAS_TRAIT_NOT_FROM(target, trait, source) (HAS_TRAIT(target, trait) && (length(target.status_traits[trait] - source) > 0)) /* Remember to update _globalvars/traits.dm if you're adding/removing/renaming traits. @@ -587,3 +592,15 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai //for ai #define TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM "element-required" +/// Prevents usage of manipulation appendages (picking, holding or using items, manipulating storage). +#define TRAIT_HANDS_BLOCKED "handsblocked" +/// This mob should never close UI even if it doesn't have a client +#define TRAIT_PRESERVE_UI_WITHOUT_CLIENT "preserve_ui_without_client" +//important_recursive_contents traits +/* + * Used for movables that need to be updated, via COMSIG_ENTER_AREA and COMSIG_EXIT_AREA, when transitioning areas. + * Use [/atom/movable/proc/become_area_sensitive(trait_source)] to properly enable it. How you remove it isn't as important. + */ +#define TRAIT_AREA_SENSITIVE "area-sensitive" +///every hearing sensitive atom has this trait +#define TRAIT_HEARING_SENSITIVE "hearing_sensitive" diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 742a1745fa..ebe01f7968 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -18,6 +18,7 @@ #define LAZYINITLIST(L) if (!L) L = list() #define UNSETEMPTY(L) if (L && !length(L)) L = null +#define ASSOC_UNSETEMPTY(L, K) if (!length(L[K])) L -= K; #define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = list(); } } #define LAZYADD(L, I) if(!L) { L = list(); } L += I; ///This is used to add onto lazy assoc list when the value you're adding is a /list/. This one has extra safety over lazyaddassoc because the value could be null (and thus cant be used to += objects) @@ -30,36 +31,87 @@ #define LAZYSET(L, K, V) if(!L) { L = list(); } L[K] = V; #define LAZYLEN(L) length(L) #define LAZYCLEARLIST(L) if(L) L.Cut() +#define LAZYNULL(L) L = null #define SANITIZE_LIST(L) ( islist(L) ? L : list() ) #define reverseList(L) reverseRange(L.Copy()) -// binary search sorted insert -// IN: Object to be inserted -// LIST: List to insert object into -// TYPECONT: The typepath of the contents of the list -// COMPARE: The variable on the objects to compare -#define BINARY_INSERT(IN, LIST, TYPECONT, COMPARE) \ - var/__BIN_CTTL = length(LIST);\ - if(!__BIN_CTTL) {\ - LIST += IN;\ - } else {\ - var/__BIN_LEFT = 1;\ - var/__BIN_RIGHT = __BIN_CTTL;\ - var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ - var/##TYPECONT/__BIN_ITEM;\ - while(__BIN_LEFT < __BIN_RIGHT) {\ - __BIN_ITEM = LIST[__BIN_MID];\ - if(__BIN_ITEM.##COMPARE <= IN.##COMPARE) {\ - __BIN_LEFT = __BIN_MID + 1;\ - } else {\ - __BIN_RIGHT = __BIN_MID;\ +/// Passed into BINARY_INSERT to compare keys +#define COMPARE_KEY __BIN_LIST[__BIN_MID] +/// Passed into BINARY_INSERT to compare values +#define COMPARE_VALUE __BIN_LIST[__BIN_LIST[__BIN_MID]] + +/**** + * Binary search sorted insert + * INPUT: Object to be inserted + * LIST: List to insert object into + * TYPECONT: The typepath of the contents of the list + * COMPARE: The object to compare against, usualy the same as INPUT + * COMPARISON: The variable on the objects to compare + * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + var ##TYPECONT/__BIN_ITEM;\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(__BIN_ITEM.##COMPARISON <= COMPARE.##COMPARISON) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ };\ - __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = __BIN_ITEM.##COMPARISON > COMPARE.##COMPARISON ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ };\ - __BIN_ITEM = LIST[__BIN_MID];\ - __BIN_MID = __BIN_ITEM.##COMPARE > IN.##COMPARE ? __BIN_MID : __BIN_MID + 1;\ - LIST.Insert(__BIN_MID, IN);\ - } + } while(FALSE) + +#define SORT_FIRST_INDEX(list) (list[1]) +#define SORT_COMPARE_DIRECTLY(thing) (thing) +#define SORT_VAR_NO_TYPE(varname) var/varname +/**** + * Even more custom binary search sorted insert, using defines instead of vars + * INPUT: Item to be inserted + * LIST: List to insert INPUT into + * TYPECONT: A define setting the var to the typepath of the contents of the list + * COMPARE: The item to compare against, usualy the same as INPUT + * COMPARISON: A define that takes an item to compare as input, and returns their comparable value + * COMPTYPE: How should the list be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_DEFINE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + ##TYPECONT(__BIN_ITEM);\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(##COMPARISON(__BIN_ITEM) <= ##COMPARISON(COMPARE)) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = ##COMPARISON(__BIN_ITEM) > ##COMPARISON(COMPARE) ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) //Returns a list in plain english as a string /proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" ) @@ -622,3 +674,10 @@ GLOBAL_LIST_EMPTY(string_lists) return return GLOB.string_lists[string_id] = values + +/** + * Removes any null entries from the list + * Returns TRUE if the list had nulls, FALSE otherwise +**/ +/proc/list_clear_nulls(list/list_to_clear) + return (list_to_clear.RemoveAll(null) > 0) diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index 96b884559f..1ea34cfbda 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -14,68 +14,6 @@ return null return format_text ? format_text(A.name) : A.name -/proc/get_areas_in_range(dist=0, atom/center=usr) - if(!dist) - var/turf/T = get_turf(center) - return T ? list(T.loc) : list() - if(!center) - return list() - - var/list/turfs = RANGE_TURFS(dist, center) - var/list/areas = list() - for(var/V in turfs) - var/turf/T = V - areas |= T.loc - return areas - -/proc/get_adjacent_areas(atom/center) - . = list(get_area(get_ranged_target_turf(center, NORTH, 1)), - get_area(get_ranged_target_turf(center, SOUTH, 1)), - get_area(get_ranged_target_turf(center, EAST, 1)), - get_area(get_ranged_target_turf(center, WEST, 1))) - listclearnulls(.) - -/proc/get_open_turf_in_dir(atom/center, dir) - var/turf/open/T = get_ranged_target_turf(center, dir, 1) - if(istype(T)) - return T - -/proc/get_adjacent_open_turfs(atom/center) - . = list(get_open_turf_in_dir(center, NORTH), - get_open_turf_in_dir(center, SOUTH), - get_open_turf_in_dir(center, EAST), - get_open_turf_in_dir(center, WEST)) - listclearnulls(.) - -/proc/get_adjacent_open_areas(atom/center) - . = list() - var/list/adjacent_turfs = get_adjacent_open_turfs(center) - for(var/I in adjacent_turfs) - . |= get_area(I) - -// Like view but bypasses luminosity check - -/proc/get_hear(range, atom/source) - - var/lum = source.luminosity - source.luminosity = 6 - - var/list/heard = view(range, source) - source.luminosity = lum - - return heard - -/proc/alone_in_area(area/the_area, mob/must_be_alone, check_type = /mob/living/carbon) - var/area/our_area = get_area(the_area) - for(var/C in GLOB.alive_mob_list) - if(!istype(C, check_type)) - continue - if(C == must_be_alone) - continue - if(our_area == get_area(C)) - return 0 - return 1 - //We used to use linear regression to approximate the answer, but Mloc realized this was actually faster. //And lo and behold, it is, and it's more accurate to boot. /proc/cheap_hypotenuse(Ax,Ay,Bx,By) @@ -236,64 +174,6 @@ return found_mobs - -/proc/get_hearers_in_view(R, atom/source) - // Returns a list of hearers in view(R) from source (ignoring luminosity). Used in saycode. - var/turf/T = get_turf(source) - . = list() - - if(!T) - return - - var/list/processing_list = list() - if (R == 0) // if the range is zero, we know exactly where to look for, we can skip view - processing_list += T.contents // We can shave off one iteration by assuming turfs cannot hear - else // A variation of get_hear inlined here to take advantage of the compiler's fastpath for obj/mob in view - var/lum = T.luminosity - T.luminosity = 6 // This is the maximum luminosity - for(var/mob/M in view(R, T)) - processing_list += M - for(var/obj/O in view(R, T)) - processing_list += O - T.luminosity = lum - - while(processing_list.len) // recursive_hear_check inlined here - var/atom/A = processing_list[1] - if(A.flags_1 & HEAR_1) - . += A - processing_list.Cut(1, 2) - processing_list += A.contents - -/proc/inLineOfSight(X1,Y1,X2,Y2,Z=1,PX1=16.5,PY1=16.5,PX2=16.5,PY2=16.5) - var/turf/T - if(X1==X2) - if(Y1==Y2) - return 1 //Light cannot be blocked on same tile - else - var/s = SIGN(Y2-Y1) - Y1+=s - while(Y1!=Y2) - T=locate(X1,Y1,Z) - if(T.opacity) - return 0 - Y1+=s - else - var/m=(32*(Y2-Y1)+(PY2-PY1))/(32*(X2-X1)+(PX2-PX1)) - var/b=(Y1+PY1/32-0.015625)-m*(X1+PX1/32-0.015625) //In tiles - var/signX = SIGN(X2-X1) - var/signY = SIGN(Y2-Y1) - if(X1 0 + * because view() isnt a raycasting algorithm, this does not hold symmetry to it. something in view might not be hearable with this. + * if you want that use get_hearers_in_view() - however thats significantly more expensive + * + * * view_radius - what radius search circle we are using, worse performance as this increases but not as much as it used to + * * source - object at the center of our search area. everything in get_turf(source) is guaranteed to be part of the search area + */ +/proc/get_hearers_in_LOS(view_radius, atom/source, contents_type=RECURSIVE_CONTENTS_HEARING_SENSITIVE) + var/turf/center_turf = get_turf(source) + if(!center_turf) + return + + if(view_radius <= 0)//special case for if only source cares + . = list() + for(var/atom/movable/target as anything in center_turf) + var/list/hearing_contents = target.important_recursive_contents?[contents_type] + if(hearing_contents) + . += hearing_contents + return + + . = SSspatial_grid.orthogonal_range_search(source, contents_type, view_radius) + + for(var/atom/movable/target as anything in .) + var/turf/target_turf = get_turf(target) + + var/distance = get_dist(center_turf, target_turf) + + if(distance > view_radius) + . -= target + continue + + else if(distance < 2) //we should always be able to see something 0 or 1 tiles away + continue + + //this turf search algorithm is the worst scaling part of this proc, scaling worse than view() for small-moderate ranges and > 50 length contents_to_return + //luckily its significantly faster than view for large ranges in large spaces and/or relatively few contents_to_return + //i can do things that would scale better, but they would be slower for low volume searches which is the vast majority of the current workload + //maybe in the future a high volume algorithm would be worth it + var/turf/inbetween_turf = center_turf + + //this is the lowest overhead way of doing a loop in dm other than a goto. distance is guaranteed to be >= steps taken to target by this algorithm + for(var/step_counter in 1 to distance) + inbetween_turf = get_step_towards(inbetween_turf, target_turf) + + if(inbetween_turf == target_turf)//we've gotten to target's turf without returning due to turf opacity, so we must be able to see target + break + + if(inbetween_turf.opacity) //this turf or something on it is opaque so we cant see through it + . -= target + break + +//Used when converting pixels to tiles to make them accurate +#define OFFSET_X (0.5 / world.icon_size) +#define OFFSET_Y (0.5 / world.icon_size) + +///Calculate if two atoms are in sight, returns TRUE or FALSE +/proc/inLineOfSight(X1,Y1,X2,Y2,Z=1,PX1=16.5,PY1=16.5,PX2=16.5,PY2=16.5) + var/turf/current_turf + if(X1 == X2) + if(Y1 == Y2) + return TRUE //Light cannot be blocked on same tile + else + var/sign = SIGN(Y2-Y1) + Y1 += sign + while(Y1 != Y2) + current_turf = locate(X1, Y1, Z) + if(current_turf.opacity) + return FALSE + Y1 += sign + else + //This looks scary but we're just calculating a linear function (y = mx + b) + + //m = y/x + var/m = (world.icon_size*(Y2-Y1) + (PY2-PY1)) / (world.icon_size*(X2-X1) + (PX2-PX1))//In pixels + + //b = y - mx + var/b = (Y1 + PY1/world.icon_size - OFFSET_Y) - m*(X1 + PX1/world.icon_size - OFFSET_X)//In tiles + + var/signX = SIGN(X2-X1) + var/signY = SIGN(Y2-Y1) + if(X1 < X2) + b += m + while(X1 != X2 || Y1 != Y2) + if(round(m*X1 + b - Y1)) // Basically, if y >= mx+b + Y1 += signY //Line exits tile vertically + else + X1 += signX //Line exits tile horizontally + current_turf = locate(X1, Y1, Z) + if(current_turf.opacity) + return FALSE + return TRUE + +#undef OFFSET_X +#undef OFFSET_Y + +/proc/is_in_sight(atom/first_atom, atom/second_atom) + var/turf/first_turf = get_turf(first_atom) + var/turf/second_turf = get_turf(second_atom) + + if(!first_turf || !second_turf) + return FALSE + + return inLineOfSight(first_turf.x, first_turf.y, second_turf.x, second_turf.y, first_turf.z) + +///Returns all atoms present in a circle around the center +/proc/circle_range(center = usr,radius = 3) + + var/turf/center_turf = get_turf(center) + var/list/atoms = new/list() + var/rsq = radius * (radius + 0.5) + + for(var/atom/checked_atom as anything in range(radius, center_turf)) + var/dx = checked_atom.x - center_turf.x + var/dy = checked_atom.y - center_turf.y + if(dx * dx + dy * dy <= rsq) + atoms += checked_atom + + return atoms + +///Returns all atoms present in a circle around the center but uses view() instead of range() (Currently not used) +/proc/circle_view(center=usr,radius=3) + + var/turf/center_turf = get_turf(center) + var/list/atoms = new/list() + var/rsq = radius * (radius + 0.5) + + for(var/atom/checked_atom as anything in view(radius, center_turf)) + var/dx = checked_atom.x - center_turf.x + var/dy = checked_atom.y - center_turf.y + if(dx * dx + dy * dy <= rsq) + atoms += checked_atom + + return atoms + +///Returns the distance between two atoms +/proc/get_dist_euclidean(atom/first_location, atom/second_location) + var/dx = first_location.x - second_location.x + var/dy = first_location.y - second_location.y + + var/dist = sqrt(dx ** 2 + dy ** 2) + + return dist + +///Returns a list of turfs around a center based on RANGE_TURFS() +/proc/circle_range_turfs(center = usr, radius = 3) + + var/turf/center_turf = get_turf(center) + var/list/turfs = new/list() + var/rsq = radius * (radius + 0.5) + + for(var/turf/checked_turf as anything in RANGE_TURFS(radius, center_turf)) + var/dx = checked_turf.x - center_turf.x + var/dy = checked_turf.y - center_turf.y + if(dx * dx + dy * dy <= rsq) + turfs += checked_turf + return turfs + +///Returns a list of turfs around a center based on view() +/proc/circle_view_turfs(center=usr,radius=3) //Is there even a diffrence between this proc and circle_range_turfs()? // Yes + var/turf/center_turf = get_turf(center) + var/list/turfs = new/list() + var/rsq = radius * (radius + 0.5) + + for(var/turf/checked_turf in view(radius, center_turf)) + var/dx = checked_turf.x - center_turf.x + var/dy = checked_turf.y - center_turf.y + if(dx * dx + dy * dy <= rsq) + turfs += checked_turf + return turfs + +///Returns the list of turfs around the outside of a center based on RANGE_TURFS() +/proc/border_diamond_range_turfs(atom/center = usr, radius = 3) + var/turf/center_turf = get_turf(center) + var/list/turfs = list() + + for(var/turf/checked_turf as anything in RANGE_TURFS(radius, center_turf)) + var/dx = checked_turf.x - center_turf.x + var/dy = checked_turf.y - center_turf.y + var/abs_sum = abs(dx) + abs(dy) + if(abs_sum == radius) + turfs += checked_turf + return turfs + +///Returns a slice of a list of turfs, defined by the ones that are inside the inner/outer angle's bounds +/proc/slice_off_turfs(atom/center, list/turf/turfs, inner_angle, outer_angle) + var/turf/center_turf = get_turf(center) + var/list/sliced_turfs = list() + + for(var/turf/checked_turf as anything in turfs) + var/angle_to = Get_Angle(center_turf, checked_turf) + if(angle_to < inner_angle || angle_to > outer_angle) + continue + sliced_turfs += checked_turf + return sliced_turfs + +/** + * Get a bounding box of a list of atoms. + * + * Arguments: + * - atoms - List of atoms. Can accept output of view() and range() procs. + * + * Returns: list(x1, y1, x2, y2) + */ +/proc/get_bbox_of_atoms(list/atoms) + var/list/list_x = list() + var/list/list_y = list() + for(var/_a in atoms) + var/atom/a = _a + list_x += a.x + list_y += a.y + return list( + min(list_x), + min(list_y), + max(list_x), + max(list_y)) + +/// Like view but bypasses luminosity check +/proc/get_hear(range, atom/source) + var/lum = source.luminosity + source.luminosity = 6 + + . = view(range, source) + source.luminosity = lum + +///Returns the open turf next to the center in a specific direction +/proc/get_open_turf_in_dir(atom/center, dir) + var/turf/open/get_turf = get_step(center, dir) + if(istype(get_turf)) + return get_turf + +///Returns a list with all the adjacent open turfs. Clears the list of nulls in the end. +/proc/get_adjacent_open_turfs(atom/center) + var/list/hand_back = list() + // Inlined get_open_turf_in_dir, just to be fast + var/turf/open/new_turf = get_step(center, NORTH) + if(istype(new_turf)) + hand_back += new_turf + new_turf = get_step(center, SOUTH) + if(istype(new_turf)) + hand_back += new_turf + new_turf = get_step(center, EAST) + if(istype(new_turf)) + hand_back += new_turf + new_turf = get_step(center, WEST) + if(istype(new_turf)) + hand_back += new_turf + return hand_back + +///Returns a list with all the adjacent areas by getting the adjacent open turfs +/proc/get_adjacent_open_areas(atom/center) + . = list() + var/list/adjacent_turfs = get_adjacent_open_turfs(center) + for(var/near_turf in adjacent_turfs) + . |= get_area(near_turf) + +/** + * Returns a list with the names of the areas around a center at a certain distance + * Returns the local area if no distance is indicated + * Returns an empty list if the center is null +**/ +/proc/get_areas_in_range(distance = 0, atom/center = usr) + if(!distance) + var/turf/center_turf = get_turf(center) + return center_turf ? list(center_turf.loc) : list() + if(!center) + return list() + + var/list/turfs = RANGE_TURFS(distance, center) + var/list/areas = list() + for(var/turf/checked_turf as anything in turfs) + areas |= checked_turf.loc + return areas + +///Returns a list of all areas that are adjacent to the center atom's area, clear the list of nulls at the end. +/proc/get_adjacent_areas(atom/center) + . = list( + get_area(get_ranged_target_turf(center, NORTH, 1)), + get_area(get_ranged_target_turf(center, SOUTH, 1)), + get_area(get_ranged_target_turf(center, EAST, 1)), + get_area(get_ranged_target_turf(center, WEST, 1)) + ) + list_clear_nulls(.) + +///Returns a list of all turfs that are adjacent to the center atom's turf, clear the list of nulls at the end. +/proc/get_adjacent_turfs(atom/center) + . = list( + get_step(center, NORTH), + get_step(center, SOUTH), + get_step(center, EAST), + get_step(center, WEST) + ) + list_clear_nulls(.) + +///Checks if the mob provided (must_be_alone) is alone in an area +/proc/alone_in_area(area/the_area, mob/must_be_alone, check_type = /mob/living/carbon) + var/area/our_area = get_area(the_area) + for(var/carbon in GLOB.alive_mob_list) + if(!istype(carbon, check_type)) + continue + if(carbon == must_be_alone) + continue + if(our_area == get_area(carbon)) + return FALSE + return TRUE + +/** + * Behaves like the orange() proc, but only looks in the outer range of the function (The "peel" of the orange). + * This is useful for things like checking if a mob is in a certain range, but not within a smaller range. + * + * @params outer_range - The outer range of the cicle to pull from. + * @params inner_range - The inner range of the circle to NOT pull from. + * @params center - The center of the circle to pull from, can be an atom (we'll apply get_turf() to it within circle_x_turfs procs.) + * @params view_based - If TRUE, we'll use circle_view_turfs instead of circle_range_turfs procs. + */ +/proc/turf_peel(outer_range, inner_range, center, view_based = FALSE) + if(inner_range > outer_range) // If the inner range is larger than the outer range, you're using this wrong. + CRASH("Turf peel inner range is larger than outer range!") + var/list/peel = list() + var/list/outer + var/list/inner + if(view_based) + outer = circle_view_turfs(center, outer_range) + inner = circle_view_turfs(center, inner_range) + else + outer = circle_range_turfs(center, outer_range) + inner = circle_range_turfs(center, inner_range) + for(var/turf/possible_spawn as anything in outer) + if(possible_spawn in inner) + continue + peel += possible_spawn + + if(!length(peel)) + return center //Offer the center only as a default case when we don't have a valid circle. + return peel + +///check if 2 diagonal turfs are blocked by dense objects +/proc/diagonally_blocked(turf/our_turf, turf/dest_turf) + if(get_dist(our_turf, dest_turf) != 1) + return FALSE + var/direction_to_turf = get_dir(dest_turf, our_turf) + if(!ISDIAGONALDIR(direction_to_turf)) + return FALSE + for(var/direction_check in GLOB.cardinals) + if(!(direction_check & direction_to_turf)) + continue + var/turf/test_turf = get_step(dest_turf, direction_check) + if(isnull(test_turf)) + continue + if(!test_turf.is_blocked_turf(exclude_mobs = TRUE)) + return FALSE + return TRUE diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 51905f2233..7a4e48a217 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -24,17 +24,19 @@ /proc/Get_Angle(atom/movable/start,atom/movable/end)//For beams. if(!start || !end) return 0 - var/dy - var/dx - dy=(32*end.y+end.pixel_y)-(32*start.y+start.pixel_y) - dx=(32*end.x+end.pixel_x)-(32*start.x+start.pixel_x) - if(!dy) - return (dx>=0)?90:270 - .=arctan(dx/dy) - if(dy<0) - .+=180 - else if(dx<0) - .+=360 + var/dy =(world.icon_size * end.y + end.pixel_y) - (world.icon_size * start.y + start.pixel_y) + var/dx =(world.icon_size * end.x + end.pixel_x) - (world.icon_size * start.x + start.pixel_x) + return delta_to_angle(dx, dy) + +/// Calculate the angle produced by a pair of x and y deltas +/proc/delta_to_angle(x, y) + if(!y) + return (x >= 0) ? 90 : 270 + . = arctan(x/y) + if(y < 0) + . += 180 + else if(x < 0) + . += 360 /proc/Get_Pixel_Angle(y, x)//for getting the angle when animating something's pixel_x and pixel_y if(!y) diff --git a/code/_globalvars/lists/mapping.dm b/code/_globalvars/lists/mapping.dm index 6285d8afd4..5f86fdeb28 100644 --- a/code/_globalvars/lists/mapping.dm +++ b/code/_globalvars/lists/mapping.dm @@ -92,3 +92,6 @@ GLOBAL_LIST_EMPTY(all_abstract_markers) GLOBAL_LIST_EMPTY(fires_list) GLOBAL_LIST_EMPTY(streetlamp_list) + +/// List of all the maps that have been cached for /proc/load_map +GLOBAL_LIST_EMPTY(cached_maps) diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm index d4748464a2..3e50855d26 100644 --- a/code/_globalvars/lists/objects.dm +++ b/code/_globalvars/lists/objects.dm @@ -22,6 +22,7 @@ GLOBAL_LIST_EMPTY(crafting_recipes) //list of all table craft recipes GLOBAL_LIST_EMPTY(anvil_recipes) //list of all table craft recipes GLOBAL_LIST_EMPTY(alch_grind_recipes) //list of all alchemy grinding recipes GLOBAL_LIST_EMPTY(alch_cauldron_recipes) //list of all alchemy cauldron recipes +GLOBAL_LIST_EMPTY(cooking_recipes) //list of all cooking recipes. currently unused, pending a broader crafting refactor. GLOBAL_LIST_EMPTY(rcd_list) //list of Rapid Construction Devices. GLOBAL_LIST_EMPTY(apcs_list) //list of all Area Power Controller machines, separate from machines for powernet speeeeeeed. GLOBAL_LIST_EMPTY(tracked_implants) //list of all current implants that are tracked to work out what sort of trek everyone is on. Sadly not on lavaworld not implemented... diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm index 10aa6af558..9aceaa0738 100644 --- a/code/controllers/subsystem.dm +++ b/code/controllers/subsystem.dm @@ -38,6 +38,8 @@ var/processing_flag = PROCESSING_DEFAULT + var/lazy_load = TRUE + //Do not override ///datum/controller/subsystem/New() diff --git a/code/controllers/subsystem/dcs.dm b/code/controllers/subsystem/dcs.dm index d34d6d7bcb..dea335b5a2 100644 --- a/code/controllers/subsystem/dcs.dm +++ b/code/controllers/subsystem/dcs.dm @@ -1,16 +1,60 @@ PROCESSING_SUBSYSTEM_DEF(dcs) name = "Datum Component System" flags = SS_NO_INIT + wait = 1 SECONDS var/list/elements_by_type = list() /datum/controller/subsystem/processing/dcs/Recover() comp_lookup = SSdcs.comp_lookup -/datum/controller/subsystem/processing/dcs/proc/GetElement(eletype) - . = elements_by_type[eletype] - if(.) - return +/datum/controller/subsystem/processing/dcs/proc/GetElement(list/arguments) + var/datum/element/eletype = arguments[1] + var/element_id = eletype if(!ispath(eletype, /datum/element)) CRASH("Attempted to instantiate [eletype] as a /datum/element") - . = elements_by_type[eletype] = new eletype + + + if(initial(eletype.element_flags) & ELEMENT_BESPOKE) + element_id = GetIdFromArguments(arguments) + + . = elements_by_type[element_id] + if(.) + return + . = elements_by_type[element_id] = new eletype + +/**** + * Generates an id for bespoke elements when given the argument list + * Generating the id here is a bit complex because we need to support named arguments + * Named arguments can appear in any order and we need them to appear after ordered arguments + * We assume that no one will pass in a named argument with a value of null + **/ +/datum/controller/subsystem/processing/dcs/proc/GetIdFromArguments(list/arguments) + var/datum/element/eletype = arguments[1] + var/list/fullid = list("[eletype]") + var/list/named_arguments = list() + + for(var/i in initial(eletype.argument_hash_start_idx) to length(arguments)) + var/key = arguments[i] + + if(istext(key)) + var/value = arguments[key] + if (isnull(value)) + fullid += key + else + if (!istext(value) && !isnum(value)) + value = REF(value) + named_arguments[key] = value + + continue + + if (isnum(key)) + fullid += "[key]" + else + fullid += REF(key) + + if(length(named_arguments)) + named_arguments = sortTim(named_arguments, GLOBAL_PROC_REF(cmp_text_asc)) + fullid += named_arguments + + return list2params(fullid) diff --git a/code/controllers/subsystem/dungeon_generator.dm b/code/controllers/subsystem/dungeon_generator.dm new file mode 100644 index 0000000000..3ab9869420 --- /dev/null +++ b/code/controllers/subsystem/dungeon_generator.dm @@ -0,0 +1,351 @@ +SUBSYSTEM_DEF(dungeon_generator) + name = "Matthios Creation" + wait = 1 SECONDS + + init_order = INIT_ORDER_DUNGEON + runlevels = RUNLEVEL_GAME | RUNLEVEL_INIT | RUNLEVEL_LOBBY + lazy_load = FALSE + + var/list/parent_types = list() + + var/list/created_types = list() + + var/list/markers = list() + + var/list/placed_types = list() + + var/created_since = 0 + var/unlinked_dungeon_length = 0 + +/datum/controller/subsystem/dungeon_generator/Initialize(start_timeofday) + unlinked_dungeon_length = length(GLOB.unlinked_dungeon_entries) + while(length(markers)) + for(var/obj/effect/dungeon_directional_helper/helper as anything in markers) + if(!get_turf(helper)) + continue + find_soulmate(helper.dir, get_turf(helper), helper) + markers -= helper + return ..() + +/datum/controller/subsystem/dungeon_generator/fire(resumed) + var/current_run = 0 + if(length(markers)) + for(var/obj/effect/dungeon_directional_helper/helper as anything in markers) + if(current_run >= 4) + return + if(!get_turf(helper)) + continue + find_soulmate(helper.dir, get_turf(helper), helper) + markers -= helper + if(TICK_CHECK_LOW) + return + current_run++ + +/datum/controller/subsystem/dungeon_generator/proc/find_soulmate(direction, turf/creator, obj/effect/dungeon_directional_helper/looking_for_love) + creator = get_step(creator, direction) + if(!creator) + return + if(creator.type != /turf/closed/dungeon_void) + return + switch(direction) + if(NORTH) + direction = SOUTH + if(SOUTH) + direction = NORTH + if(EAST) + direction = WEST + if(WEST) + direction = EAST + + if(!length(parent_types)) + for(var/datum/map_template/dungeon/path as anything in subtypesof(/datum/map_template/dungeon)) + if(!is_abstract(path)) + continue + if(!initial(path.type_weight)) + continue + parent_types += path + parent_types[path] = initial(path.type_weight) + + if(!length(created_types)) + for(var/path in subtypesof(/datum/map_template/dungeon)) + if(is_abstract(path)) + continue + var/datum/map_template/dungeon/template = new path + created_types += template + created_types[template] = template.rarity + + var/picked_type = pickweight(parent_types) + var/picking = TRUE + if(unlinked_dungeon_length > 0) + if(created_since > 30) + if(prob(10 + created_since)) + picked_type = /datum/map_template/dungeon/entry + + if(!try_pickedtype_first(picked_type, direction, creator, looking_for_love)) + var/list/true_list = created_types.Copy() + while(picking) + if(!GET_TURF_ABOVE(creator)) + message_admins("[ADMIN_JMP(creator)] A dungeon piece was set to spawn on a top level z. This is not intended, their is a bad template.") + return + if(!length(true_list)) + return + var/datum/map_template/dungeon/template = pickweight(true_list) + true_list -= template + if(is_abstract(template)) + continue + if(is_type_in_list(template, list(subtypesof(picked_type) + subtypesof(/datum/map_template/dungeon/entry)))) + continue + var/turf/true_spawn + switch(direction) + if(WEST) + if(!template.west_offset) + continue + if(creator.y - template.west_offset < 0) + continue + var/turf/turf = locate(creator.x, creator.y - template.west_offset, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x + template.width, creator.y - template.east_offset, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, 0, -(template.west_offset)) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width, true_spawn.y + template.height, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + if(NORTH) + if(!template.north_offset) + continue + if(creator.x - template.north_offset < 0) + continue + if(creator.y - template.height < 0) + continue + var/turf/turf = locate(creator.x - template.north_offset - 1, creator.y + template.height, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x -(template.north_offset - 1) + template.width, creator.y + template.height, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, -(template.north_offset), -(template.height-1)) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width, true_spawn.y + template.height-1, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + if(SOUTH) + if(!template.south_offset) + continue + if(creator.y - template.south_offset < 0) + continue + var/turf/turf = locate(creator.x, creator.y + template.height, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x + template.width - template.south_offset, creator.y + template.height, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, -template.south_offset, 0) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width, true_spawn.y + template.height, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + if(EAST) + if(!template.east_offset) + continue + if(creator.y - template.east_offset < 0) + continue + if(creator.x - template.width < 0) + continue + var/turf/turf = locate(creator.x - (template.width-1), creator.y - template.east_offset, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x, creator.y - template.east_offset, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, -(template.width-1), -template.east_offset) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width-1, true_spawn.y + template.height, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + picking = FALSE + placed_types |= template.type + placed_types[template.type]++ + created_since++ + +/datum/controller/subsystem/dungeon_generator/proc/try_pickedtype_first(picked_type, direction, turf/creator, obj/effect/dungeon_directional_helper/looking_for_love) + var/picking = TRUE + + var/list/true_list = created_types.Copy() + while(picking) + if(!length(true_list)) + return FALSE + var/datum/map_template/dungeon/template = pickweight(true_list) + true_list -= template + if(is_abstract(template)) + continue + if(!is_type_in_list(template, subtypesof(picked_type))) + continue + var/turf/true_spawn + switch(direction) + if(WEST) + if(!template.west_offset) + continue + if(creator.y - template.west_offset < 0) + continue + var/turf/turf = locate(creator.x, creator.y - template.west_offset, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x + template.width, creator.y - template.east_offset, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, 0, -(template.west_offset)) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width, true_spawn.y + template.height, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + if(NORTH) + if(!template.north_offset) + continue + if(creator.x - template.north_offset < 0) + continue + if(creator.y - template.height < 0) + continue + var/turf/turf = locate(creator.x - template.north_offset - 1, creator.y + template.height, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x -(template.north_offset - 1) + template.width, creator.y + template.height, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, -(template.north_offset), -(template.height-1)) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width, true_spawn.y + template.height-1, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + if(SOUTH) + if(!template.south_offset) + continue + if(creator.y - template.south_offset < 0) + continue + var/turf/turf = locate(creator.x, creator.y + template.height, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x + template.width - template.south_offset, creator.y + template.height, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, -template.south_offset, 0) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width, true_spawn.y + template.height, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + if(EAST) + if(!template.east_offset) + continue + if(creator.y - template.east_offset < 0) + continue + if(creator.x - template.width < 0) + continue + var/turf/turf = locate(creator.x - (template.width-1), creator.y - template.east_offset, creator.z) + if(turf?.type != /turf/closed/dungeon_void) + continue + var/turf/turf2 = locate(creator.x, creator.y - template.east_offset, creator.z) + if(turf2?.type != /turf/closed/dungeon_void) + continue + true_spawn = get_offset_target_turf(creator, -(template.width-1), -template.east_offset) + if(true_spawn.x + template.width > world.maxx) + continue + if(true_spawn.y + template.height > world.maxy) + continue + var/list/turfs = block(true_spawn, locate(true_spawn.x + template.width-1, true_spawn.y + template.height, true_spawn.z)) + var/fail = FALSE + for(var/turf/list_turf in turfs) + if(list_turf.type != /turf/closed/dungeon_void) + fail = TRUE + break + if(fail) + continue + if(!template.load(true_spawn)) + continue + + picking = FALSE + created_since++ + placed_types |= template.type + placed_types[template.type]++ + if(picked_type == /datum/map_template/dungeon/entry) + created_since = 0 + unlinked_dungeon_length-- + return TRUE diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index 4c699519d8..1cdfd56e14 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -209,6 +209,11 @@ SUBSYSTEM_DEF(mapping) #ifdef ROGUEWORLD otherZ += load_map_config("_maps/map_files/otherz/rogueworld.json") #endif + + #ifndef NO_DUNGEON + otherZ += load_map_config("_maps/map_files/otherz/dungeon.json") + #endif + if(otherZ.len) for(var/datum/map_config/OtherZ in otherZ) LoadGroup(FailedZs, OtherZ.map_name, OtherZ.map_path, OtherZ.map_file, OtherZ.traits, ZTRAITS_STATION) diff --git a/code/controllers/subsystem/spatial_gridmap.dm b/code/controllers/subsystem/spatial_gridmap.dm new file mode 100644 index 0000000000..41b25ec697 --- /dev/null +++ b/code/controllers/subsystem/spatial_gridmap.dm @@ -0,0 +1,840 @@ +///the subsystem creates this many [/mob/oranges_ear] mob instances during init. allocations that require more than this create more. +#define NUMBER_OF_PREGENERATED_ORANGES_EARS 2500 + +/** + * # Spatial Grid Cell + * + * used by [/datum/controller/subsystem/spatial_grid] to cover every z level so that the coordinates of every turf in the world corresponds to one of these in + * the subsystems list of grid cells by z level. each one of these contains content lists holding all atoms meeting a certain criteria that is in our borders. + * these datums shouldnt have significant behavior, they should just hold data. the lists are filled and emptied by the subsystem. + */ +/datum/spatial_grid_cell + ///our x index in the list of cells. this is our index inside of our row list + var/cell_x + ///our y index in the list of cells. this is the index of our row list inside of our z level grid + var/cell_y + ///which z level we belong to, corresponding to the index of our gridmap in SSspatial_grid.grids_by_z_level + var/cell_z + //every data point in a grid cell is separated by usecase + + //when empty, the contents lists of these grid cell datums are just references to a dummy list from SSspatial_grid + //this is meant to allow a great compromise between memory usage and speed. + //now orthogonal_range_search() doesnt need to check if the list is null and each empty list is taking 12 bytes instead of 24 + //the only downside is that it needs to be switched over to a new list when it goes from 0 contents to > 0 contents and switched back on the opposite case + + ///every hearing sensitive movable inside this cell + var/list/hearing_contents + ///every client possessed mob inside this cell + var/list/client_contents + ///every atmos machine inside this cell + var/list/atmos_contents + +/datum/spatial_grid_cell/New(cell_x, cell_y, cell_z) + . = ..() + src.cell_x = cell_x + src.cell_y = cell_y + src.cell_z = cell_z + //cache for sanic speed (lists are references anyways) + var/list/dummy_list = SSspatial_grid.dummy_list + + if(length(dummy_list)) + dummy_list.Cut() + stack_trace("SSspatial_grid.dummy_list had something inserted into it at some point! this is a problem as it is supposed to stay empty") + hearing_contents = dummy_list + client_contents = dummy_list + atmos_contents = dummy_list + +/datum/spatial_grid_cell/Destroy(force) + if(!force)//the response to someone trying to qdel this is a right proper fuck you + stack_trace("dont try to destroy spatial grid cells without a good reason. if you need to do it use force") + return QDEL_HINT_LETMELIVE + + . = ..() + +/** + * # Spatial Grid + * + * a gamewide grid of spatial_grid_cell datums, each "covering" [SPATIAL_GRID_CELLSIZE] ^ 2 turfs. + * each spatial_grid_cell datum stores information about what is inside its covered area, so that searches through that area dont have to literally search + * through all turfs themselves to know what is within it since view() calls are expensive, and so is iterating through stuff you dont want. + * this allows you to only go through lists of what you want very cheaply. + * + * you can also register to objects entering and leaving a spatial cell, this allows you to do things like stay idle until a player enters, so you wont + * have to use expensive view() calls or iteratite over the global list of players and call get_dist() on every one. which is fineish for a few things, but is + * k * n operations for k objects iterating through n players. + * + * currently this system is only designed for searching for relatively uncommon things, small subsets of /atom/movable. + * dont add stupid shit to the cells please, keep the information that the cells store to things that need to be searched for often + * + * The system currently implements two different "classes" of spatial type + * + * The first exists to support important_recursive_contents. + * So if a client is inside a locker and the locker crosses a boundary, you'll still get a signal from the spatial grid. + * These types are [SPATIAL_GRID_CONTENTS_TYPE_HEARING] and [SPATIAL_GRID_CONTENTS_TYPE_CLIENTS] + * + * The second pattern is more paired down, and supports more wide use. + * Rather then the object and anything the object is in being sensitive, it's limited to just the object itself + * Currently only [SPATIAL_GRID_CONTENTS_TYPE_ATMOS] uses this pattern. This is because it's far more common, and so worth optimizing + * + */ +SUBSYSTEM_DEF(spatial_grid) + can_fire = FALSE + name = "Spatial Grid" + init_order = INIT_ORDER_SPATIAL_GRID + ///list of the spatial_grid_cell datums per z level, arranged in the order of y index then x index + var/list/grids_by_z_level = list() + ///everything that spawns before us is added to this list until we initialize + var/list/waiting_to_add_by_type = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(), SPATIAL_GRID_CONTENTS_TYPE_ATMOS = list()) + ///associative list of the form: movable.spatial_grid_key (string) -> inner list of spatial grid types for that key. + ///inner lists contain contents channel types such as SPATIAL_GRID_CONTENTS_TYPE_HEARING etc. + ///we use this to make adding to a cell static cost, and to save on memory + var/list/spatial_grid_categories = list() + + var/cells_on_x_axis = 0 + var/cells_on_y_axis = 0 + + ///empty spatial grid cell content lists are just a reference to this instead of a standalone list to save memory without needed to check if its null when iterating + var/list/dummy_list = list() + + ///list of all of /mob/oranges_ear instances we have pregenerated for view() iteration speedup + var/list/mob/oranges_ear/pregenerated_oranges_ears = list() + ///how many pregenerated /mob/oranges_ear instances currently exist. this should hopefully never exceed its starting value + var/number_of_oranges_ears = NUMBER_OF_PREGENERATED_ORANGES_EARS + +/datum/controller/subsystem/spatial_grid/Initialize() + cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx) + cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy) + + // enter_cell only runs if 'initialized' + initialized = TRUE + + for(var/datum/space_level/z_level as anything in SSmapping.z_list) + propogate_spatial_grid_to_new_z(null, z_level) + CHECK_TICK + + //go through the pre init queue for anything waiting to be let in the grid + for(var/channel_type in waiting_to_add_by_type) + for(var/atom/movable/movable as anything in waiting_to_add_by_type[channel_type]) + var/turf/movable_turf = get_turf(movable) + if(movable_turf) + enter_cell(movable, movable_turf) + + UnregisterSignal(movable, COMSIG_QDELETING) + waiting_to_add_by_type[channel_type] -= movable + + pregenerate_more_oranges_ears(NUMBER_OF_PREGENERATED_ORANGES_EARS) + + RegisterSignal(SSdcs, COMSIG_GLOB_NEW_Z, PROC_REF(propogate_spatial_grid_to_new_z)) + RegisterSignal(SSdcs, COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, PROC_REF(after_world_bounds_expanded)) + return ..() + +///add a movable to the pre init queue for whichever type is specified so that when the subsystem initializes they get added to the grid +/datum/controller/subsystem/spatial_grid/proc/enter_pre_init_queue(atom/movable/waiting_movable, type) + RegisterSignal(waiting_movable, COMSIG_QDELETING, PROC_REF(queued_item_deleted), override = TRUE) + //override because something can enter the queue for two different types but that is done through unrelated procs that shouldnt know about eachother + waiting_to_add_by_type[type] += waiting_movable + +///removes an initialized and probably deleted movable from our pre init queue before we're initialized +/datum/controller/subsystem/spatial_grid/proc/remove_from_pre_init_queue(atom/movable/movable_to_remove, exclusive_type) + if(exclusive_type) + waiting_to_add_by_type[exclusive_type] -= movable_to_remove + + var/waiting_movable_is_in_other_queues = FALSE//we need to check if this movable is inside the other queues + for(var/type in waiting_to_add_by_type) + if(movable_to_remove in waiting_to_add_by_type[type]) + waiting_movable_is_in_other_queues = TRUE + + if(!waiting_movable_is_in_other_queues) + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) + + return + + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) + for(var/type in waiting_to_add_by_type) + waiting_to_add_by_type[type] -= movable_to_remove + +///if a movable is inside our pre init queue before we're initialized and it gets deleted we need to remove that reference with this proc +/datum/controller/subsystem/spatial_grid/proc/queued_item_deleted(atom/movable/movable_being_deleted) + SIGNAL_HANDLER + remove_from_pre_init_queue(movable_being_deleted, null) + +///creates the spatial grid for a new z level +/datum/controller/subsystem/spatial_grid/proc/propogate_spatial_grid_to_new_z(datum/controller/subsystem/processing/dcs/fucking_dcs, datum/space_level/z_level) + SIGNAL_HANDLER + + var/list/new_cell_grid = list() + + grids_by_z_level += list(new_cell_grid) + + for(var/y in 1 to cells_on_y_axis) + new_cell_grid += list(list()) + for(var/x in 1 to cells_on_x_axis) + var/datum/spatial_grid_cell/cell = new(x, y, z_level.z_value) + new_cell_grid[y] += cell + +///adds cells to the grid for every z level when world.maxx or world.maxy is expanded after this subsystem is initialized. hopefully this is never needed. +///because i never tested this. +/datum/controller/subsystem/spatial_grid/proc/after_world_bounds_expanded(datum/controller/subsystem/processing/dcs/fucking_dcs, has_expanded_world_maxx, has_expanded_world_maxy) + SIGNAL_HANDLER + var/old_x_axis = cells_on_x_axis + var/old_y_axis = cells_on_y_axis + + cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx) + cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy) + + for(var/z_level in 1 to length(grids_by_z_level)) + var/list/z_level_gridmap = grids_by_z_level[z_level] + + for(var/cell_row_for_expanded_y_axis in 1 to cells_on_y_axis) + + if(cell_row_for_expanded_y_axis > old_y_axis)//we are past the old length of the number of rows, so add to the list + z_level_gridmap += list(list()) + + //now we know theres a row at this position, so add cells to it that need to be added and update the ones that already exist + var/list/cell_row = z_level_gridmap[cell_row_for_expanded_y_axis] + + for(var/grid_cell_for_expanded_x_axis in 1 to cells_on_x_axis) + + if(grid_cell_for_expanded_x_axis > old_x_axis) + var/datum/spatial_grid_cell/new_cell_inserted = new(grid_cell_for_expanded_x_axis, cell_row_for_expanded_y_axis, z_level) + cell_row += new_cell_inserted + continue + + //now we know the cell index we're at contains an already existing cell that needs its x and y values updated + var/datum/spatial_grid_cell/old_cell_that_needs_updating = cell_row[grid_cell_for_expanded_x_axis] + old_cell_that_needs_updating.cell_x = grid_cell_for_expanded_x_axis + old_cell_that_needs_updating.cell_y = cell_row_for_expanded_y_axis + +///the left or bottom side index of a box composed of spatial grid cells with the given actual center x or y coordinate +#define BOUNDING_BOX_MIN(center_coord) max(GET_SPATIAL_INDEX(center_coord - range), 1) +///the right or upper side index of a box composed of spatial grid cells with the given center x or y coordinate. +///outputted value cant exceed the number of cells on that axis +#define BOUNDING_BOX_MAX(center_coord, axis_size) min(GET_SPATIAL_INDEX(center_coord + range), axis_size) + +/** + * https://en.wikipedia.org/wiki/Range_searching#Orthogonal_range_searching + * + * searches through the grid cells intersecting a rectangular search space (with sides of length 2 * range) then returns all contents of type inside them. + * much faster than iterating through view() to find all of what you want. + * + * this does NOT return things only in range distance from center! the search space is a square not a circle, if you want only things in a certain distance + * then you need to filter that yourself + * + * * center - the atom that is the center of the searched circle + * * type - the type of grid contents you are looking for, see __DEFINES/spatial_grid.dm + * * range - the bigger this is, the more spatial grid cells the search space intersects + */ +/datum/controller/subsystem/spatial_grid/proc/orthogonal_range_search(atom/center, type, range) + var/turf/center_turf = get_turf(center) + + var/center_x = center_turf.x//used inside the macros + var/center_y = center_turf.y + + . = list() + + //technically THIS list only contains lists, but inside those lists are grid cell datums and we can go without a SINGLE var init if we do this + var/list/list/datum/spatial_grid_cell/grid_level = grids_by_z_level[center_turf.z] + + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].client_contents + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].hearing_contents + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + . += grid_level[row][x_index].atmos_contents + + return . + +///get the grid cell encomapassing targets coordinates +/datum/controller/subsystem/spatial_grid/proc/get_cell_of(atom/target) + var/turf/target_turf = get_turf(target) + if(!target_turf) + return + + return grids_by_z_level[target_turf.z][GET_SPATIAL_INDEX(target_turf.y)][GET_SPATIAL_INDEX(target_turf.x)] + +///get all grid cells intersecting the bounding box around center with sides of length 2 * range +/datum/controller/subsystem/spatial_grid/proc/get_cells_in_range(atom/center, range) + return get_cells_in_bounds(center, range, range) + +///get all grid cells intersecting the bounding box around center with sides of length (2 * range_x, 2 * range_y) +/datum/controller/subsystem/spatial_grid/proc/get_cells_in_bounds(atom/center, range_x, range_y) + var/turf/center_turf = get_turf(center) + + var/center_x = center_turf.x + var/center_y = center_turf.y + + var/list/intersecting_grid_cells = list() + + //the minimum x and y cell indexes to test + var/min_x = max(GET_SPATIAL_INDEX(center_x - range_x), 1) + var/min_y = max(GET_SPATIAL_INDEX(center_y - range_y), 1)//calculating these indices only takes around 2 microseconds + + //the maximum x and y cell indexes to test + var/max_x = min(GET_SPATIAL_INDEX(center_x + range_x), cells_on_x_axis) + var/max_y = min(GET_SPATIAL_INDEX(center_y + range_y), cells_on_y_axis) + + var/list/grid_level = grids_by_z_level[center_turf.z] + + for(var/row in min_y to max_y) + var/list/grid_row = grid_level[row] + + for(var/x_index in min_x to max_x) + intersecting_grid_cells += grid_row[x_index] + + return intersecting_grid_cells + +/// Adds grid awareness to the passed in atom, of the passed in type +/// Basically, when this atom moves between grids, it wants to have enter/exit cell called on it +/datum/controller/subsystem/spatial_grid/proc/add_grid_awareness(atom/movable/add_to, type) + // We need to ensure we have a new list reference, to build our new key out of + var/list/current_list = spatial_grid_categories[add_to.spatial_grid_key] + if(current_list) + current_list = current_list.Copy() + else + current_list = list() + // Now we do a binary insert, to ensure it's sorted (don't wanna overcache) + BINARY_INSERT_DEFINE(type, current_list, SORT_VAR_NO_TYPE, type, SORT_COMPARE_DIRECTLY, COMPARE_KEY) + update_grid_awareness(add_to, current_list) + +/// Removes grid awareness from the passed in atom, of the passed in type +/datum/controller/subsystem/spatial_grid/proc/remove_grid_awareness(atom/movable/remove_from, type) + // We need to ensure we have a new list reference, to build our new key out of + var/list/current_list = spatial_grid_categories[remove_from.spatial_grid_key] + if(current_list) + current_list = current_list.Copy() + else + current_list = list() + current_list -= type + update_grid_awareness(remove_from, current_list) + +/// Alerts the atom's current cell that it wishes to be treated as a member +/// This functionally amounts to "hey, I was recently made aware by [add_grid_awareness], please insert me into my current cell" +/datum/controller/subsystem/spatial_grid/proc/add_grid_membership(atom/movable/add_to, turf/target_turf, type) + if(!target_turf) + return + if(initialized) + add_single_type(add_to, target_turf, type) + else //SSspatial_grid isnt init'd yet, add ourselves to the queue + enter_pre_init_queue(add_to, type) + +/// Removes grid membership from the passed in atom, of the passed in type +/datum/controller/subsystem/spatial_grid/proc/remove_grid_membership(atom/movable/remove_from, turf/target_turf, type) + if(!target_turf) + return + if(initialized) + remove_single_type(remove_from, target_turf, type) + else //SSspatial_grid isnt init'd yet, remove ourselves from the queue + remove_from_pre_init_queue(remove_from, type) + +/// Updates the string that atoms hold that stores their grid awareness +/// We will use it to key into their spatial grid categories later +/datum/controller/subsystem/spatial_grid/proc/update_grid_awareness(atom/movable/update, list/new_list) + // We locally store a stringified version of the list, to prevent people trying to mutate it + update.spatial_grid_key = new_list.Join("-") + // Ensure the global representation is cached + if(!spatial_grid_categories[update.spatial_grid_key]) + spatial_grid_categories[update.spatial_grid_key] = new_list + +///find the spatial map cell that target belongs to, then add the target to it, as its type prefers. +///make sure to provide the turf new_target is "in" +/datum/controller/subsystem/spatial_grid/proc/enter_cell(atom/movable/new_target, turf/target_turf) + if(!initialized) + return + if(QDELETED(new_target)) + CRASH("qdeleted or null target trying to enter the spatial grid!") + + if(!target_turf || !new_target.spatial_grid_key) + CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid!") + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + for(var/type in spatial_grid_categories[new_target.spatial_grid_key]) + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways) + GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/new_target_contents = new_target.important_recursive_contents + GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_SET(intersecting_cell.atmos_contents, new_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target) + +///acts like enter_cell() but only adds the target to a specified type of grid cell contents list +/datum/controller/subsystem/spatial_grid/proc/add_single_type(atom/movable/new_target, turf/target_turf, exclusive_type) + if(!initialized) + return + if(QDELETED(new_target)) + CRASH("qdeleted or null target trying to enter the spatial grid!") + + if(!target_turf || !(exclusive_type in spatial_grid_categories[new_target.spatial_grid_key])) + CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid as a [exclusive_type]!") + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + switch(exclusive_type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways) + GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/new_target_contents = new_target.important_recursive_contents + GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_SET(intersecting_cell.atmos_contents, new_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target) + + return intersecting_cell + +/** + * find the spatial map cell that target used to belong to, then remove the target (and sometimes its important_recusive_contents) from it. + * make sure to provide the turf old_target used to be "in" + * + * * old_target - the thing we want to remove from the spatial grid cell + * * target_turf - the turf we use to determine the cell we're removing from + * * exclusive_type - either null or a valid contents channel. if you just want to remove a single type from the grid cell then use this + */ +/datum/controller/subsystem/spatial_grid/proc/exit_cell(atom/movable/old_target, turf/target_turf, exclusive_type) + if(!initialized) + return + + if(!target_turf || !old_target.spatial_grid_key) + stack_trace("/datum/controller/subsystem/spatial_grid/proc/exit_cell() was given null arguments or a old_target that doesn't use the spatial grid!") + return FALSE + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + for(var/type in spatial_grid_categories[old_target.spatial_grid_key]) + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/old_target_contents = old_target.important_recursive_contents?[type] || old_target + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/old_target_contents = old_target.important_recursive_contents?[type] || old_target + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_REMOVE(intersecting_cell.atmos_contents, old_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target) + + return TRUE + +///acts like exit_cell() but only removes the target from the specified type of grid cell contents list +/datum/controller/subsystem/spatial_grid/proc/remove_single_type(atom/movable/old_target, turf/target_turf, exclusive_type) + if(!target_turf || !exclusive_type || !old_target.spatial_grid_key) + stack_trace("/datum/controller/subsystem/spatial_grid/proc/remove_single_type() was given null arguments or an old_target that doesn't use the spatial grid!") + return FALSE + + if(!(exclusive_type in spatial_grid_categories[old_target.spatial_grid_key])) + return FALSE + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + + switch(exclusive_type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/old_target_contents = old_target.important_recursive_contents?[exclusive_type] || old_target //cache for sanic speeds (lists are references anyways) + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/old_target_contents = old_target.important_recursive_contents?[exclusive_type] || old_target + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_REMOVE(intersecting_cell.atmos_contents, old_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target) + + return TRUE + +/// if for whatever reason this movable is "untracked" e.g. it breaks the assumption that a movable is only inside the contents of any grid cell associated with its loc, +/// this will error. this checks every grid cell in the world so dont call this on live unless you have to. +/// returns TRUE if this movable is untracked, FALSE otherwise +/datum/controller/subsystem/spatial_grid/proc/untracked_movable_error(atom/movable/movable_to_check) + if(!movable_to_check?.spatial_grid_key) + return FALSE + + if(!initialized) + return FALSE + + var/datum/spatial_grid_cell/loc_cell = get_cell_of(movable_to_check) + var/list/containing_cells = find_hanging_cell_refs_for_movable(movable_to_check, remove_from_cells=FALSE) + //if we're in multiple cells, throw an error. + //if we're in 1 cell but it cant be deduced by our location, throw an error. + if(length(containing_cells) > 1 || (length(containing_cells) == 1 && loc_cell && containing_cells[1] != loc_cell && containing_cells[1] != null)) + var/error_data = "" + + var/location_string = "which is in nullspace, and thus not be within the contents of any spatial grid cell" + if(loc_cell) + location_string = "which is supposed to only be in the contents of a spatial grid cell at coords: ([GRID_INDEX_TO_COORDS(loc_cell.cell_x)], [GRID_INDEX_TO_COORDS(loc_cell.cell_y)], [loc_cell.cell_z])" + + var/error_explanation = "was in the contents of [length(containing_cells)] spatial grid cells when it was only supposed to be in one!" + if(length(containing_cells) == 1) + error_explanation = "was in the contents of 1 spatial grid cell but it was inside the area handled by another grid cell!" + var/datum/spatial_grid_cell/bad_cell = containing_cells[1] + + error_data = "within the contents of a cell at coords: ([GRID_INDEX_TO_COORDS(bad_cell.cell_x)], [GRID_INDEX_TO_COORDS(bad_cell.cell_y)], [bad_cell.cell_z])" + + if(!error_data) + for(var/datum/spatial_grid_cell/cell in containing_cells) + var/coords = "([GRID_INDEX_TO_COORDS(cell.cell_x)], [GRID_INDEX_TO_COORDS(cell.cell_y)], [cell.cell_z])" + var/contents = "" + + if(movable_to_check in cell.hearing_contents) + contents = "hearing" + + if(movable_to_check in cell.client_contents) + if(length(contents) > 0) + contents = "[contents], client" + else + contents = "client" + + if(movable_to_check in cell.atmos_contents) + if(length(contents) > 0) + contents = "[contents], atmos" + else + contents = "atmos" + + if(length(error_data) > 0) + error_data = "[error_data], {coords: [coords], within channels: [contents]}" + else + error_data = "within the contents of the following cells: {coords: [coords], within channels: [contents]}" + + /** + * example: + * + * /mob/living/trolls_the_maintainer instance, which is supposed to only be in the contents of a spatial grid cell at coords: (136, 136, 14), + * was in the contents of 3 spatial grid cells when it was only supposed to be in one! within the contents of the following cells: + * {(68, 153, 2), within channels: hearing}, + * {coords: (221, 170, 3), within channels: hearing}, + * {coords: (255, 153, 11), within channels: hearing}, + * {coords: (136, 136, 14), within channels: hearing}. + */ + stack_trace("[movable_to_check.type] instance, [location_string], [error_explanation] [error_data].") + + return TRUE + + return FALSE + +/** + * remove this movable from the grid by finding the grid cell its in and removing it from that. + * if it cant infer a grid cell its located in (e.g. if its in nullspace but it can happen if the grid isnt expanded to a z level), search every grid cell. + */ +/datum/controller/subsystem/spatial_grid/proc/force_remove_from_grid(atom/movable/to_remove) + if(!to_remove?.spatial_grid_key) + return + + if(!initialized) + remove_from_pre_init_queue(to_remove)//the spatial grid doesnt exist yet, so just take it out of the queue + return + +#ifdef UNIT_TESTS + if(untracked_movable_error(to_remove)) + find_hanging_cell_refs_for_movable(to_remove, remove_from_cells=FALSE) //dont remove from cells because we should be able to see 2 errors + return +#endif + + var/datum/spatial_grid_cell/loc_cell = get_cell_of(to_remove) + + if(loc_cell) + GRID_CELL_REMOVE_ALL(loc_cell, to_remove) + else + find_hanging_cell_refs_for_movable(to_remove, remove_from_cells=TRUE) + +///remove this movable from the given spatial_grid_cell +/datum/controller/subsystem/spatial_grid/proc/force_remove_from_cell(atom/movable/to_remove, datum/spatial_grid_cell/input_cell) + if(!input_cell) + return + + GRID_CELL_REMOVE_ALL(input_cell, to_remove) + +///if shit goes south, this will find hanging references for qdeleting movables inside the spatial grid +/datum/controller/subsystem/spatial_grid/proc/find_hanging_cell_refs_for_movable(atom/movable/to_remove, remove_from_cells = TRUE) + + var/list/queues_containing_movable = list() + for(var/queue_channel in waiting_to_add_by_type) + var/list/queue_list = waiting_to_add_by_type[queue_channel] + if(to_remove in queue_list) + queues_containing_movable += queue_channel//just add the associative key + if(remove_from_cells) + queue_list -= to_remove + + if(!initialized) + return queues_containing_movable + + var/list/containing_cells = list() + for(var/list/z_level_grid as anything in grids_by_z_level) + for(var/list/cell_row as anything in z_level_grid) + for(var/datum/spatial_grid_cell/cell as anything in cell_row) + if(to_remove in (cell.hearing_contents | cell.client_contents | cell.atmos_contents)) + containing_cells += cell + if(remove_from_cells) + force_remove_from_cell(to_remove, cell) + + return containing_cells + +///debug proc for checking if a movable is in multiple cells when it shouldnt be (ie always unless multitile entering is implemented) +/atom/proc/find_all_cells_containing(remove_from_cells = FALSE) + var/datum/spatial_grid_cell/real_cell = SSspatial_grid.get_cell_of(src) + var/list/containing_cells = SSspatial_grid.find_hanging_cell_refs_for_movable(src, remove_from_cells) + + message_admins("[src] is located in the contents of [length(containing_cells)] spatial grid cells") + + var/cell_coords = "the following cells contain [src]: " + for(var/datum/spatial_grid_cell/cell as anything in containing_cells) + cell_coords += "([cell.cell_x], [cell.cell_y], [cell.cell_z]), " + + message_admins(cell_coords) + message_admins("[src] is supposed to only be contained in the cell at indexes ([real_cell.cell_x], [real_cell.cell_y], [real_cell.cell_z]). but is contained at the cells at [cell_coords]") + +///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears. +///i really fucking hope this never gets called after init :clueless: +/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate) + for(var/new_ear in 1 to number_to_generate) + pregenerated_oranges_ears += new/mob/oranges_ear(null) + + number_of_oranges_ears = length(pregenerated_oranges_ears) + +///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf. +///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to) +/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears) + var/input_length = length(atoms_that_need_ears) + + if(input_length > number_of_oranges_ears) + stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen") + pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it + + . = list() + + ///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf + var/mob/oranges_ear/current_ear + ///the next atom in atoms_that_need_ears an ear assigned to it + var/atom/assigned_atom + ///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one + ///because allocating more than one oranges_ear to a given loc wastes view iterations + var/turf/turf_loc + + for(var/current_ear_index in 1 to input_length) + assigned_atom = atoms_that_need_ears[current_ear_index] + + turf_loc = get_turf(assigned_atom) + if(!turf_loc) + continue + + current_ear = pregenerated_oranges_ears[current_ear_index] + + if(turf_loc.assigned_oranges_ear) + turf_loc.assigned_oranges_ear.references += assigned_atom + continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves + + current_ear.references += assigned_atom + + current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us + turf_loc.assigned_oranges_ear = current_ear + + . += current_ear + +///debug proc for finding how full the cells of src's z level are +/atom/proc/find_grid_statistics_for_z_level(insert_clients = 0) + var/raw_clients = 0 + var/raw_hearables = 0 + var/raw_atmos = 0 + + var/cells_with_clients = 0 + var/cells_with_hearables = 0 + var/cells_with_atmos = 0 + + var/list/client_list = list() + var/list/hearable_list = list() + var/list/atmos_list = list() + + var/x_cell_count = world.maxx / SPATIAL_GRID_CELLSIZE + var/y_cell_count = world.maxy / SPATIAL_GRID_CELLSIZE + + var/total_cells = x_cell_count ** 2 + + var/average_clients_per_cell = 0 + var/average_hearables_per_cell = 0 + var/average_atmos_mech_per_call = 0 + + var/hearable_min_x = x_cell_count + var/hearable_max_x = 1 + + var/hearable_min_y = y_cell_count + var/hearable_max_y = 1 + + var/client_min_x = x_cell_count + var/client_max_x = 1 + + var/client_min_y = y_cell_count + var/client_max_y = 1 + + var/atmos_min_x = x_cell_count + var/atmos_max_x = 1 + + var/atmos_min_y = y_cell_count + var/atmos_max_y = 1 + + var/list/inserted_clients = list() + + if(insert_clients) + var/list/turfs = Z_TURFS(z) + + for(var/client_to_insert in 0 to insert_clients) + var/turf/random_turf = pick(turfs) + var/mob/fake_client = new() + fake_client.important_recursive_contents = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(fake_client), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(fake_client)) + fake_client.forceMove(random_turf) + inserted_clients += fake_client + + var/list/all_z_level_cells = SSspatial_grid.get_cells_in_range(src, 1000) + + for(var/datum/spatial_grid_cell/cell as anything in all_z_level_cells) + var/client_length = length(cell.client_contents) + var/hearable_length = length(cell.hearing_contents) + var/atmos_length = length(cell.atmos_contents) + + raw_clients += client_length + raw_hearables += hearable_length + raw_atmos += atmos_length + + if(client_length) + cells_with_clients++ + + client_list += cell.client_contents + + if(cell.cell_x < client_min_x) + client_min_x = cell.cell_x + + if(cell.cell_x > client_max_x) + client_max_x = cell.cell_x + + if(cell.cell_y < client_min_y) + client_min_y = cell.cell_y + + if(cell.cell_y > client_max_y) + client_max_y = cell.cell_y + + if(hearable_length) + cells_with_hearables++ + + hearable_list += cell.hearing_contents + + if(cell.cell_x < hearable_min_x) + hearable_min_x = cell.cell_x + + if(cell.cell_x > hearable_max_x) + hearable_max_x = cell.cell_x + + if(cell.cell_y < hearable_min_y) + hearable_min_y = cell.cell_y + + if(cell.cell_y > hearable_max_y) + hearable_max_y = cell.cell_y + + if(raw_atmos) + cells_with_atmos++ + + atmos_list += cell.atmos_contents + + if(cell.cell_x < atmos_min_x) + atmos_min_x = cell.cell_x + + if(cell.cell_x > atmos_max_x) + atmos_max_x = cell.cell_x + + if(cell.cell_y < atmos_min_y) + atmos_min_y = cell.cell_y + + if(cell.cell_y > atmos_max_y) + atmos_max_y = cell.cell_y + + var/total_client_distance = 0 + var/total_hearable_distance = 0 + var/total_atmos_distance = 0 + + var/average_client_distance = 0 + var/average_hearable_distance = 0 + var/average_atmos_distance = 0 + + for(var/hearable in hearable_list)//n^2 btw + for(var/other_hearable in hearable_list) + if(hearable == other_hearable) + continue + total_hearable_distance += get_dist(hearable, other_hearable) + + for(var/client in client_list)//n^2 btw + for(var/other_client in client_list) + if(client == other_client) + continue + total_client_distance += get_dist(client, other_client) + + for(var/atmos in atmos_list)//n^2 btw + for(var/other_atmos in atmos_list) + if(atmos == other_atmos) + continue + total_atmos_distance += get_dist(atmos, other_atmos) + + if(length(hearable_list)) + average_hearable_distance = total_hearable_distance / length(hearable_list) + if(length(client_list)) + average_client_distance = total_client_distance / length(client_list) + if(length(atmos_list)) + average_atmos_distance = total_atmos_distance / length(atmos_list) + + average_clients_per_cell = raw_clients / total_cells + average_hearables_per_cell = raw_hearables / total_cells + average_atmos_mech_per_call = raw_atmos / total_cells + + for(var/mob/inserted_client as anything in inserted_clients) + qdel(inserted_client) + + message_admins("on z level [z] there are [raw_clients] clients ([insert_clients] of whom are fakes inserted to random station turfs)\ + , [raw_hearables] hearables, and [raw_atmos] atmos machines. all of whom are inside the bounding box given by \ + clients: ([client_min_x], [client_min_y]) x ([client_max_x], [client_max_y]), \ + hearables: ([hearable_min_x], [hearable_min_y]) x ([hearable_max_x], [hearable_max_y]) \ + and atmos machines: ([atmos_min_x], [atmos_min_y]) x ([atmos_max_x], [atmos_max_y]), \ + on average there are [average_clients_per_cell] clients per cell, [average_hearables_per_cell] hearables per cell, \ + and [average_atmos_mech_per_call] per cell, \ + [cells_with_clients] cells have clients, [cells_with_hearables] have hearables, and [cells_with_atmos] have atmos machines \ + the average client distance is: [average_client_distance], the average hearable_distance is [average_hearable_distance], \ + and the average atmos distance is [average_atmos_distance] ") + +#undef BOUNDING_BOX_MAX +#undef BOUNDING_BOX_MIN + +#undef NUMBER_OF_PREGENERATED_ORANGES_EARS diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm index c5e9abeeac..1e07fae271 100644 --- a/code/controllers/subsystem/timer.dm +++ b/code/controllers/subsystem/timer.dm @@ -94,7 +94,7 @@ SUBSYSTEM_DEF(timer) if(ctime_timer.flags & TIMER_LOOP) ctime_timer.spent = 0 ctime_timer.timeToRun = REALTIMEOFDAY + ctime_timer.wait - BINARY_INSERT(ctime_timer, clienttime_timers, datum/timedevent, timeToRun) + BINARY_INSERT(ctime_timer, clienttime_timers, /datum/timedevent, ctime_timer, timeToRun, COMPARE_KEY) else qdel(ctime_timer) @@ -423,7 +423,7 @@ SUBSYSTEM_DEF(timer) L = SStimer.second_queue if(L) - BINARY_INSERT(src, L, datum/timedevent, timeToRun) + BINARY_INSERT(src, L, /datum/timedevent, src, timeToRun, COMPARE_KEY) return //get the list of buckets diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm index 11807131a2..25410e19f7 100644 --- a/code/datums/components/_component.dm +++ b/code/datums/components/_component.dm @@ -1,62 +1,80 @@ /** - * # Component - * - * The component datum - * - * A component should be a single standalone unit - * of functionality, that works by receiving signals from it's parent - * object to provide some single functionality (i.e a slippery component) - * that makes the object it's attached to cause people to slip over. - * Useful when you want shared behaviour independent of type inheritance - */ + * # Component + * + * The component datum + * + * A component should be a single standalone unit + * of functionality, that works by receiving signals from it's parent + * object to provide some single functionality (i.e a slippery component) + * that makes the object it's attached to cause people to slip over. + * Useful when you want shared behaviour independent of type inheritance + */ /datum/component - /// Defines how duplicate existing components are handled when added to a datum - /// See `COMPONENT_DUPE_*` definitions for available options + /** + * Defines how duplicate existing components are handled when added to a datum + * + * See [COMPONENT_DUPE_*][COMPONENT_DUPE_ALLOWED] definitions for available options + */ var/dupe_mode = COMPONENT_DUPE_HIGHLANDER - /// The type to check for duplication - /// `null` means exact match on `type` (default) - /// Any other type means that and all subtypes + /** + * The type to check for duplication + * + * `null` means exact match on `type` (default) + * + * Any other type means that and all subtypes + */ var/dupe_type /// The datum this components belongs to var/datum/parent - /// Only set to true if you are able to properly transfer this component - /// At a minimum RegisterWithParent and UnregisterFromParent should be used - /// Make sure you also implement PostTransfer for any post transfer handling + /** + * Only set to true if you are able to properly transfer this component + * + * At a minimum [RegisterWithParent][/datum/component/proc/RegisterWithParent] and [UnregisterFromParent][/datum/component/proc/UnregisterFromParent] should be used + * + * Make sure you also implement [PostTransfer][/datum/component/proc/PostTransfer] for any post transfer handling + */ var/can_transfer = FALSE /** - * Create a new component. - * Additional arguments are passed to `Initialize()` - * - * Arguments: - * * datum/P the parent datum this component reacts to signals from - */ -/datum/component/New(datum/P, ...) - parent = P - var/list/arguments = args.Copy(2) + * Create a new component. + * + * Additional arguments are passed to [Initialize()][/datum/component/proc/Initialize] + * + * Arguments: + * * datum/P the parent datum this component reacts to signals from + */ +/datum/component/New(list/raw_args) + parent = raw_args[1] + var/list/arguments = raw_args.Copy(2) if(Initialize(arglist(arguments)) == COMPONENT_INCOMPATIBLE) + stack_trace("Incompatible [type] assigned to a [parent.type]! args: [json_encode(arguments)]") qdel(src, TRUE, TRUE) - CRASH("Incompatible [type] assigned to a [P.type]! args: [json_encode(arguments)]") + return - _JoinParent(P) + _JoinParent(parent) /** * Called during component creation with the same arguments as in new excluding parent. - * Do not call `qdel(src)` from this function, `return COMPONENT_INCOMPATIBLE` instead - */ + * + * Do not call `qdel(src)` from this function, `return COMPONENT_INCOMPATIBLE` instead + */ /datum/component/proc/Initialize(...) return /** - * Properly removes the component from `parent` and cleans up references - * Setting `force` makes it not check for and remove the component from the parent - * Setting `silent` deletes the component without sending a `COMSIG_COMPONENT_REMOVING` signal - */ + * Properly removes the component from `parent` and cleans up references + * + * Arguments: + * * force - makes it not check for and remove the component from the parent + * * silent - deletes the component without sending a [COMSIG_COMPONENT_REMOVING] signal + */ /datum/component/Destroy(force=FALSE, silent=FALSE) - if(!force && parent) + if(!parent) + return ..() + if(!force) _RemoveFromParent() if(!silent) SEND_SIGNAL(parent, COMSIG_COMPONENT_REMOVING, src) @@ -64,8 +82,8 @@ return ..() /** - * Internal proc to handle behaviour of components when joining a parent - */ + * Internal proc to handle behaviour of components when joining a parent + */ /datum/component/proc/_JoinParent() var/datum/P = parent //lazy init the parent's dc list @@ -77,14 +95,14 @@ var/our_type = type for(var/I in _GetInverseTypeList(our_type)) var/test = dc[I] - if(test) //already another component of this type here + if(test) //already another component of this type here var/list/components_of_type if(!length(test)) components_of_type = list(test) dc[I] = components_of_type else components_of_type = test - if(I == our_type) //exact match, take priority + if(I == our_type) //exact match, take priority var/inserted = FALSE for(var/J in 1 to components_of_type.len) var/datum/component/C = components_of_type[J] @@ -94,28 +112,28 @@ break if(!inserted) components_of_type += src - else //indirect match, back of the line with ya + else //indirect match, back of the line with ya components_of_type += src - else //only component of this type, no list + else //only component of this type, no list dc[I] = src RegisterWithParent() /** - * Internal proc to handle behaviour when being removed from a parent - */ + * Internal proc to handle behaviour when being removed from a parent + */ /datum/component/proc/_RemoveFromParent() var/datum/P = parent var/list/dc = P.datum_components for(var/I in _GetInverseTypeList()) var/list/components_of_type = dc[I] - if(length(components_of_type)) // + if(length(components_of_type)) // var/list/subtracted = components_of_type - src if(subtracted.len == 1) //only 1 guy left - dc[I] = subtracted[1] //make him special + dc[I] = subtracted[1] //make him special else dc[I] = subtracted - else //just us + else //just us dc -= I if(!dc.len) P.datum_components = null @@ -123,37 +141,39 @@ UnregisterFromParent() /** - * Register the component with the parent object - * - * Use this proc to register with your parent object - * Overridable proc that's called when added to a new parent - */ + * Register the component with the parent object + * + * Use this proc to register with your parent object + * + * Overridable proc that's called when added to a new parent + */ /datum/component/proc/RegisterWithParent() return /** - * Unregister from our parent object - * - * Use this proc to unregister from your parent object - * Overridable proc that's called when removed from a parent - * * - */ + * Unregister from our parent object + * + * Use this proc to unregister from your parent object + * + * Overridable proc that's called when removed from a parent + * * + */ /datum/component/proc/UnregisterFromParent() return /** - * Register to listen for a signal from the passed in target - * - * This sets up a listening relationship such that when the target object emits a signal - * the source datum this proc is called upon, will recieve a callback to the given proctype - * Return values from procs registered must be a bitfield - * - * Arguments: - * * datum/target The target to listen for signals from - * * sig_type_or_types Either a string signal name, or a list of signal names (strings) - * * proctype The proc to call back when the signal is emitted - * * override If a previous registration exists you must explicitly set this - */ + * Register to listen for a signal from the passed in target + * + * This sets up a listening relationship such that when the target object emits a signal + * the source datum this proc is called upon, will receive a callback to the given proctype + * Return values from procs registered must be a bitfield + * + * Arguments: + * * datum/target The target to listen for signals from + * * sig_type_or_types Either a string signal name, or a list of signal names (strings) + * * proctype The proc to call back when the signal is emitted + * * override If a previous registration exists you must explicitly set this + */ /datum/proc/RegisterSignal(datum/target, sig_type_or_types, proctype, override = FALSE) if(QDELETED(src) || QDELETED(target)) return @@ -184,27 +204,28 @@ else // Many other things have registered here lookup[sig_type][src] = TRUE - signal_enabled = TRUE - /** - * Stop listening to a given signal from target - * - * Breaks the relationship between target and source datum, removing the callback when the signal fires - * Doesn't care if a registration exists or not - * - * Arguments: - * * datum/target Datum to stop listening to signals from - * * sig_typeor_types Signal string key or list of signal keys to stop listening to specifically - */ + * Stop listening to a given signal from target + * + * Breaks the relationship between target and source datum, removing the callback when the signal fires + * + * Doesn't care if a registration exists or not + * + * Arguments: + * * datum/target Datum to stop listening to signals from + * * sig_typeor_types Signal string key or list of signal keys to stop listening to specifically + */ /datum/proc/UnregisterSignal(datum/target, sig_type_or_types) - if(!target) - return var/list/lookup = target.comp_lookup if(!signal_procs || !signal_procs[target] || !lookup) return if(!islist(sig_type_or_types)) sig_type_or_types = list(sig_type_or_types) for(var/sig in sig_type_or_types) + if(!signal_procs[target][sig]) + if(!istext(sig)) + stack_trace("We're unregistering with something that isn't a valid signal \[[sig]\], you fucked up") + continue switch(length(lookup[sig])) if(2) lookup[sig] = (lookup[sig]-src)[1] @@ -216,6 +237,8 @@ target.comp_lookup = null break if(0) + if(lookup[sig] != src) + continue lookup -= sig if(!length(lookup)) target.comp_lookup = null @@ -228,35 +251,50 @@ signal_procs -= target /** - * Called on a component when a component of the same type was added to the same parent - * See `/datum/component/var/dupe_mode` - * `C`'s type will always be the same of the called component - */ + * Called on a component when a component of the same type was added to the same parent + * + * See [/datum/component/var/dupe_mode] + * + * `C`'s type will always be the same of the called component + */ /datum/component/proc/InheritComponent(datum/component/C, i_am_original) return + /** - * Callback Just before this component is transferred - * - * Use this to do any special cleanup you might need to do before being deregged from an object - * - */ + * Called on a component when a component of the same type was added to the same parent with [COMPONENT_DUPE_SELECTIVE] + * + * See [/datum/component/var/dupe_mode] + * + * `C`'s type will always be the same of the called component + * + * return TRUE if you are absorbing the component, otherwise FALSE if you are fine having it exist as a duplicate component + */ +/datum/component/proc/CheckDupeComponent(datum/component/C, ...) + return + + +/** + * Callback Just before this component is transferred + * + * Use this to do any special cleanup you might need to do before being deregged from an object + */ /datum/component/proc/PreTransfer() return /** - * Callback Just after a component is transferred - * - * Use this to do any special setup you need to do after being moved to a new object - * Do not call `qdel(src)` from this function, `return COMPONENT_INCOMPATIBLE` instead - * - */ + * Callback Just after a component is transferred + * + * Use this to do any special setup you need to do after being moved to a new object + * + * Do not call `qdel(src)` from this function, `return COMPONENT_INCOMPATIBLE` instead + */ /datum/component/proc/PostTransfer() return COMPONENT_INCOMPATIBLE //Do not support transfer by default as you must properly support it /** - * Internal proc to create a list of our type and all parent types - */ + * Internal proc to create a list of our type and all parent types + */ /datum/component/proc/_GetInverseTypeList(our_type = type) //we can do this one simple trick var/current_type = parent_type @@ -267,37 +305,39 @@ . += current_type /** - * Internal proc to handle most all of the signaling procedure - * Will runtime if used on datums with an empty component list - * Use the `SEND_SIGNAL` define instead - */ + * Internal proc to handle most all of the signaling procedure + * + * Will runtime if used on datums with an empty component list + * + * Use the [SEND_SIGNAL] define instead + */ /datum/proc/_SendSignal(sigtype, list/arguments) var/target = comp_lookup[sigtype] if(!length(target)) - var/datum/C = target - if(!C.signal_enabled) - return NONE - var/proctype = C.signal_procs[src][sigtype] - return NONE | CallAsync(C, proctype, arguments) + var/datum/listening_datum = target + return NONE | call(listening_datum, listening_datum.signal_procs[src][sigtype])(arglist(arguments)) . = NONE - for(var/I in target) - var/datum/C = I - if(!C.signal_enabled) - continue - var/proctype = C.signal_procs[src][sigtype] - . |= CallAsync(C, proctype, arguments) + // This exists so that even if one of the signal receivers unregisters the signal, + // all the objects that are receiving the signal get the signal this final time. + // AKA: No you can't cancel the signal reception of another object by doing an unregister in the same signal. + var/list/queued_calls = list() + for(var/datum/listening_datum as anything in target) + queued_calls[listening_datum] = listening_datum.signal_procs[src][sigtype] + for(var/datum/listening_datum as anything in queued_calls) + . |= call(listening_datum, queued_calls[listening_datum])(arglist(arguments)) // The type arg is casted so initial works, you shouldn't be passing a real instance into this /** - * Return any component assigned to this datum of the given type - * This will throw an error if it's possible to have more than one component of that type on the parent - * - * Arguments: - * * datum/component/c_type The typepath of the component you want to get a reference to - */ + * Return any component assigned to this datum of the given type + * + * This will throw an error if it's possible to have more than one component of that type on the parent + * + * Arguments: + * * datum/component/c_type The typepath of the component you want to get a reference to + */ /datum/proc/GetComponent(datum/component/c_type) RETURN_TYPE(c_type) - if(initial(c_type.dupe_mode) == COMPONENT_DUPE_ALLOWED) + if(initial(c_type.dupe_mode) == COMPONENT_DUPE_ALLOWED || initial(c_type.dupe_mode) == COMPONENT_DUPE_SELECTIVE) stack_trace("GetComponent was called to get a component of which multiple copies could be on an object. This can easily break and should be changed. Type: \[[c_type]\]") var/list/dc = datum_components if(!dc) @@ -308,15 +348,16 @@ // The type arg is casted so initial works, you shouldn't be passing a real instance into this /** - * Return any component assigned to this datum of the exact given type - * This will throw an error if it's possible to have more than one component of that type on the parent - * - * Arguments: - * * datum/component/c_type The typepath of the component you want to get a reference to - */ + * Return any component assigned to this datum of the exact given type + * + * This will throw an error if it's possible to have more than one component of that type on the parent + * + * Arguments: + * * datum/component/c_type The typepath of the component you want to get a reference to + */ /datum/proc/GetExactComponent(datum/component/c_type) RETURN_TYPE(c_type) - if(initial(c_type.dupe_mode) == COMPONENT_DUPE_ALLOWED) + if(initial(c_type.dupe_mode) == COMPONENT_DUPE_ALLOWED || initial(c_type.dupe_mode) == COMPONENT_DUPE_SELECTIVE) stack_trace("GetComponent was called to get a component of which multiple copies could be on an object. This can easily break and should be changed. Type: \[[c_type]\]") var/list/dc = datum_components if(!dc) @@ -330,28 +371,35 @@ return null /** - * Get all components of a given type that are attached to this datum - * - * Arguments: - * * c_type The component type path - */ + * Get all components of a given type that are attached to this datum + * + * Arguments: + * * c_type The component type path + */ /datum/proc/GetComponents(c_type) - var/list/dc = datum_components - if(!dc) - return null - . = dc[c_type] - if(!length(.)) - return list(.) + var/list/components = datum_components?[c_type] + if(!components) + return list() + return islist(components) ? components : list(components) /** - * Creates an instance of `new_type` in the datum and attaches to it as parent - * Sends the `COMSIG_COMPONENT_ADDED` signal to the datum - * Returns the component that was created. Or the old component in a dupe situation where `COMPONENT_DUPE_UNIQUE` was set - * If this tries to add an component to an incompatible type, the component will be deleted and the result will be `null`. This is very unperformant, try not to do it - * Properly handles duplicate situations based on the `dupe_mode` var - */ -/datum/proc/AddComponent(new_type, ...) + * Creates an instance of `new_type` in the datum and attaches to it as parent + * + * Sends the [COMSIG_COMPONENT_ADDED] signal to the datum + * + * Returns the component that was created. Or the old component in a dupe situation where [COMPONENT_DUPE_UNIQUE] was set + * + * If this tries to add a component to an incompatible type, the component will be deleted and the result will be `null`. This is very unperformant, try not to do it + * + * Properly handles duplicate situations based on the `dupe_mode` var + */ +/datum/proc/_AddComponent(list/raw_args) + var/new_type = raw_args[1] var/datum/component/nt = new_type + + if(QDELING(src)) + CRASH("Attempted to add a new component of type \[[nt]\] to a qdeleting parent of type \[[type]\]!") + var/dm = initial(nt.dupe_mode) var/dt = initial(nt.dupe_type) @@ -365,9 +413,9 @@ new_comp = nt nt = new_comp.type - args[1] = src + raw_args[1] = src - if(dm != COMPONENT_DUPE_ALLOWED) + if(dm != COMPONENT_DUPE_ALLOWED && dm != COMPONENT_DUPE_SELECTIVE) if(!dt) old_comp = GetExactComponent(nt) else @@ -376,26 +424,37 @@ switch(dm) if(COMPONENT_DUPE_UNIQUE) if(!new_comp) - new_comp = new nt(arglist(args)) + new_comp = new nt(raw_args) if(!QDELETED(new_comp)) old_comp.InheritComponent(new_comp, TRUE) QDEL_NULL(new_comp) if(COMPONENT_DUPE_HIGHLANDER) if(!new_comp) - new_comp = new nt(arglist(args)) + new_comp = new nt(raw_args) if(!QDELETED(new_comp)) new_comp.InheritComponent(old_comp, FALSE) QDEL_NULL(old_comp) if(COMPONENT_DUPE_UNIQUE_PASSARGS) if(!new_comp) - var/list/arguments = args.Copy(2) + var/list/arguments = raw_args.Copy(2) old_comp.InheritComponent(null, TRUE, arguments) else old_comp.InheritComponent(new_comp, TRUE) else if(!new_comp) - new_comp = new nt(arglist(args)) // There's a valid dupe mode but there's no old component, act like normal + new_comp = new nt(raw_args) // There's a valid dupe mode but there's no old component, act like normal + else if(dm == COMPONENT_DUPE_SELECTIVE) + var/list/arguments = raw_args.Copy() + arguments[1] = new_comp + var/make_new_component = TRUE + for(var/datum/component/existing_component as anything in GetComponents(new_type)) + if(existing_component.CheckDupeComponent(arglist(arguments))) + make_new_component = FALSE + QDEL_NULL(new_comp) + break + if(!new_comp && make_new_component) + new_comp = new nt(raw_args) else if(!new_comp) - new_comp = new nt(arglist(args)) // Dupes are allowed, act like normal + new_comp = new nt(raw_args) // Dupes are allowed, act like normal if(!old_comp && !QDELETED(new_comp)) // Nothing related to duplicate components happened and the new component is healthy SEND_SIGNAL(src, COMSIG_COMPONENT_ADDED, new_comp) @@ -403,23 +462,24 @@ return old_comp /** - * Get existing component of type, or create it and return a reference to it - * - * Use this if the item needs to exist at the time of this call, but may not have been created before now - * - * Arguments: - * * component_type The typepath of the component to create or return - * * ... additional arguments to be passed when creating the component if it does not exist - */ -/datum/proc/LoadComponent(component_type, ...) - . = GetComponent(component_type) + * Get existing component of type, or create it and return a reference to it + * + * Use this if the item needs to exist at the time of this call, but may not have been created before now + * + * Arguments: + * * component_type The typepath of the component to create or return + * * ... additional arguments to be passed when creating the component if it does not exist + */ +/datum/proc/_LoadComponent(list/arguments) + . = GetComponent(arguments[1]) if(!.) - return AddComponent(arglist(args)) + return _AddComponent(arguments) /** - * Removes the component from parent, ends up with a null parent - */ -/datum/component/proc/RemoveComponent() + * Removes the component from parent, ends up with a null parent + * Used as a helper proc by the component transfer proc, does not clean up the component like Destroy does + */ +/datum/component/proc/ClearFromParent() if(!parent) return var/datum/old_parent = parent @@ -429,18 +489,18 @@ SEND_SIGNAL(old_parent, COMSIG_COMPONENT_REMOVING, src) /** - * Transfer this component to another parent - * - * Component is taken from source datum - * - * Arguments: - * * datum/component/target Target datum to transfer to - */ + * Transfer this component to another parent + * + * Component is taken from source datum + * + * Arguments: + * * datum/component/target Target datum to transfer to + */ /datum/proc/TakeComponent(datum/component/target) if(!target || target.parent == src) return if(target.parent) - target.RemoveComponent() + target.ClearFromParent() target.parent = src var/result = target.PostTransfer() switch(result) @@ -453,13 +513,13 @@ target._JoinParent() /** - * Transfer all components to target - * - * All components from source datum are taken - * - * Arguments: - * * /datum/target the target to move the components to - */ + * Transfer all components to target + * + * All components from source datum are taken + * + * Arguments: + * * /datum/target the target to move the components to + */ /datum/proc/TransferComponents(datum/target) var/list/dc = datum_components if(!dc) @@ -475,7 +535,7 @@ target.TakeComponent(comps) /** - * Return the object that is the host of any UI's that this component has - */ + * Return the object that is the host of any UI's that this component has + */ /datum/component/ui_host() return parent diff --git a/code/datums/components/construction.dm b/code/datums/components/construction.dm index 40e84a358d..4de2856582 100644 --- a/code/datums/components/construction.dm +++ b/code/datums/components/construction.dm @@ -102,7 +102,7 @@ /datum/component/construction/proc/spawn_result() // Some constructions result in new components being added. if(ispath(result, /datum/component)) - parent.AddComponent(result) + parent._AddComponent(result) qdel(src) else if(ispath(result, /atom)) diff --git a/code/datums/components/crafting/recipes.dm b/code/datums/components/crafting/recipes.dm index 2f7f55f86e..77a27dc0de 100644 --- a/code/datums/components/crafting/recipes.dm +++ b/code/datums/components/crafting/recipes.dm @@ -5,6 +5,8 @@ var/reqs[] = list() //type paths of items consumed associated with how many are needed var/blacklist[] = list() //type paths of items explicitly not allowed as an ingredient var/result //type path of item resulting from this craft + /// String defines of items needed but not consumed. Lazy list. + var/list/tool_behaviors var/tools[] = list() //type paths of items needed but not consumed var/time = 0 //time in deciseconds var/parts[] = list() //type paths of items that will be placed in the result diff --git a/code/datums/components/rotation.dm b/code/datums/components/rotation.dm index 6356b62b31..257ba1f14d 100644 --- a/code/datums/components/rotation.dm +++ b/code/datums/components/rotation.dm @@ -94,8 +94,9 @@ //Signals + verbs removed via UnRegister . = ..() -/datum/component/simple_rotation/RemoveComponent() +/datum/component/simple_rotation/ClearFromParent() remove_verbs() + remove_signals() . = ..() /datum/component/simple_rotation/proc/ExamineMessage(datum/source, mob/user, list/examine_list) diff --git a/code/datums/components/storage/storage_types.dm b/code/datums/components/storage/storage_types.dm index 0fa3b2f19f..19ba7f4bba 100644 --- a/code/datums/components/storage/storage_types.dm +++ b/code/datums/components/storage/storage_types.dm @@ -102,3 +102,12 @@ allow_quick_gather = TRUE allow_quick_empty = TRUE insert_preposition = "in" + +/datum/component/storage/concrete/roguetown/saddle + screen_max_rows = 4 + screen_max_columns = 4 + max_w_class = WEIGHT_CLASS_NORMAL + +/datum/component/storage/tray + insert_preposition = "on" + max_w_class = WEIGHT_CLASS_NORMAL diff --git a/code/datums/dungeon_generator/dungeon_direction_helpers.dm b/code/datums/dungeon_generator/dungeon_direction_helpers.dm new file mode 100644 index 0000000000..eaabd376eb --- /dev/null +++ b/code/datums/dungeon_generator/dungeon_direction_helpers.dm @@ -0,0 +1,52 @@ +///so this is the most important step of the dungeon maker if you don't put these down right your gonna obliterate the dungeon +/obj/effect/dungeon_directional_helper + name = "Dungeon Direction Helper" + desc = "These help stitch together dungeons, it looks for the opposite direction on a template, basically write in the template if it has this, invis on creation" + + icon = 'icons/effects/dungeon_helper.dmi' + icon_state = "helper" + invisibility = INVISIBILITY_ABSTRACT + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + + var/top = FALSE + +/obj/effect/dungeon_directional_helper/New() + . = ..() + var/turf/opposite_turf = get_step(get_turf(src), dir) + + if(!locate(/obj/effect/dungeon_directional_helper) in opposite_turf) + SSdungeon_generator.markers |= src + alpha = 0 + +/obj/effect/dungeon_directional_helper/Destroy(force) + if(src in SSdungeon_generator.markers) + SSdungeon_generator.markers -= src + return ..() + +/obj/effect/dungeon_directional_helper/south + dir = SOUTH + +/obj/effect/dungeon_directional_helper/north + dir = NORTH + +/obj/effect/dungeon_directional_helper/east + dir = EAST + +/obj/effect/dungeon_directional_helper/west + dir = WEST + +/obj/effect/dungeon_directional_helper/south/top + dir = SOUTH + top = TRUE + +/obj/effect/dungeon_directional_helper/north/top + dir = NORTH + top = TRUE + +/obj/effect/dungeon_directional_helper/east/top + dir = EAST + top = TRUE + +/obj/effect/dungeon_directional_helper/west/top + dir = WEST + top = TRUE diff --git a/code/datums/dungeon_generator/dungeon_entry_exit.dm b/code/datums/dungeon_generator/dungeon_entry_exit.dm new file mode 100644 index 0000000000..1b728b5cdf --- /dev/null +++ b/code/datums/dungeon_generator/dungeon_entry_exit.dm @@ -0,0 +1,147 @@ +GLOBAL_LIST_INIT(unlinked_dungeon_entries, list()) +GLOBAL_LIST_INIT(dungeon_entries, list()) +GLOBAL_LIST_INIT(dungeon_exits, list()) + +/obj/structure/dungeon_entry/center + dungeon_id = "center" + +/obj/structure/dungeon_entry + name = "The Tomb of Matthios" + desc = "" + + icon = 'icons/roguetown/misc/portal.dmi' + icon_state = "portal" + density = TRUE + anchored = TRUE + pixel_x = -48 + max_integrity = 0 + bound_width = 128 + appearance_flags = NONE + opacity = TRUE + obj_flags = INDESTRUCTIBLE + + var/dungeon_id + var/list/dungeon_exits = list() + var/can_enter = TRUE + + +/obj/structure/dungeon_entry/New(loc, ...) + GLOB.dungeon_entries |= src + if(!dungeon_id) + GLOB.unlinked_dungeon_entries |= src + return ..() + +/obj/structure/dungeon_entry/Initialize() + . = ..() + if(dungeon_id) + for(var/obj/structure/dungeon_exit/exit as anything in GLOB.dungeon_exits) + if(exit.dungeon_id != dungeon_id) + continue + dungeon_exits |= exit + exit.entry = src + GLOB.unlinked_dungeon_entries -= src + return + shuffle_inplace(GLOB.dungeon_exits) + for(var/obj/structure/dungeon_exit/exit as anything in GLOB.dungeon_exits) + if(exit.dungeon_id) + continue + dungeon_exits |= exit + exit.entry = src + GLOB.unlinked_dungeon_entries -= src + break + +/obj/structure/dungeon_entry/Destroy() + for(var/obj/structure/dungeon_exit/exit as anything in dungeon_exits) + exit.entry = null + dungeon_exits = null + GLOB.dungeon_entries -= src + GLOB.unlinked_dungeon_entries -= src + return ..() + +/obj/structure/dungeon_entry/attack_hand(mob/user) + . = ..() + if(.) + return + use(user) + +/obj/structure/dungeon_entry/attackby(obj/item/W, mob/user, params) + return use(user) + +//ATTACK GHOST IGNORING PARENT RETURN VALUE +/obj/structure/dungeon_entry/attack_ghost(mob/dead/observer/user) + use(user, TRUE) + return ..() + +/obj/structure/dungeon_entry/proc/use(mob/user, is_ghost) + if(!is_ghost && !can_enter) + return + if(!length(dungeon_exits)) + return + var/atom/exit = pick(dungeon_exits) + + if(!is_ghost) + playsound(src, 'sound/foley/ladder.ogg', 100, FALSE) + if(!do_after(user, 3 SECONDS, src)) + return + movable_travel_z_level(user, get_turf(exit)) + +/obj/structure/dungeon_exit + name = "dungeon exit" + desc = "" + + obj_flags = INDESTRUCTIBLE + + icon = 'icons/roguetown/misc/structure.dmi' + icon_state = "ladder10" + + var/dungeon_id + var/obj/structure/dungeon_entry/entry + +/obj/structure/dungeon_exit/Initialize() + . = ..() + GLOB.dungeon_exits |= src + + if(dungeon_id) + for(var/obj/structure/dungeon_entry/exit as anything in GLOB.dungeon_entries) + if(exit.dungeon_id != dungeon_id) + continue + exit.dungeon_exits |= src + entry = exit + GLOB.unlinked_dungeon_entries -= exit + return + shuffle_inplace(GLOB.unlinked_dungeon_entries) + for(var/obj/structure/dungeon_entry/exit as anything in GLOB.unlinked_dungeon_entries) + if(exit.dungeon_id) + continue + exit.dungeon_exits |= src + entry = exit + GLOB.unlinked_dungeon_entries -= exit + break + +/obj/structure/dungeon_exit/Destroy() + entry = null + GLOB.dungeon_exits -= src + return ..() + +/obj/structure/dungeon_exit/attack_hand(mob/user) + . = ..() + if(.) + return + use(user) + +/obj/structure/dungeon_exit/attackby(obj/item/W, mob/user, params) + return use(user) + +//ATTACK GHOST IGNORING PARENT RETURN VALUE +/obj/structure/dungeon_exit/attack_ghost(mob/dead/observer/user) + use(user, TRUE) + return ..() + +/obj/structure/dungeon_exit/proc/use(mob/user, is_ghost) + if(!entry) + return + if(!is_ghost) + playsound(src, 'sound/foley/ladder.ogg', 100, FALSE) + if(!do_after(user, 3 SECONDS, src)) + return + movable_travel_z_level(user, get_turf(entry)) diff --git a/code/datums/dungeon_generator/dungeon_templates/_base.dm b/code/datums/dungeon_generator/dungeon_templates/_base.dm new file mode 100644 index 0000000000..4e40c3340f --- /dev/null +++ b/code/datums/dungeon_generator/dungeon_templates/_base.dm @@ -0,0 +1,16 @@ +/datum/map_template/dungeon + ///the pickweight of this dungeon type + var/rarity = 100 + ///our type_pick weight + var/type_weight = 1 + + ///basically if these are set we assume it exists + ///I will close any pr that attempts to add the spawners outside the middle + ///this is to be assumed how many to the left + var/north_offset + ///this is to be assumed how many to the left + var/south_offset + ///this is to be assumed how many down + var/east_offset + ///this is to be assumed how many down + var/west_offset diff --git a/code/datums/dungeon_generator/dungeon_templates/entry/common.dm b/code/datums/dungeon_generator/dungeon_templates/entry/common.dm new file mode 100644 index 0000000000..9738dfc8c9 --- /dev/null +++ b/code/datums/dungeon_generator/dungeon_templates/entry/common.dm @@ -0,0 +1,35 @@ +/datum/map_template/dungeon/entry + name = "Entry Tile" + abstract_type = /datum/map_template/dungeon/entry + type_weight = 0 + +/datum/map_template/dungeon/entry/tented + mappath = "_maps/dungeon_generator/entry/Tented Entrance.dmm" + id = "tented" + width = 15 + height = 15 + + north_offset = 7 + south_offset = 7 + east_offset = 8 + west_offset = 8 + +/datum/map_template/dungeon/entry/eastentrance + mappath = "_maps/dungeon_generator/entry/eastentrance.dmm" + id = "eastentrance" + width = 25 + height = 35 + + north_offset = 9 + south_offset = 7 + west_offset = 18 + +/datum/map_template/dungeon/entry/Northernentrance + mappath = "_maps/dungeon_generator/entry/Northernentrance.dmm" + id = "Northernentrance" + width = 45 + height = 25 + + south_offset = 22 + west_offset = 6 + east_offset = 6 diff --git a/code/datums/dungeon_generator/dungeon_templates/hallways/common.dm b/code/datums/dungeon_generator/dungeon_templates/hallways/common.dm new file mode 100644 index 0000000000..0203cc352d --- /dev/null +++ b/code/datums/dungeon_generator/dungeon_templates/hallways/common.dm @@ -0,0 +1,431 @@ +/datum/map_template/dungeon/hallway + name = "Hall Tile" + abstract_type = /datum/map_template/dungeon/hallway + type_weight = 5 + +/datum/map_template/dungeon/hallway/LturnNorthEast + mappath = "_maps/dungeon_generator/hallway/LturnNorthEast.dmm" + id = "LturnNorthEast" + width = 5 + height = 5 + + north_offset = 1 + east_offset = 2 + +/datum/map_template/dungeon/hallway/LturnSouthEast + mappath = "_maps/dungeon_generator/hallway/LturnSouthEast.dmm" + id = "LturnSouthEast" + width = 5 + height = 5 + + south_offset = 1 + east_offset = 3 + +/datum/map_template/dungeon/hallway/LturnWestSouth + mappath = "_maps/dungeon_generator/hallway/LturnWestSouth.dmm" + id = "LturnWestSouth" + width = 5 + height = 5 + + south_offset = 2 + west_offset = 3 + +/datum/map_template/dungeon/hallway/LturnWestnorth + mappath = "_maps/dungeon_generator/hallway/LturnWestnorth.dmm" + id = "LturnWestnorth" + width = 5 + height = 5 + + north_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/Malphpiece3 + mappath = "_maps/dungeon_generator/hallway/Malphpiece3.dmm" + id = "Malphpiece3" + width = 5 + height = 8 + + north_offset = 2 + south_offset = 1 + east_offset = 4 + west_offset = 4 + +/datum/map_template/dungeon/hallway/Malphpiece5 + mappath = "_maps/dungeon_generator/hallway/Malphpiece5.dmm" + id = "Malphpiece5" + width = 10 + height = 4 + + east_offset = 1 + west_offset = 2 + +/datum/map_template/dungeon/hallway/Malphpiece9 + mappath = "_maps/dungeon_generator/hallway/Malphpiece9.dmm" + id = "Malphpiece9" + width = 5 + height = 9 + + north_offset = 2 + south_offset = 2 + +/datum/map_template/dungeon/hallway/MowPiece2 + mappath = "_maps/dungeon_generator/hallway/MowPiece2.dmm" + id = "MowPiece2" + width = 12 + height = 12 + + north_offset = 6 + south_offset = 6 + east_offset = 6 + west_offset = 6 + +/datum/map_template/dungeon/hallway/NormalHallway + mappath = "_maps/dungeon_generator/hallway/NormalHallway.dmm" + id = "NormalHallway" + width = 3 + height = 5 + + east_offset = 1 + west_offset = 1 + +/datum/map_template/dungeon/hallway/North_Hallway + mappath = "_maps/dungeon_generator/hallway/North Hallway.dmm" + id = "North_Hallway" + width = 5 + height = 3 + + south_offset = 1 + north_offset = 1 + +/datum/map_template/dungeon/hallway/TjunctionNorthSouthEast + mappath = "_maps/dungeon_generator/hallway/TjunctionNorthSouthEast.dmm" + id = "TjunctionNorthSouthEast" + width = 6 + height = 6 + + south_offset = 2 + north_offset = 2 + east_offset = 2 + +/datum/map_template/dungeon/hallway/TjunctionNorthWestEast + mappath = "_maps/dungeon_generator/hallway/TjunctionNorthWestEast.dmm" + id = "TjunctionNorthWestEast" + width = 6 + height = 6 + + west_offset = 2 + north_offset = 2 + east_offset = 2 + +/datum/map_template/dungeon/hallway/TjunctionWestNorthSouth + mappath = "_maps/dungeon_generator/hallway/TjunctionWestNorthSouth.dmm" + id = "TjunctionWestNorthSouth" + width = 6 + height = 6 + + west_offset = 2 + north_offset = 2 + south_offset = 2 + +/datum/map_template/dungeon/hallway/Malphpiece2 + mappath = "_maps/dungeon_generator/room/Malphpiece2.dmm" + id = "Malphpiece2" + width = 6 + height = 5 + + north_offset = 3 + south_offset = 2 + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/hallwesteastshort + mappath = "_maps/dungeon_generator/hallway/hallwesteastshort.dmm" + id = "hallwesteastshort" + width = 3 + height = 6 + + east_offset = 1 + west_offset = 1 + +/datum/map_template/dungeon/hallway/lampternhallway + mappath = "_maps/dungeon_generator/hallway/lampternhallway.dmm" + id = "lampternhallway" + width = 6 + height = 20 + + north_offset = 3 + south_offset = 3 + east_offset = 9 + west_offset = 9 + +/datum/map_template/dungeon/hallway/longhall + mappath = "_maps/dungeon_generator/hallway/longhall.dmm" + id = "longhall" + width = 15 + height = 6 + + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/northminedhall + mappath = "_maps/dungeon_generator/hallway/northminedhall.dmm" + id = "northminedhall" + width = 7 + height = 20 + + north_offset = 3 + south_offset = 3 + +/datum/map_template/dungeon/hallway/right_left_floor_transition + mappath = "_maps/dungeon_generator/hallway/right left floor transition.dmm" + id = "right_left_floor_transition" + width = 8 + height = 5 + + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/Crossjunction + mappath = "_maps/dungeon_generator/hallway/Crossjunction.dmm" + id = "Crossjunction" + width = 6 + height = 6 + + east_offset = 3 + west_offset = 3 + north_offset = 3 + south_offset = 3 + +/datum/map_template/dungeon/hallway/Floortransition2 + mappath = "_maps/dungeon_generator/hallway/Floortransition2.dmm" + id = "Floortransition2" + width = 5 + height = 8 + + north_offset = 2 + south_offset = 2 + +/datum/map_template/dungeon/hallway/Hallwayeastwestplane + mappath = "_maps/dungeon_generator/hallway/Hallwayeastwestplane.dmm" + id = "Hallwayeastwestplane" + width = 15 + height = 5 + + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/smallhallns + mappath = "_maps/dungeon_generator/hallway/smallhallns.dmm" + id = "smallhallns" + width = 3 + height = 6 + + north_offset = 1 + south_offset = 1 + +/datum/map_template/dungeon/hallway/wild_dungeon_small + mappath = "_maps/dungeon_generator/hallway/Wild Dungeon Small.dmm" + id = "wild_dungeon_small" + width = 10 + height = 10 + + north_offset = 5 + south_offset = 5 + east_offset = 5 + west_offset = 4 + +/datum/map_template/dungeon/hallway/wild_dungeon_medium + mappath = "_maps/dungeon_generator/hallway/Wild Dungeon Medium.dmm" + id = "wild_dungeon_medium" + width = 15 + height = 15 + + north_offset = 7 + south_offset = 7 + east_offset = 7 + west_offset = 7 + +/datum/map_template/dungeon/hallway/wild_dungeon_large + mappath = "_maps/dungeon_generator/hallway/Wild Dungeon Large.dmm" + id = "wild_dungeon_large" + width = 20 + height = 20 + + north_offset = 9 + south_offset = 9 + east_offset = 10 + west_offset = 10 + +/datum/map_template/dungeon/hallway/puzzle_dungeon_small + mappath = "_maps/dungeon_generator/hallway/Puzzle Trap Small.dmm" + id = "puzzle_dungeon_small" + width = 10 + height = 10 + + north_offset = 5 + south_offset = 5 + east_offset = 4 + west_offset = 5 + +/datum/map_template/dungeon/hallway/puzzle_dungeon_medium + mappath = "_maps/dungeon_generator/hallway/Puzzle Trap Medium.dmm" + id = "puzzle_dungeon_medium" + width = 15 + height = 15 + + north_offset = 7 + south_offset = 7 + east_offset = 7 + west_offset = 7 + +/datum/map_template/dungeon/hallway/puzzle_dungeon_large + mappath = "_maps/dungeon_generator/hallway/Puzzle Trap Large.dmm" + id = "puzzle_dungeon_large" + width = 20 + height = 20 + + north_offset = 10 + south_offset = 10 + east_offset = 10 + west_offset = 10 + +/datum/map_template/dungeon/hallway/SmallCubeStone + mappath = "_maps/dungeon_generator/hallway/SmallCubeStone.dmm" + id = "SmallCubeStone" + width = 4 + height = 4 + + north_offset = 2 + south_offset = 2 + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/SmallCubeStone2 + mappath = "_maps/dungeon_generator/hallway/SmallCubeStone2.dmm" + id = "SmallCubeStone2" + width = 5 + height = 5 + + north_offset = 2 + south_offset = 2 + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/LongSliverStone + mappath = "_maps/dungeon_generator/hallway/LongSliverStone.dmm" + id = "LongSliverStone" + width = 8 + height = 1 + + north_offset = 4 + south_offset = 4 + +/datum/map_template/dungeon/hallway/LongSliverStone2 + mappath = "_maps/dungeon_generator/hallway/LongSliverStone2.dmm" + id = "LongSliverStone2" + width = 1 + height = 8 + + east_offset = 4 + west_offset = 4 + +/datum/map_template/dungeon/hallway/TinySliverStone + mappath = "_maps/dungeon_generator/hallway/TinySliverStone.dmm" + id = "TinySliverStone" + width = 4 + height = 1 + + north_offset = 2 + south_offset = 2 + +/datum/map_template/dungeon/hallway/TinySliverStone2 + mappath = "_maps/dungeon_generator/hallway/TinySliverStone2.dmm" + id = "TinySliverStone2" + width = 1 + height = 4 + + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/SmallSliceStone + mappath = "_maps/dungeon_generator/hallway/SmallSliceStone.dmm" + id = "SmallSliceStone" + width = 5 + height = 1 + + north_offset = 2 + south_offset = 2 + +/datum/map_template/dungeon/hallway/SmallSliceStone2 + mappath = "_maps/dungeon_generator/hallway/SmallSliceStone2.dmm" + id = "SmallSliceStone2" + width = 1 + height = 5 + + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/SliceStone + mappath = "_maps/dungeon_generator/hallway/SliceStone.dmm" + id = "SliceStone" + width = 2 + height = 5 + + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/SliceStone2 + mappath = "_maps/dungeon_generator/hallway/SliceStone2.dmm" + id = "SliceStone2" + width = 5 + height = 2 + + north_offset = 2 + south_offset = 2 + +/datum/map_template/dungeon/hallway/CaveTEast + mappath = "_maps/dungeon_generator/hallway/CaveTEast.dmm" + id = "CaveTEast" + width = 10 + height = 15 + + north_offset = 4 + south_offset = 4 + east_offset = 7 + +/datum/map_template/dungeon/hallway/CaveTWest + mappath = "_maps/dungeon_generator/hallway/CaveTWest.dmm" + id = "CaveTWest" + width = 10 + height = 15 + + north_offset = 4 + south_offset = 4 + west_offset = 7 + +/datum/map_template/dungeon/hallway/HolyGrailHall + mappath = "_maps/dungeon_generator/hallway/HolyGrailHall.dmm" + id = "HolyGrailHall" + width = 15 + height = 25 + + east_offset = 4 + west_offset = 4 + +/datum/map_template/dungeon/hallway/LongHallStone2 + mappath = "_maps/dungeon_generator/hallway/LongHallStone2.dmm" + id = "LongHallStone2" + width = 10 + height = 5 + + east_offset = 2 + west_offset = 2 + +/datum/map_template/dungeon/hallway/LongHallStone + mappath = "_maps/dungeon_generator/hallway/LongHallStone.dmm" + id = "LongHallStone" + width = 5 + height = 10 + + north_offset = 2 + south_offset = 2 diff --git a/code/datums/dungeon_generator/dungeon_templates/rest/common.dm b/code/datums/dungeon_generator/dungeon_templates/rest/common.dm new file mode 100644 index 0000000000..94bbe5883d --- /dev/null +++ b/code/datums/dungeon_generator/dungeon_templates/rest/common.dm @@ -0,0 +1,58 @@ +/datum/map_template/dungeon/rest + name = "Rest Tile" + abstract_type = /datum/map_template/dungeon/rest + type_weight = 1 + +/datum/map_template/dungeon/rest/snackplatter + mappath = "_maps/dungeon_generator/rest/snackplatter.dmm" + id = "snackplatter" + width = 9 + height = 9 + + north_offset = 4 + south_offset = 4 + east_offset = 4 + west_offset = 4 + +/datum/map_template/dungeon/rest/stingerpatch + mappath = "_maps/dungeon_generator/rest/stingerpatch.dmm" + id = "stingerpatch" + width = 10 + height = 10 + + north_offset = 4 + south_offset = 4 + east_offset = 5 + west_offset = 4 + +/datum/map_template/dungeon/rest/Malphpiece1 + mappath = "_maps/dungeon_generator/rest/Malphpiece1.dmm" + id = "Malphpiece1" + width = 8 + height = 8 + + north_offset = 2 + south_offset = 3 + east_offset = 2 + west_offset = 5 + +/datum/map_template/dungeon/rest/largehallway + mappath = "_maps/dungeon_generator/rest/largehallway.dmm" + id = "largehallway" + width = 15 + height = 15 + + north_offset = 8 + south_offset = 7 + west_offset = 7 + +/datum/map_template/dungeon/rest/farm + mappath = "_maps/dungeon_generator/rest/farm.dmm" + id = "farm" + width = 15 + height = 15 + rarity = 200 + + east_offset = 4 + south_offset = 7 + west_offset = 9 diff --git a/code/datums/dungeon_generator/dungeon_templates/rooms/common.dm b/code/datums/dungeon_generator/dungeon_templates/rooms/common.dm new file mode 100644 index 0000000000..7adffd278c --- /dev/null +++ b/code/datums/dungeon_generator/dungeon_templates/rooms/common.dm @@ -0,0 +1,427 @@ +/datum/map_template/dungeon/room + name = "Room Tile" + abstract_type = /datum/map_template/dungeon/room + type_weight = 15 + +/datum/map_template/dungeon/room/sewer + mappath = "_maps/dungeon_generator/room/Sewers2.dmm" + id = "Sewers2" + width = 14 + height = 14 + + north_offset = 7 + south_offset = 7 + east_offset = 6 + west_offset = 6 + +/datum/map_template/dungeon/room/acidfight + mappath = "_maps/dungeon_generator/room/acidfight.dmm" + id = "acid_fight" + width = 20 + height = 20 + + north_offset = 9 + south_offset = 9 + east_offset = 10 + west_offset = 10 + + +/datum/map_template/dungeon/room/skeletonroom + mappath = "_maps/dungeon_generator/room/skeletonroom.dmm" + id = "skeletonroom" + width = 15 + height = 15 + + north_offset = 7 + south_offset = 7 + east_offset = 8 + west_offset = 8 + +/datum/map_template/dungeon/room/graveend + mappath = "_maps/dungeon_generator/room/graveend.dmm" + id = "graveend" + width = 24 + height = 7 + rarity = 20 + + west_offset = 3 + +/datum/map_template/dungeon/room/fightpit + mappath = "_maps/dungeon_generator/room/fightpit.dmm" + id = "fightpit" + width = 20 + height = 20 + + west_offset = 10 + south_offset = 10 + +/datum/map_template/dungeon/room/sewers + mappath = "_maps/dungeon_generator/room/sewers.dmm" + id = "sewers" + width = 15 + height = 15 + + north_offset = 7 + south_offset = 7 + east_offset = 7 + west_offset = 7 + +/datum/map_template/dungeon/room/campnotherthing + mappath = "_maps/dungeon_generator/room/campnotherthing.dmm" + id = "campnotherthing" + width = 12 + height = 12 + + north_offset = 5 + south_offset = 5 + east_offset = 6 + west_offset = 6 + +/datum/map_template/dungeon/room/cavecamp + mappath = "_maps/dungeon_generator/room/cavecamp.dmm" + id = "cavecamp" + width = 12 + height = 12 + + north_offset = 6 + south_offset = 6 + east_offset = 5 + west_offset = 6 + +/datum/map_template/dungeon/room/drugden + mappath = "_maps/dungeon_generator/room/drugden.dmm" + id = "drugden" + width = 20 + height = 20 + + north_offset = 10 + south_offset = 10 + east_offset = 10 + west_offset = 10 + +/datum/map_template/dungeon/room/dwelfhome + mappath = "_maps/dungeon_generator/room/dwelfhome.dmm" + id = "dwelfhome" + width = 15 + height = 15 + + north_offset = 7 + south_offset = 7 + east_offset = 7 + west_offset = 7 + +/datum/map_template/dungeon/room/Malphpiece4 + mappath = "_maps/dungeon_generator/room/Malphpiece4.dmm" + id = "Malphpiece4" + width = 13 + height = 13 + + north_offset = 11 + south_offset = 5 + east_offset = 3 + west_offset = 10 + +/datum/map_template/dungeon/room/ForgottenInn + mappath = "_maps/dungeon_generator/room/ForgottenInn.dmm" + id = "ForgottenInn" + width = 20 + height = 20 + + north_offset = 10 + south_offset = 10 + east_offset = 11 + west_offset = 11 + +/datum/map_template/dungeon/room/fightingpit + mappath = "_maps/dungeon_generator/room/fightingpit.dmm" + id = "fightingpit" + width = 20 + height = 15 + + east_offset = 7 + west_offset = 7 + +/datum/map_template/dungeon/room/small_lab + mappath = "_maps/dungeon_generator/room/small lab.dmm" + id = "small_lab" + width = 20 + height = 20 + + east_offset = 9 + west_offset = 9 + north_offset = 10 + south_offset = 10 + +/datum/map_template/dungeon/room/Allbutwesthall + mappath = "_maps/dungeon_generator/room/Allbutwesthall.dmm" + id = "Allbutwesthall" + width = 15 + height = 15 + + east_offset = 7 + north_offset = 7 + south_offset = 7 + +/datum/map_template/dungeon/room/Bathhouse_Dungeon + mappath = "_maps/dungeon_generator/room/Bathhouse Dungeon.dmm" + id = "Bathhouse_Dungeon" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 15 + south_offset = 15 + +/datum/map_template/dungeon/room/lava_small + mappath = "_maps/dungeon_generator/room/Lava Small.dmm" + id = "lava_small" + width = 10 + height = 10 + + west_offset = 5 + east_offset = 5 + north_offset = 5 + south_offset = 5 + +/datum/map_template/dungeon/room/lava_medium + mappath = "_maps/dungeon_generator/room/Lava Medium.dmm" + id = "lava_medium" + width = 15 + height = 15 + + west_offset = 7 + east_offset = 7 + north_offset = 7 + south_offset = 7 + +/datum/map_template/dungeon/room/lava_large + mappath = "_maps/dungeon_generator/room/Lava Large.dmm" + id = "lava_large" + width = 20 + height = 20 + + west_offset = 9 + east_offset = 9 + north_offset = 10 + south_offset = 10 + +/datum/map_template/dungeon/room/DarkCorridors + mappath = "_maps/dungeon_generator/room/DarkCorridors.dmm" + id = "DarkCorridors" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/GoblinInfestedJoint + mappath = "_maps/dungeon_generator/room/GoblinInfestedJoint.dmm" + id = "GoblinInfestedJoint" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/SmallChurch + mappath = "_maps/dungeon_generator/room/SmallChurch.dmm" + id = "SmallChurch" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/TheatherOfSadism + mappath = "_maps/dungeon_generator/room/TheatherOfSadism.dmm" + id = "TheatherOfSadism" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/TownRuins + mappath = "_maps/dungeon_generator/room/TownRuins.dmm" + id = "TownRuins" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/hctomb1 + mappath = "_maps/dungeon_generator/room/hctomb1.dmm" + id = "hctomb1" + width = 30 + height = 30 + + west_offset = 15 + east_offset = 15 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/hctomb2 + mappath = "_maps/dungeon_generator/room/hctomb2.dmm" + id = "hctomb2" + width = 30 + height = 30 + + west_offset = 15 + east_offset = 15 + north_offset = 15 + south_offset = 15 + +/datum/map_template/dungeon/room/hctomb3 + mappath = "_maps/dungeon_generator/room/hctomb3.dmm" + id = "hctomb3" + width = 30 + height = 30 + + west_offset = 15 + east_offset = 15 + north_offset = 15 + south_offset = 15 + +/datum/map_template/dungeon/room/hctomb4 + mappath = "_maps/dungeon_generator/room/hctomb4.dmm" + id = "hctomb4" + width = 30 + height = 30 + + west_offset = 12 + east_offset = 14 + north_offset = 15 + south_offset = 15 + +/datum/map_template/dungeon/room/hctomb5 + mappath = "_maps/dungeon_generator/room/hctomb5.dmm" + id = "hctomb5" + width = 20 + height = 20 + + west_offset = 6 + east_offset = 8 + north_offset = 9 + +/datum/map_template/dungeon/room/goblincamp + mappath = "_maps/dungeon_generator/room/goblincamp.dmm" + id = "goblincamp" + width = 30 + height = 30 + + west_offset = 15 + east_offset = 15 + north_offset = 13 + south_offset = 13 + +/datum/map_template/dungeon/room/rousecamp + mappath = "_maps/dungeon_generator/room/rousecamp.dmm" + id = "rousecamp" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 15 + south_offset = 15 + +/datum/map_template/dungeon/room/queensretreat + mappath = "_maps/dungeon_generator/room/queensretreat.dmm" + id = "queensretreat" + width = 30 + height = 30 + + west_offset = 15 + east_offset = 15 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/lavafort + mappath = "_maps/dungeon_generator/room/lavafort.dmm" + id = "lavafort" + width = 30 + height = 30 + + west_offset = 15 + east_offset = 15 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/magicanvil + mappath = "_maps/dungeon_generator/room/magicanvil.dmm" + id = "magicanvil" + width = 30 + height = 30 + + rarity = 20 + + west_offset = 13 + east_offset = 12 + north_offset = 14 + south_offset = 15 + +/datum/map_template/dungeon/room/Thelastbreath + mappath = "_maps/dungeon_generator/room/Thelastbreath.dmm" + id = "Thelastbreath" + width = 45 + height = 45 + + west_offset = 22 + east_offset = 22 + north_offset = 22 + south_offset = 22 + +/datum/map_template/dungeon/room/AcidMageTower + mappath = "_maps/dungeon_generator/room/AcidMageTower.dmm" + id = "AcidMageTower" + width = 30 + height = 30 + + west_offset = 15 + east_offset = 15 + north_offset = 15 + south_offset = 15 + +/datum/map_template/dungeon/room/Goonies + mappath = "_maps/dungeon_generator/room/Goonies.dmm" + id = "Goonies" + width = 60 + height = 40 + + west_offset = 21 + east_offset = 20 + north_offset = 30 + south_offset = 30 + +/datum/map_template/dungeon/room/SmithRest + mappath = "_maps/dungeon_generator/room/SmithRest.dmm" + id = "SmithRest" + width = 30 + height = 30 + + west_offset = 14 + east_offset = 14 + north_offset = 14 + south_offset = 14 + +/datum/map_template/dungeon/room/SteamCastle + mappath = "_maps/dungeon_generator/room/SteamCastle.dmm" + id = "SteamCastle" + width = 50 + height = 70 + + west_offset = 18 + east_offset = 18 + south_offset = 26 diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm index 8b3194e612..b3b449dc28 100644 --- a/code/datums/elements/_element.dm +++ b/code/datums/elements/_element.dm @@ -7,7 +7,24 @@ /datum/element /// Option flags for element behaviour var/element_flags = NONE - + /** + * The index of the first attach argument to consider for duplicate elements + * + * All arguments from this index onwards (1 based, until `argument_hash_end_idx` is reached, if set) + * are hashed into the key to determine if this is a new unique element or one already exists + * + * Is only used when flags contains [ELEMENT_BESPOKE] + * + * This is infinity so you must explicitly set this + */ + var/argument_hash_start_idx = INFINITY + /** + * The index of the last attach argument to consider for duplicate elements + * Only used when `element_flags` contains [ELEMENT_BESPOKE]. + * If not set, it'll copy every argument from `argument_hash_start_idx` onwards as normal + */ + var/argument_hash_end_idx = 0 + var/id_arg_index = 1 /// Activates the functionality defined by the element on the given target datum @@ -15,13 +32,21 @@ SHOULD_CALL_PARENT(TRUE) if(type == /datum/element) return ELEMENT_INCOMPATIBLE - if(element_flags & ELEMENT_DETACH) - RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(Detach), override = TRUE) + SEND_SIGNAL(target, COMSIG_ELEMENT_ATTACH, src) + if(element_flags & ELEMENT_DETACH_ON_HOST_DESTROY) + RegisterSignal(target, COMSIG_QDELETING, PROC_REF(OnTargetDelete), override = TRUE) + +/datum/element/proc/OnTargetDelete(datum/source) + SIGNAL_HANDLER + Detach(source) /// Deactivates the functionality defines by the element on the given datum -/datum/element/proc/Detach(datum/source, force) +/datum/element/proc/Detach(datum/source, ...) + SIGNAL_HANDLER SHOULD_CALL_PARENT(TRUE) - UnregisterSignal(source, COMSIG_PARENT_QDELETING) + + SEND_SIGNAL(source, COMSIG_ELEMENT_DETACH, src) + UnregisterSignal(source, COMSIG_QDELETING) /datum/element/Destroy(force) if(!force) @@ -32,13 +57,55 @@ //DATUM PROCS /// Finds the singleton for the element type given and attaches it to src -/datum/proc/AddElement(eletype, ...) - var/datum/element/ele = SSdcs.GetElement(eletype) - args[1] = src - if(ele.Attach(arglist(args)) == ELEMENT_INCOMPATIBLE) - CRASH("Incompatible [eletype] assigned to a [type]! args: [json_encode(args)]") - -/// Finds the singleton for the element type given and detaches it from src -/datum/proc/RemoveElement(eletype) - var/datum/element/ele = SSdcs.GetElement(eletype) - ele.Detach(src) +/datum/proc/_AddElement(list/arguments) + if(QDELING(src)) + var/datum/element/element_type = arguments[1] + stack_trace("We just tried to add the element [element_type] to a qdeleted datum, something is fucked") + return + + var/datum/element/ele = SSdcs.GetElement(arguments) + if(!ele) // We couldn't fetch the element, likely because it was not an element. + return // the crash message has already been sent + arguments[1] = src + if(ele.Attach(arglist(arguments)) == ELEMENT_INCOMPATIBLE) + CRASH("Incompatible element [ele.type] was assigned to a [type]! args: [json_encode(args)]") + +/** + * Finds the singleton for the element type given and detaches it from src + * You only need additional arguments beyond the type if you're using [ELEMENT_BESPOKE] + */ +/datum/proc/_RemoveElement(list/arguments) + var/datum/element/ele = SSdcs.GetElement(arguments, FALSE) + if(!ele) // We couldn't fetch the element, likely because it didn't exist. + return + if(ele.element_flags & ELEMENT_COMPLEX_DETACH) + arguments[1] = src + ele.Detach(arglist(arguments)) + else + ele.Detach(src) + +/** + * Used to manage (typically non_bespoke) elements with multiple sources through traits + * so we don't have to make them a components again. + * The element will be later removed once all trait sources are gone, there's no need of a + * "RemoveElementTrait" counterpart. + */ +/datum/proc/AddElementTrait(trait, source, datum/element/eletype, ...) + if(!ispath(eletype, /datum/element)) + CRASH("AddElementTrait called, but [eletype] is not of a /datum/element path") + ADD_TRAIT(src, trait, source) + if(HAS_TRAIT_NOT_FROM(src, trait, source)) + return + var/list/arguments = list(eletype) + /// 3 is the length of fixed args of this proc, any further one is passed down to AddElement. + if(length(args) > 3) + arguments += args.Copy(4) + /// We actually pass down a copy of the arguments since it's manipulated by the end of the proc. + _AddElement(arguments.Copy()) + var/datum/ele = SSdcs.GetElement(arguments) + ele.RegisterSignal(src, SIGNAL_REMOVETRAIT(trait), TYPE_PROC_REF(/datum/element, _detach_on_trait_removed)) + +/datum/element/proc/_detach_on_trait_removed(datum/source, trait) + SIGNAL_HANDLER + Detach(source) + UnregisterSignal(source, SIGNAL_REMOVETRAIT(trait)) diff --git a/code/datums/elements/digitalcamo.dm b/code/datums/elements/digitalcamo.dm deleted file mode 100644 index 0803d975a3..0000000000 --- a/code/datums/elements/digitalcamo.dm +++ /dev/null @@ -1,35 +0,0 @@ -/datum/element/digitalcamo - element_flags = ELEMENT_DETACH - var/list/attached_mobs = list() - -/datum/element/digitalcamo/New() - . = ..() - START_PROCESSING(SSdcs, src) - -/datum/element/digitalcamo/Attach(datum/target) - . = ..() - if(!isliving(target) || (target in attached_mobs)) - return ELEMENT_INCOMPATIBLE - RegisterSignal(target, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) - RegisterSignal(target, COMSIG_LIVING_CAN_TRACK, PROC_REF(can_track)) - var/image/img = image(loc = target) - img.override = TRUE - attached_mobs[target] = img - -/datum/element/digitalcamo/Detach(datum/target) - . = ..() - UnregisterSignal(target, list(COMSIG_PARENT_EXAMINE, COMSIG_LIVING_CAN_TRACK)) - for(var/mob/living/silicon/ai/AI in GLOB.player_list) - AI.client.images -= attached_mobs[target] - attached_mobs -= target - -/datum/element/digitalcamo/proc/on_examine(datum/source, mob/M) - to_chat(M, "[source.p_their()] skin seems to be shifting and morphing like is moving around below it.") - -/datum/element/digitalcamo/proc/can_track(datum/source) - return COMPONENT_CANT_TRACK - -/datum/element/digitalcamo/process() - for(var/mob/living/silicon/ai/AI in GLOB.player_list) - for(var/mob in attached_mobs) - AI.client.images |= attached_mobs[mob] diff --git a/code/datums/elements/earhealing.dm b/code/datums/elements/earhealing.dm index 935d43334b..55461e1633 100644 --- a/code/datums/elements/earhealing.dm +++ b/code/datums/elements/earhealing.dm @@ -1,5 +1,5 @@ /datum/element/earhealing - element_flags = ELEMENT_DETACH + element_flags = COMSIG_ELEMENT_ATTACH var/list/user_by_item = list() /datum/element/earhealing/New() diff --git a/code/datums/elements/firestacker.dm b/code/datums/elements/firestacker.dm index 7809c088d1..0596fcde1b 100644 --- a/code/datums/elements/firestacker.dm +++ b/code/datums/elements/firestacker.dm @@ -1,40 +1,46 @@ /** - * Can be applied to /atom/movable subtypes to make them apply fire stacks to things they hit - */ + * Can be applied to /atom/movable subtypes to make them apply fire stacks to things they hit + */ /datum/element/firestacker - element_flags = ELEMENT_DETACH - /// A list in format {atom/movable/owner, number} - /// Used to keep track of movables which want to apply a different number of fire stacks than default - var/list/amount_by_owner = list() + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// How many firestacks to apply per hit + var/amount /datum/element/firestacker/Attach(datum/target, amount) . = ..() - if(!ismovableatom(target)) + + if(!ismovable(target)) return ELEMENT_INCOMPATIBLE + + src.amount = amount + RegisterSignal(target, COMSIG_MOVABLE_IMPACT, PROC_REF(impact), override = TRUE) if(isitem(target)) RegisterSignal(target, COMSIG_ITEM_ATTACK, PROC_REF(item_attack), override = TRUE) RegisterSignal(target, COMSIG_ITEM_ATTACK_SELF, PROC_REF(item_attack_self), override = TRUE) - if(amount) // If amount is not given we default to 1 and don't need to save it here - amount_by_owner[target] = amount - -/datum/element/firestacker/Detach(datum/source, force) +/datum/element/firestacker/Detach(datum/source) . = ..() UnregisterSignal(source, list(COMSIG_MOVABLE_IMPACT, COMSIG_ITEM_ATTACK, COMSIG_ITEM_ATTACK_SELF)) - amount_by_owner -= source /datum/element/firestacker/proc/stack_on(datum/owner, mob/living/target) - target.adjust_fire_stacks(amount_by_owner[owner] || 1) + target.adjust_fire_stacks(amount) -/datum/element/firestacker/proc/impact(datum/source, atom/hit_atom, datum/thrownthing/throwingdatum) - if(isliving(hit_atom)) +/datum/element/firestacker/proc/impact(datum/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught) + SIGNAL_HANDLER + + if(!caught && isliving(hit_atom)) stack_on(source, hit_atom) /datum/element/firestacker/proc/item_attack(datum/source, atom/movable/target, mob/living/user) + SIGNAL_HANDLER + if(isliving(target)) stack_on(source, target) /datum/element/firestacker/proc/item_attack_self(datum/source, mob/user) + SIGNAL_HANDLER + if(isliving(user)) stack_on(source, user) diff --git a/code/datums/elements/snail_crawl.dm b/code/datums/elements/snail_crawl.dm index 786d832916..4809e94d5a 100644 --- a/code/datums/elements/snail_crawl.dm +++ b/code/datums/elements/snail_crawl.dm @@ -1,5 +1,5 @@ /datum/element/snailcrawl - element_flags = ELEMENT_DETACH + element_flags = COMSIG_ELEMENT_ATTACH /datum/element/snailcrawl/Attach(datum/target) . = ..() diff --git a/code/datums/status_effects/rogue/roguebuff.dm b/code/datums/status_effects/rogue/roguebuff.dm index 89556b9d28..259e93955a 100644 --- a/code/datums/status_effects/rogue/roguebuff.dm +++ b/code/datums/status_effects/rogue/roguebuff.dm @@ -232,6 +232,12 @@ desc = "" icon_state = "weed" +/datum/status_effect/buff/seelie_drugs + id = "seelie drugs" + alert_type = /atom/movable/screen/alert/status_effect/buff/druqks + effectedstats = list("intelligence" = 2, "endurance" = 4, "speed" = -3) + duration = 20 SECONDS + /datum/status_effect/buff/vitae id = "druqks" alert_type = /atom/movable/screen/alert/status_effect/buff/vitae diff --git a/code/game/area/areas/ruins/tomb.dm b/code/game/area/areas/ruins/tomb.dm new file mode 100644 index 0000000000..46f53f258f --- /dev/null +++ b/code/game/area/areas/ruins/tomb.dm @@ -0,0 +1,104 @@ +// Areas for the tomb +// Copied from other areas but they all have the "Tomb of Matthios" name +// The only real difference is audio + +/area/rogue/under/tomb + name = "Tomb of Matthios" + icon_state = "basement" + first_time_text = "THE TOMB OF MATTHIOS" + soundenv = 5 + ambientsounds = AMB_BASEMENT + ambientnight = AMB_BASEMENT + spookysounds = SPOOKY_DUNGEON + spookynight = SPOOKY_DUNGEON + droning_sound = 'sound/music/area/catacombs.ogg' + droning_sound_dusk = null + droning_sound_night = null + +/area/rogue/under/tomb/indoors + icon_state = "indoors" + +// Some nice sounds for rest areas +/area/rogue/under/tomb/indoors/rest + icon_state = "shelter" + ambientsounds = AMB_TOWNDAY + ambientnight = AMB_TOWNNIGHT + droning_sound = 'sound/music/area/townstreets.ogg' + droning_sound_dusk = 'sound/music/area/septimus.ogg' + droning_sound_night = 'sound/music/area/sleeping.ogg' + +/area/rogue/under/tomb/indoors/magic + icon_state = "magician" + spookysounds = SPOOKY_MYSTICAL + spookynight = SPOOKY_MYSTICAL + droning_sound = 'sound/music/area/magiciantower.ogg' + +/area/rogue/under/tomb/indoors/royal + icon_state = "manor" + droning_sound = 'sound/music/area/manor2.ogg' + +/area/rogue/under/tomb/indoors/church + icon_state = "church" + droning_sound = 'sound/music/area/church.ogg' + +/area/rogue/under/tomb/wilds + icon_state = "woods" + soundenv = 15 + ambientsounds = AMB_FORESTDAY + ambientnight = AMB_FORESTNIGHT + spookysounds = SPOOKY_CROWS + spookynight = SPOOKY_FOREST + droning_sound = 'sound/music/area/forest.ogg' + droning_sound_dusk = 'sound/music/area/septimus.ogg' + droning_sound_night = 'sound/music/area/forestnight.ogg' + +/area/rogue/under/tomb/wilds/ambush + +/area/rogue/under/tomb/wilds/bog + icon_state = "bog" + ambientsounds = AMB_BOGDAY + ambientnight = AMB_BOGNIGHT + spookysounds = SPOOKY_FROG + spookynight = SPOOKY_GEN + droning_sound = 'sound/music/area/bog.ogg' + droning_sound_dusk = null + droning_sound_night = null + +/area/rogue/under/tomb/sewer + icon_state = "sewer" + ambientsounds = AMB_CAVEWATER + ambientnight = AMB_CAVEWATER + spookysounds = SPOOKY_RATS + spookynight = SPOOKY_RATS + droning_sound = 'sound/music/area/sewers.ogg' + +/area/rogue/under/tomb/lake + icon_state = "lake" + ambientsounds = AMB_BEACH + ambientnight = AMB_BEACH + spookysounds = SPOOKY_CAVE + spookynight = SPOOKY_GEN + +/area/rogue/under/tomb/cave + icon_state = "cave" + soundenv = 8 + ambientsounds = AMB_GENCAVE + ambientnight = AMB_GENCAVE + spookysounds = SPOOKY_CAVE + spookynight = SPOOKY_CAVE + droning_sound = 'sound/music/area/caves.ogg' + +/area/rogue/under/tomb/cave/lava + icon_state = "cavelava" + ambientsounds = AMB_CAVELAVA + ambientnight = AMB_CAVELAVA + droning_sound = 'sound/music/area/decap.ogg' + +/area/rogue/under/tomb/cave/wet + icon_state = "cavewet" + ambientsounds = AMB_CAVEWATER + ambientnight = AMB_CAVEWATER + +/area/rogue/under/tomb/cave/spider + icon_state = "spider" + droning_sound = 'sound/music/area/spidercave.ogg' diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 81e0c81711..ffaea87656 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1197,3 +1197,14 @@ /atom/proc/InitializeAIController() if(ai_controller) ai_controller = new ai_controller(src) + +///Returns a list of all locations (except the area) the movable is within. +/proc/get_nested_locs(atom/movable/atom_on_location, include_turf = FALSE) + . = list() + var/atom/location = atom_on_location.loc + var/turf/our_turf = get_turf(atom_on_location) + while(location && location != our_turf) + . += location + location = location.loc + if(our_turf && include_turf) //At this point, only the turf is left, provided it exists. + . += our_turf diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 540efe0028..8f37f3f1c8 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -29,7 +29,6 @@ var/lastcardinal = 0 var/lastcardpress = 0 var/atom/movable/moving_from_pull //attempt to resume grab after moving instead of before. - var/list/client_mobs_in_contents // This contains all the client mobs within this container var/list/acted_explosions //for explosion dodging glide_size = 6 appearance_flags = TILE_BOUND|PIXEL_SCALE @@ -45,6 +44,20 @@ var/can_be_z_moved = TRUE var/jumping = FALSE var/zfalling = FALSE + /** + * an associative lazylist of relevant nested contents by "channel", the list is of the form: list(channel = list(important nested contents of that type)) + * each channel has a specific purpose and is meant to replace potentially expensive nested contents iteration. + * do NOT add channels to this for little reason as it can add considerable memory usage. + */ + var/list/important_recursive_contents + ///contains every client mob corresponding to every client eye in this container. lazily updated by SSparallax and is sparse: + ///only the last container of a client eye has this list assuming no movement since SSparallax's last fire + var/list/client_mobs_in_contents + + /// String representing the spatial grid groups we want to be held in. + /// acts as a key to the list of spatial grid contents types we exist in via SSspatial_grid.spatial_grid_categories. + /// We do it like this to prevent people trying to mutate them and to save memory on holding the lists ourselves + var/spatial_grid_key /atom/movable/proc/can_zFall(turf/source, levels = 1, turf/target, direction) if(!direction) @@ -400,6 +413,23 @@ if (length(client_mobs_in_contents)) update_parallax_contents() + var/turf/old_turf = get_turf(OldLoc) + var/turf/new_turf = get_turf(src) + + if(HAS_SPATIAL_GRID_CONTENTS(src)) + if(old_turf && new_turf && (old_turf.z != new_turf.z \ + || GET_SPATIAL_INDEX(old_turf.x) != GET_SPATIAL_INDEX(new_turf.x) \ + || GET_SPATIAL_INDEX(old_turf.y) != GET_SPATIAL_INDEX(new_turf.y))) + + SSspatial_grid.exit_cell(src, old_turf) + SSspatial_grid.enter_cell(src, new_turf) + + else if(old_turf && !new_turf) + SSspatial_grid.exit_cell(src, old_turf) + + else if(new_turf && !old_turf) + SSspatial_grid.enter_cell(src, new_turf) + return TRUE /atom/movable/Destroy(force) @@ -418,6 +448,10 @@ for(var/atom/movable/AM in contents) qdel(AM) moveToNullspace() + //This absolutely must be after moveToNullspace() + //We rely on Entered and Exited to manage this list, and the copy of this list that is on any /atom/movable "Containers" + //If we clear this before the nullspace move, a ref to this object will be hung in any of its movable containers + LAZYNULL(important_recursive_contents) invisibility = INVISIBILITY_ABSTRACT if(pulledby) pulledby.stop_pulling() @@ -563,6 +597,8 @@ /atom/movable/proc/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) set waitfor = 0 SEND_SIGNAL(src, COMSIG_MOVABLE_IMPACT, hit_atom, throwingdatum) + if(QDELETED(hit_atom)) + return return hit_atom.hitby(src, throwingdatum=throwingdatum) /atom/movable/hitby(atom/movable/AM, skipcatch, hitpush = TRUE, blocked, datum/thrownthing/throwingdatum, damage_flag = "blunt") @@ -989,3 +1025,130 @@ animate(I, alpha = 175, pixel_x = to_x, pixel_y = to_y, time = 3, transform = M, easing = CUBIC_EASING) sleep(1) animate(I, alpha = 0, transform = matrix(), time = 1) + +/atom/movable/Exited(atom/movable/gone, atom/newLoc) + . = ..() + if(!LAZYLEN(gone.important_recursive_contents)) + return + var/list/nested_locs = get_nested_locs(src) + src + for(var/channel in gone.important_recursive_contents) + for(var/atom/movable/location as anything in nested_locs) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[channel]) + recursive_contents[channel] -= gone.important_recursive_contents[channel] + switch(channel) + if(RECURSIVE_CONTENTS_CLIENT_MOBS, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + if(!length(recursive_contents[channel])) + // This relies on a nice property of the linked recursive and gridmap types + // They're defined in relation to each other, so they have the same value + SSspatial_grid.remove_grid_awareness(location, channel) + ASSOC_UNSETEMPTY(recursive_contents, channel) + UNSETEMPTY(location.important_recursive_contents) + +/atom/movable/Entered(atom/movable/arrived, atom/old_loc) + . = ..() + + if(!LAZYLEN(arrived.important_recursive_contents)) + return + var/list/nested_locs = get_nested_locs(src) + src + for(var/channel in arrived.important_recursive_contents) + for(var/atom/movable/location as anything in nested_locs) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[channel]) + switch(channel) + if(RECURSIVE_CONTENTS_CLIENT_MOBS, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + if(!length(recursive_contents[channel])) + SSspatial_grid.add_grid_awareness(location, channel) + recursive_contents[channel] |= arrived.important_recursive_contents[channel] + +///allows this movable to hear and adds itself to the important_recursive_contents list of itself and every movable loc its in +/atom/movable/proc/become_hearing_sensitive(trait_source = TRAIT_GENERIC) + var/already_hearing_sensitive = HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE) + ADD_TRAIT(src, TRAIT_HEARING_SENSITIVE, trait_source) + if(already_hearing_sensitive) // If we were already hearing sensitive, we don't wanna be in important_recursive_contents twice, else we'll have potential issues like one radio sending the same message multiple times + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + if(!length(recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE])) + SSspatial_grid.add_grid_awareness(location, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] += list(src) + + var/turf/our_turf = get_turf(src) + SSspatial_grid.add_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + +/** + * removes the hearing sensitivity channel from the important_recursive_contents list of this and all nested locs containing us if there are no more sources of the trait left + * since RECURSIVE_CONTENTS_HEARING_SENSITIVE is also a spatial grid content type, removes us from the spatial grid if the trait is removed + * + * * trait_source - trait source define or ALL, if ALL, force removes hearing sensitivity. if a trait source define, removes hearing sensitivity only if the trait is removed + */ +/atom/movable/proc/lose_hearing_sensitivity(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE)) + return + REMOVE_TRAIT(src, TRAIT_HEARING_SENSITIVE, trait_source) + if(HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE)) + return + + var/turf/our_turf = get_turf(src) + /// We get our awareness updated by the important recursive contents stuff, here we remove our membership + SSspatial_grid.remove_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] -= src + if(!length(recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE])) + SSspatial_grid.remove_grid_awareness(location, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + UNSETEMPTY(location.important_recursive_contents) + +///allows this movable to know when it has "entered" another area no matter how many movable atoms its stuffed into, uses important_recursive_contents +/atom/movable/proc/become_area_sensitive(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + ADD_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + +///removes the area sensitive channel from the important_recursive_contents list of this and all nested locs containing us if there are no more source of the trait left +/atom/movable/proc/lose_area_sensitivity(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + REMOVE_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + if(HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + +///propogates ourselves through our nested contents, similar to other important_recursive_contents procs +///main difference is that client contents need to possibly duplicate recursive contents for the clients mob AND its eye +/mob/proc/enable_client_mobs_in_contents() + for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) + LAZYINITLIST(movable_loc.important_recursive_contents) + var/list/recursive_contents = movable_loc.important_recursive_contents // blue hedgehog velocity + if(!length(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS])) + SSspatial_grid.add_grid_awareness(movable_loc, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + LAZYINITLIST(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] |= src + + var/turf/our_turf = get_turf(src) + /// We got our awareness updated by the important recursive contents stuff, now we add our membership + SSspatial_grid.add_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + +///Clears the clients channel of this mob +/mob/proc/clear_important_client_contents() + var/turf/our_turf = get_turf(src) + SSspatial_grid.remove_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + + for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) + LAZYINITLIST(movable_loc.important_recursive_contents) + var/list/recursive_contents = movable_loc.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] -= src + if(!length(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS])) + SSspatial_grid.remove_grid_awareness(movable_loc, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS) + UNSETEMPTY(movable_loc.important_recursive_contents) diff --git a/code/game/objects/effects/decals/cleanable/sigil.dm b/code/game/objects/effects/decals/cleanable/sigil.dm new file mode 100644 index 0000000000..c97f98996c --- /dev/null +++ b/code/game/objects/effects/decals/cleanable/sigil.dm @@ -0,0 +1,29 @@ +/obj/effect/decal/cleanable/sigil + name = "sigils" + desc = "Strange runics." + icon_state = "center" + icon = 'icons/effects/sigils.dmi' + +/obj/effect/decal/cleanable/sigil/N + icon_state = "N" + +/obj/effect/decal/cleanable/sigil/NE + icon_state = "NE" + +/obj/effect/decal/cleanable/sigil/E + icon_state = "E" + +/obj/effect/decal/cleanable/sigil/SE + icon_state = "SE" + +/obj/effect/decal/cleanable/sigil/S + icon_state = "S" + +/obj/effect/decal/cleanable/sigil/SW + icon_state = "SW" + +/obj/effect/decal/cleanable/sigil/W + icon_state = "W" + +/obj/effect/decal/cleanable/sigil/NW + icon_state = "NW" diff --git a/code/game/objects/effects/waterfall_effect.dm b/code/game/objects/effects/waterfall_effect.dm new file mode 100644 index 0000000000..dcb453ae58 --- /dev/null +++ b/code/game/objects/effects/waterfall_effect.dm @@ -0,0 +1,38 @@ +/obj/effect/waterfall + name = "waterfall" + icon = 'icons/effects/waterfall.dmi' + icon_state = "waterfall_temp" + pixel_y = 32 + var/datum/reagent/water_reagent = /datum/reagent/water + +/obj/effect/waterfall/Initialize() + . = ..() + var/turf/open = get_turf(src) + if(istransparentturf(open)) + return + color = initial(water_reagent.color) + var/obj/particle_emitter/effect = MakeParticleEmitter(/particles/mist/waterfall) + effect.layer = 5 + effect.alpha = 175 + +/obj/effect/waterfall/acid + water_reagent = /datum/reagent/rogueacid + +/particles/mist + name = "mist" + icon = 'icons/effects/particles/smoke.dmi' + icon_state = list("steam_1" = 1, "steam_2" = 1, "steam_3" = 1) + count = 500 + spawning = 4 + lifespan = 5 SECONDS + fade = 1 SECONDS + fadein = 1 SECONDS + velocity = generator("box", list(-0.5, -0.25, 0), list(0.5, 0.25, 0), NORMAL_RAND) + position = generator("box", list(-20, -16), list(20, -2), UNIFORM_RAND) + friction = 0.2 + grow = 0.0015 + +/particles/mist/waterfall + count = 75 + lifespan = generator("num", 2 SECONDS, 3 SECONDS) + position = generator("box", list(-20, 4), list(20, 10), UNIFORM_RAND) diff --git a/code/game/objects/items/magic_staffs.dm b/code/game/objects/items/magic_staffs.dm index cde46a376f..2621ceb06a 100644 --- a/code/game/objects/items/magic_staffs.dm +++ b/code/game/objects/items/magic_staffs.dm @@ -34,6 +34,24 @@ resistance_flags = FLAMMABLE var/cast_time_reduction = null +/obj/item/rogueweapon/woodstaff/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list( + /datum/crafting_recipe/toper_staff, + /datum/crafting_recipe/amethyst_staff, + /datum/crafting_recipe/emerald_staff, + /datum/crafting_recipe/sapphire_staff, + /datum/crafting_recipe/quartz_staff, + /datum/crafting_recipe/ruby_staff, + /datum/crafting_recipe/diamond_staff, + /datum/crafting_recipe/riddle_of_steel_staff, + ) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) + /obj/item/rogueweapon/woodstaff/getonmobprop(tag) . = ..() if(tag) @@ -150,105 +168,60 @@ resistance_flags = FIRE_PROOF static_debris = list(/obj/item/roguegem/diamond = 1) -//slapcrafting stuff - -/obj/item/rogueweapon/woodstaff/attackby(obj/item/arcane_focus, mob/living/carbon/human/user, params) - if(istype(arcane_focus, /obj/item/roguegem/yellow)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/topaz(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/roguegem/amethyst)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/amethyst(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/roguegem/green)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/emerald(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/roguegem/violet)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/sapphire(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/roguegem/blue)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/quartz(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/roguegem/blue)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/quartz(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/roguegem/diamond)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/diamond(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/roguegem)) //has to be checked last because someone made the ruby also the base gem object - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/ruby(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return - else if(istype(arcane_focus, /obj/item/riddleofsteel)) - var/crafttime = (100 - ((user.mind?.get_skill_level(/datum/skill/magic/arcane))*5)) - if(do_after(user, crafttime, target = src)) - playsound(loc, 'modular_azurepeak/sound/spellbooks/crystal.ogg', 100, TRUE) - user.visible_message(span_warning("[user] slots [user.p_their()] [arcane_focus] into the staff!"), \ - span_notice("I empower the staff with an arcane-focus!")) - new /obj/item/rogueweapon/woodstaff/riddle_of_steel(get_turf(src)) - qdel(arcane_focus) - qdel(src) - else - return +//crafting datums + +/datum/crafting_recipe/toper_staff + name = "topez-focused staff" + result = /obj/item/rogueweapon/woodstaff/topaz + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/roguegem/yellow = 1) + craftdiff = 0 + +/datum/crafting_recipe/amethyst_staff + name = "amethyst-focused staff" + result = /obj/item/rogueweapon/woodstaff/amethyst + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/roguegem/amethyst = 1) + craftdiff = 0 + +/datum/crafting_recipe/emerald_staff + name = "gemerald-focused staff" + result = /obj/item/rogueweapon/woodstaff/emerald + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/roguegem/green = 1) + craftdiff = 0 + +/datum/crafting_recipe/sapphire_staff + name = "saffira-focused staff" + result = /obj/item/rogueweapon/woodstaff/sapphire + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/roguegem/violet = 1) + craftdiff = 0 + +/datum/crafting_recipe/quartz_staff + name = "blortz-focused staff" + result = /obj/item/rogueweapon/woodstaff/quartz + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/roguegem/blue = 1) + craftdiff = 0 + +/datum/crafting_recipe/ruby_staff + name = "rontz-focused staff" + result = /obj/item/rogueweapon/woodstaff/ruby + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/roguegem/ruby = 1) + craftdiff = 0 + +/datum/crafting_recipe/diamond_staff + name = "dorpel-focused staff" + result = /obj/item/rogueweapon/woodstaff/diamond + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/roguegem/diamond = 1) + craftdiff = 0 + +/datum/crafting_recipe/riddle_of_steel_staff + name = "Staff of the Riddlesteel" + result = /obj/item/rogueweapon/woodstaff/riddle_of_steel + reqs = list(/obj/item/rogueweapon/woodstaff = 1, + /obj/item/riddleofsteel = 1) + craftdiff = 0 diff --git a/code/game/objects/items/rogueitems/gems.dm b/code/game/objects/items/rogueitems/gems.dm index 3ac21f3f8e..60df4029fa 100644 --- a/code/game/objects/items/rogueitems/gems.dm +++ b/code/game/objects/items/rogueitems/gems.dm @@ -1,6 +1,6 @@ /obj/item/roguegem - name = "ruby" + name = "mother of all gems" icon_state = "ruby_cut" icon = 'icons/roguetown/items/gems.dmi' desc = "Its facets shine so brightly.." @@ -10,7 +10,7 @@ slot_flags = ITEM_SLOT_MOUTH dropshrink = 0.4 drop_sound = 'sound/items/gem.ogg' - sellprice = 100 + sellprice = 1 static_price = FALSE resistance_flags = FIRE_PROOF @@ -33,11 +33,29 @@ sellprice = 42 desc = "Glints with verdant brilliance." +/obj/item/roguegem/green/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/emerald_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) + /obj/item/roguegem/blue name = "quartz" icon_state = "quartz_cut" sellprice = 88 - desc = "Pale blue, like a frozen tear." // i am not sure if this is really quartz. + desc = "Pale blue, like a frozen tear." + +/obj/item/roguegem/blue/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/quartz_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) /obj/item/roguegem/yellow name = "topaz" @@ -45,18 +63,86 @@ sellprice = 34 desc = "Its amber hues remind you of the sunset." +/obj/item/roguegem/yellow/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/toper_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) + /obj/item/roguegem/violet name = "sapphire" icon_state = "sapphire_cut" sellprice = 56 desc = "This gem is admired by many wizards." +/obj/item/roguegem/violet/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/sapphire_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) + +/obj/item/roguegem/ruby + name = "Ruby" + icon_state = "ruby_cut" + sellprice = 100 + desc = "Its facets shine so brightly..." + +/obj/item/roguegem/ruby/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/ruby_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) + + /obj/item/roguegem/diamond name = "diamond" icon_state = "diamond_cut" sellprice = 121 desc = "Beautifully clear, it demands respect." +/obj/item/roguegem/diamond/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/diamond_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) + +/obj/item/roguegem/amethyst + name = "amythortz" + icon_state = "amethyst" + desc = "A deep lavender crystal, it surges with magical energy, yet it's artificial nature means it is worth little." + +/obj/item/roguegem/amethyst/Initialize() + . = ..() + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/amethyst_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) + +/obj/item/roguegem/random + name = "random gem" + desc = "You shouldn't be seeing this." + icon_state = null + +/obj/item/roguegem/random/Initialize() + ..() + var/newgem = list(/obj/item/roguegem/ruby = 5, /obj/item/roguegem/green = 15, /obj/item/roguegem/blue = 10, /obj/item/roguegem/yellow = 20, /obj/item/roguegem/violet = 10, /obj/item/roguegem/diamond = 5, /obj/item/riddleofsteel = 1, /obj/item/rogueore/silver = 3) + var/pickgem = pickweight(newgem) + new pickgem(get_turf(src)) + qdel(src) /// riddle @@ -77,3 +163,11 @@ /obj/item/riddleofsteel/Initialize() . = ..() set_light(2, 2, 1, l_color = "#ff0d0d") + + + var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/quartz_staff,) + + AddElement( + /datum/element/slapcrafting,\ + slapcraft_recipes = slapcraft_recipe_list,\ + ) diff --git a/code/game/objects/items/rogueitems/natural/animals.dm b/code/game/objects/items/rogueitems/natural/animals.dm index 50aefceacc..c857641b03 100644 --- a/code/game/objects/items/rogueitems/natural/animals.dm +++ b/code/game/objects/items/rogueitems/natural/animals.dm @@ -41,15 +41,11 @@ force = 0 throwforce = 0 sellprice = 10 - var/storage_type = /datum/component/storage/concrete + var/storage_type = /datum/component/storage/concrete/roguetown/saddle -/obj/item/natural/saddle/ComponentInitialize() +/obj/item/natural/saddle/Initialize() . = ..() AddComponent(storage_type) - var/datum/component/storage/STR = GetComponent(/datum/component/storage) - STR.max_combined_w_class = 16 - STR.max_w_class = WEIGHT_CLASS_NORMAL - STR.max_items = 12 /obj/item/natural/saddle/attack(mob/living/target, mob/living/carbon/human/user) if(istype(target, /mob/living/simple_animal)) diff --git a/code/game/objects/items/rogueitems/natural/stones.dm b/code/game/objects/items/rogueitems/natural/stones.dm index 289a470621..5efb0c759c 100644 --- a/code/game/objects/items/rogueitems/natural/stones.dm +++ b/code/game/objects/items/rogueitems/natural/stones.dm @@ -362,3 +362,23 @@ GLOBAL_LIST_INIT(stone_personality_descs, list( /obj/item/natural/rock/gem mineralType = /obj/effect/spawner/lootdrop/roguetown/gems + +/obj/item/natural/rock/random_ore + name = "rock?" + desc = "Wait, this shouldn't be here?" + icon_state = "stonerandom" + +/obj/item/natural/rock/random/Initialize() + . = ..() + var/obj/item/natural/rock/theboi = pick(list( + /obj/item/natural/rock/gold, + /obj/item/natural/rock/iron, + /obj/item/natural/rock/coal, + /obj/item/natural/rock/salt, + /obj/item/natural/rock/silver, + /obj/item/natural/rock/copper, + /obj/item/natural/rock/tin, + /obj/item/natural/rock/gem + )) + new theboi(get_turf(src)) + return INITIALIZE_HINT_QDEL diff --git a/code/game/objects/items/rogueitems/painting.dm b/code/game/objects/items/rogueitems/painting.dm index b523608790..2283951c22 100644 --- a/code/game/objects/items/rogueitems/painting.dm +++ b/code/game/objects/items/rogueitems/painting.dm @@ -75,3 +75,15 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fluff/walldeco/painting/queen, 32) stolen_painting = /obj/item/rogue/painting/seraphina MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fluff/walldeco/painting/seraphina, 32) + + +/obj/item/rogue/painting/skullzhg + icon_state = "skullpainting" + desc = "A moody scene depicting a skull and candles on a table. Memento mori." + sellprice = 40 + deployed_structure = /obj/structure/fluff/walldeco/painting/skull + +/obj/structure/fluff/walldeco/painting/skull + desc = "A moody scene depicting a skull and candles on a table. Memento mori." + icon_state = "skullpainting_deployed" + stolen_painting = /obj/item/rogue/painting/skullzhg diff --git a/code/game/objects/items/storage/bags.dm b/code/game/objects/items/storage/bags.dm index f50fc6c3b7..c41fde4293 100644 --- a/code/game/objects/items/storage/bags.dm +++ b/code/game/objects/items/storage/bags.dm @@ -1,14 +1,6 @@ /obj/item/storage/bag slot_flags = ITEM_SLOT_BELT -/obj/item/storage/bag/ComponentInitialize() - . = ..() - var/datum/component/storage/STR = GetComponent(/datum/component/storage) - STR.allow_quick_gather = TRUE - STR.allow_quick_empty = TRUE - STR.display_numerical_stacking = TRUE - STR.click_gather = TRUE - /* * Trays - Agouri *///wip @@ -30,11 +22,8 @@ icon_state = "tray_psy" desc = "" -/obj/item/storage/bag/tray/ComponentInitialize() +/obj/item/storage/bag/tray/Initialize() . = ..() - var/datum/component/storage/STR = GetComponent(/datum/component/storage) - STR.insert_preposition = "on" - STR.max_w_class = WEIGHT_CLASS_NORMAL // changed to fit platters, take care if its abused update_icon() /obj/item/storage/bag/tray/Moved() diff --git a/code/game/objects/items/storage/storage.dm b/code/game/objects/items/storage/storage.dm index 7442267704..de2c0d379d 100644 --- a/code/game/objects/items/storage/storage.dm +++ b/code/game/objects/items/storage/storage.dm @@ -6,15 +6,16 @@ var/list/populate_contents = list() obj_flags = CAN_BE_HIT -/obj/item/storage/get_dumping_location(obj/item/storage/source,mob/user) +/obj/item/storage/get_dumping_location(obj/item/storage/source, mob/user) return src -/obj/item/storage/Initialize() +/obj/item/storage/Initialize(mapload) . = ..() + AddComponent(component_type) PopulateContents() -/obj/item/storage/ComponentInitialize() - AddComponent(component_type) + for (var/obj/item/item in src) + item.item_flags |= IN_STORAGE /obj/item/storage/AllowDrop() return FALSE diff --git a/code/game/objects/structures/crates_lockers/roguetown.dm b/code/game/objects/structures/crates_lockers/roguetown.dm index ae714a3790..3f93f73cd7 100644 --- a/code/game/objects/structures/crates_lockers/roguetown.dm +++ b/code/game/objects/structures/crates_lockers/roguetown.dm @@ -45,7 +45,7 @@ /obj/item/clothing/mask/cigarette/pipe/westman=10, /obj/item/storage/backpack/rogue/satchel=33, /obj/item/storage/roguebag=33, - /obj/item/roguegem=1, + /obj/item/roguegem/ruby=1, /obj/item/roguegem/blue=2, /obj/item/roguegem/violet=4, /obj/item/roguegem/green=6, diff --git a/code/game/objects/structures/flora.dm b/code/game/objects/structures/flora.dm index dadcb865f1..5a999a7ee1 100644 --- a/code/game/objects/structures/flora.dm +++ b/code/game/objects/structures/flora.dm @@ -300,7 +300,7 @@ /obj/item/twohanded/required/kirbyplants/Initialize() . = ..() AddComponent(/datum/component/tactical) - addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, AddComponent), /datum/component/beauty, 500), 0) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, _AddComponent), /datum/component/beauty, 500), 0) /obj/item/twohanded/required/kirbyplants/random icon_state = "random_plant" diff --git a/code/game/objects/structures/fluff.dm b/code/game/objects/structures/fluff.dm index 5016c85b09..c5a427a41f 100644 --- a/code/game/objects/structures/fluff.dm +++ b/code/game/objects/structures/fluff.dm @@ -982,7 +982,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fluff/wallclock/vampire, 32) layer = BELOW_MOB_LAYER max_integrity = 100 sellprice = 40 - flags_1 = HEAR_1 var/chance2hear = 30 buckleverb = "crucifie" can_buckle = 1 @@ -992,6 +991,14 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fluff/wallclock/vampire, 32) buckle_requires_restraints = 1 buckle_prevents_pull = 1 +/obj/structure/fluff/psycross/Initialize() + . = ..() + become_hearing_sensitive() + +/obj/structure/fluff/psycross/Destroy() + lose_hearing_sensitivity() + return ..() + /obj/structure/fluff/psycross/post_buckle_mob(mob/living/M) ..() M.set_mob_offsets("bed_buckle", _x = 0, _y = 2) diff --git a/code/game/objects/structures/mineral_doors.dm b/code/game/objects/structures/mineral_doors.dm index ef72cc6c13..7061aad3ac 100644 --- a/code/game/objects/structures/mineral_doors.dm +++ b/code/game/objects/structures/mineral_doors.dm @@ -570,12 +570,12 @@ if(isSwitchingStates || door_opened) return if(locked) - user.visible_message(span_warning("[user] unlocks [src]."), \ + user?.visible_message(span_warning("[user] unlocks [src]."), \ span_notice("I unlock [src].")) playsound(src, unlocksound, 100) locked = 0 else - user.visible_message(span_warning("[user] locks [src]."), \ + user?.visible_message(span_warning("[user] locks [src]."), \ span_notice("I lock [src].")) playsound(src, locksound, 100) locked = 1 diff --git a/code/game/objects/structures/roguetown/hidden_doors.dm b/code/game/objects/structures/roguetown/hidden_doors.dm new file mode 100644 index 0000000000..ac37ab5784 --- /dev/null +++ b/code/game/objects/structures/roguetown/hidden_doors.dm @@ -0,0 +1,218 @@ +/obj/structure/mineral_door/secret + name = "wall" + icon = 'icons/turf/walls/brick_wall.dmi' + resistance_flags = NONE + max_integrity = 9999 + damage_deflection = 30 + layer = ABOVE_MOB_LAYER + + repairable = FALSE + repair_cost_first = null + repair_cost_second = null + repair_skill = null + + var/open_phrase = "open sesame" + + var/speaking_distance = 1 + var/lang = /datum/language/common + var/list/vip + var/vipmessage + +/obj/structure/mineral_door/secret/Initialize(mapload, ...) + . = ..() + open_phrase = open_word() + " " + magic_word() + +/obj/structure/mineral_door/secret/redstone_triggered(mob/user) + if(!door_opened) + force_open() + else + force_closed() + +/obj/structure/mineral_door/secret/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, message_mode, original_message) + var/mob/living/carbon/human/H = speaker + if(speaker == src) //door speaking to itself + return FALSE + var/distance = get_dist(speaker, src) + if(distance > speaking_distance) + return FALSE + if(obj_broken) //door is broken + return FALSE + if(!ishuman(speaker)) + return FALSE + + var/message2recognize = sanitize_hear_message(original_message) + + if(is_type_in_list(H.mind?.assigned_role, vip)) //are they a VIP? + if(findtext(message2recognize, "help")) + send_speech(span_purple("'say phrase'... 'set phrase'..."), speaking_distance, src, message_language = lang, message_mode = MODE_WHISPER) + return TRUE + if(findtext(message2recognize, "say phrase")) + send_speech(span_purple("[open_phrase]..."), speaking_distance, src, message_language = lang, message_mode = MODE_WHISPER) + return TRUE + if(findtext(message2recognize, "set phrase")) + var/new_pass = stripped_input(H, "What should the new close phrase be?") + open_phrase = new_pass + send_speech(span_purple("It is done, [flavor_name()]..."), speaking_distance, src, message_language = lang, message_mode = MODE_WHISPER) + return TRUE + + if(findtext(message2recognize, open_phrase)) + if(!door_opened) + force_open() + else + force_closed() + return TRUE + +/obj/structure/mineral_door/secret/Open(silent = FALSE) + isSwitchingStates = TRUE + if(!silent) + playsound(src, openSound, 90) + if(!windowed) + set_opacity(FALSE) + animate(src, pixel_x = -22, alpha = 50, time = close_delay) + sleep(close_delay) + density = FALSE + door_opened = TRUE + layer = OPEN_DOOR_LAYER + air_update_turf(TRUE) + isSwitchingStates = FALSE + + if(close_delay > 0) + addtimer(CALLBACK(src, PROC_REF(Close), silent), close_delay) + +/obj/structure/mineral_door/secret/force_open() + isSwitchingStates = TRUE + if(!windowed) + set_opacity(FALSE) + animate(src, pixel_x = -22, alpha = 50, time = close_delay) + sleep(close_delay) + density = FALSE + door_opened = TRUE + layer = OPEN_DOOR_LAYER + air_update_turf(TRUE) + isSwitchingStates = FALSE + + if(close_delay > 0) + addtimer(CALLBACK(src, PROC_REF(Close)), close_delay) + +/obj/structure/mineral_door/secret/Close(silent = FALSE) + if(isSwitchingStates || !door_opened) + return + var/turf/T = get_turf(src) + for(var/mob/living/L in T) + return + isSwitchingStates = TRUE + if(!silent) + playsound(src, closeSound, 90) + animate(src, pixel_x = 0, alpha = 255, time = close_delay) + sleep(close_delay) + density = TRUE + if(!windowed) + set_opacity(TRUE) + door_opened = FALSE + layer = CLOSED_DOOR_LAYER + air_update_turf(TRUE) + isSwitchingStates = FALSE + playsound(src, locksound, 100) + locked = TRUE + +/obj/structure/mineral_door/secret/force_closed() + isSwitchingStates = TRUE + if(!windowed) + set_opacity(TRUE) + animate(src, pixel_x = 0, alpha = 255, time = close_delay) + sleep(close_delay) + density = TRUE + door_opened = FALSE + layer = CLOSED_DOOR_LAYER + air_update_turf(TRUE) + isSwitchingStates = FALSE + +/proc/open_word() + var/list/open_word = list( + "open", + "pass", + "part", + "break", + "reveal", + "unbar", + "gape", //You wanted this. + "extend", + "widen", + "unfold", + "rise" + ) + return pick(open_word) + +/proc/close_word() + var/list/close_word = list( + "close", + "seal", + "still", + "fade", + "retreat", + "consume", + "envelope", + "hide", + "halt", + "cease", + "vanish", + "end" + ) + return pick(close_word) + + +/proc/magic_word() + var/list/magic_word = list( + "sesame", + "abyss", + "fire", + "wind", + "earth", + "shadow", + "night", + "oblivion", + "void", + "time", + "dead", + "decay", + "gods", + "ancient", + "twisted", + "corrupt", + "secrets", + "lore", + "text", + "ritual", + "sacrifice", + "deal", + "pact", + "bargain", + "ritual", + "dream", + "nightmare", + "vision", + "hunger", + "lust", + "necra", + "noc", + "psydon" + ) + return pick(magic_word) + +/proc/flavor_name() + var/list/flavor_name = list( + "my friend", + "love", + "my love", + "honey", + "darling", + "stranger", + "companion", + "mate", + "you harlot", + "comrade", + "fellow", + "chum", + "bafoon" + ) + return pick(flavor_name) diff --git a/code/game/objects/structures/statues.dm b/code/game/objects/structures/statues.dm index c97fa612ea..a988a3308f 100644 --- a/code/game/objects/structures/statues.dm +++ b/code/game/objects/structures/statues.dm @@ -14,7 +14,7 @@ /obj/structure/statue/Initialize() . = ..() AddComponent(art_type, impressiveness) - addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, AddComponent), /datum/component/beauty, impressiveness * 75), 0) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, _AddComponent), /datum/component/beauty, impressiveness * 75), 0) /obj/structure/statue/deconstruct(disassembled = TRUE) if(!(flags_1 & NODECONSTRUCT_1)) diff --git a/code/game/objects/structures/walldeco.dm b/code/game/objects/structures/walldeco.dm index b102d78e5e..d2d47acd7a 100644 --- a/code/game/objects/structures/walldeco.dm +++ b/code/game/objects/structures/walldeco.dm @@ -347,3 +347,94 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fluff/walldeco/alarm, 32) playsound(loc, 'sound/misc/gold_license.ogg', 100, TRUE, -1) say("UNKNOWN CREATURE IN SECURE AREA - DESIST AT ONCE!!") // what're you gonna do; tell it to stop? next_yap = world.time + 6 SECONDS + +/obj/structure/fluff/walldeco/vinez // overlay vines for more flexibile mapping + icon_state = "vinez" + +/obj/structure/fluff/walldeco/vinez/l + pixel_x = -32 + +/obj/structure/fluff/walldeco/vinez/r + pixel_x = 32 + +/obj/structure/fluff/walldeco/vinez/offset + icon_state = "vinez" + pixel_y = 32 + +/obj/structure/fluff/walldeco/vinez/blue + icon_state = "vinez_blue" + +/obj/structure/fluff/walldeco/vinez/red + icon_state = "vinez_red" + +/obj/structure/fluff/walldeco/bath // suggestive stonework + icon_state = "bath1" + pixel_x = -32 + alpha = 210 + +/obj/structure/fluff/walldeco/bath/two + icon_state = "bath2" + pixel_x = -29 + +/obj/structure/fluff/walldeco/bath/three + icon_state = "bath3" + pixel_x = -29 + +/obj/structure/fluff/walldeco/bath/four + icon_state = "bath4" + pixel_y = 32 + pixel_x = 0 + +/obj/structure/fluff/walldeco/bath/five + icon_state = "bath5" + pixel_x = -29 + +/obj/structure/fluff/walldeco/bath/six + icon_state = "bath6" + pixel_x = -29 + +/obj/structure/fluff/walldeco/bath/seven + icon_state = "bath7" + pixel_x = 32 + +/obj/structure/fluff/walldeco/bath/gents + icon_state = "gents" + pixel_x = 0 + pixel_y = 32 + +/obj/structure/fluff/walldeco/bath/ladies + icon_state = "ladies" + pixel_x = 0 + pixel_y = 32 + +/obj/structure/fluff/walldeco/bath/wallrope + icon_state = "wallrope" + layer = WALL_OBJ_LAYER+0.1 + pixel_x = 0 + pixel_y = 0 + color = "#d66262" + +/obj/structure/fluff/walldeco/sign/saiga + name = "The Drunken Saiga" + icon_state = "shopsign_inn_saiga_right" + plane = -1 + pixel_x = 3 + pixel_y = 16 + +/obj/structure/fluff/walldeco/sign/saiga/left + icon_state = "shopsign_inn_saiga_left" + +/obj/structure/fluff/walldeco/sign/trophy + name = "saiga trophy" + icon_state = "saiga_trophy" + pixel_y = 32 + +/obj/effect/decal/shadow_floor + name = "" + desc = "" + icon = 'icons/roguetown/misc/decoration.dmi' + icon_state = "shadow_floor" + mouse_opacity = 0 + +/obj/effect/decal/shadow_floor/corner + icon_state = "shad_floorcorn" diff --git a/code/game/say.dm b/code/game/say.dm index 8630378e6d..40fdfcb943 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -36,9 +36,12 @@ GLOBAL_LIST_INIT(freqtospan, list( /atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language = null, message_mode) var/rendered = compose_message(src, message_language, message, , spans, message_mode) - for(var/_AM in get_hearers_in_view(range, source)) - var/atom/movable/AM = _AM - AM.Hear(rendered, src, message_language, message, , spans, message_mode) + for(var/atom/movable/hearing_movable as anything in get_hearers_in_view(range, source)) + if(!hearing_movable) // Should not get nulls, but just in case. + stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!") + continue + + hearing_movable.Hear(rendered, src, message_language, message, , spans, message_mode) /atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, message_mode, face_name = FALSE) //This proc uses text() because it is faster than appending strings. Thanks BYOND. diff --git a/code/game/turfs/change_turf.dm b/code/game/turfs/change_turf.dm index 9f06d741ea..2763fb17d7 100644 --- a/code/game/turfs/change_turf.dm +++ b/code/game/turfs/change_turf.dm @@ -97,7 +97,7 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list( SEND_SIGNAL(src, COMSIG_TURF_CHANGE, path, new_baseturfs, flags, transferring_comps) for(var/i in transferring_comps) var/datum/component/comp = i - comp.RemoveComponent() + comp.ClearFromParent() changing_turf = TRUE qdel(src) //Just get the side effects and call Destroy diff --git a/code/game/turfs/closed/_closed.dm b/code/game/turfs/closed/_closed.dm index 571533117b..3806ace40d 100644 --- a/code/game/turfs/closed/_closed.dm +++ b/code/game/turfs/closed/_closed.dm @@ -24,14 +24,6 @@ wallpress(L) return -/turf/closed/proc/feel_turf(mob/living/user) - to_chat(user, span_notice("I start feeling around [src]")) - if(!do_after(user, 1.5 SECONDS, src)) - return - - for(var/obj/structure/lever/hidden/lever in contents) - lever.feel_button(user) - /turf/closed/proc/wallpress(mob/living/user) if(user.wallpressed) return @@ -185,6 +177,9 @@ user.start_pulling(pulling,supress_message = TRUE) if(user.m_intent != MOVE_INTENT_SNEAK) playsound(user, 'sound/foley/climb.ogg', 100, TRUE) + if(L.mind) + L.mind?.add_sleep_experience(/datum/skill/misc/climbing, (L.STAINT/2), FALSE) + return TRUE else ..() @@ -210,6 +205,14 @@ return TRUE return ..() +/turf/closed/proc/feel_turf(mob/living/user) + to_chat(user, span_notice("I start feeling around [src]")) + if(!do_after(user, 1.5 SECONDS, src)) + return + + for(var/obj/structure/lever/hidden/lever in contents) + lever.feel_button(user) + /turf/closed/indestructible name = "wall" icon = 'icons/turf/walls.dmi' diff --git a/code/game/turfs/closed/wall/roguewalls.dm b/code/game/turfs/closed/wall/roguewalls.dm index f37e8e2fff..b1db234cf8 100644 --- a/code/game/turfs/closed/wall/roguewalls.dm +++ b/code/game/turfs/closed/wall/roguewalls.dm @@ -434,3 +434,8 @@ /turf/closed/wall/mineral/rogue/decostone/mossy/red/cand icon_state = "decostone-cand-red" + +/turf/closed/dungeon_void + name = "thick dungeon shroud" + icon = 'icons/turf/roguewall.dmi' + icon_state = "shroud1" diff --git a/code/game/turfs/closed/walls.dm b/code/game/turfs/closed/walls.dm index 055c3f534c..aeed546900 100644 --- a/code/game/turfs/closed/walls.dm +++ b/code/game/turfs/closed/walls.dm @@ -96,7 +96,7 @@ if(.) return user.changeNext_move(CLICK_CD_MELEE) - to_chat(user, span_notice("I push the wall but nothing happens!")) + feel_turf(user) playsound(src, 'sound/blank.ogg', 25, TRUE) add_fingerprint(user) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 1e0e163304..b36394a6ee 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -1,6 +1,9 @@ /turf icon = 'icons/turf/floors.dmi' level = 1 + ///what /mob/oranges_ear instance is already assigned to us as there should only ever be one. + ///used for guaranteeing there is only one oranges_ear per turf when assigned, speeds up view() iteration + var/mob/oranges_ear/assigned_oranges_ear var/intact = 1 diff --git a/code/game/world.dm b/code/game/world.dm index 79dcd338a5..5e93170d08 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -417,6 +417,23 @@ GLOBAL_VAR(restart_counter) else hub_password = "SORRYNOPASSWORD" +/** + * Handles incresing the world's maxx var and intializing the new turfs and assigning them to the global area. + * If map_load_z_cutoff is passed in, it will only load turfs up to that z level, inclusive. + * This is because maploading will handle the turfs it loads itself. + */ + + +/world/proc/increase_max_x(new_maxx, map_load_z_cutoff = maxz) + if(new_maxx <= maxx) + return + maxx = new_maxx + +/world/proc/increase_max_y(new_maxy, map_load_z_cutoff = maxz) + if(new_maxy <= maxy) + return + maxy = new_maxy + /world/proc/incrementMaxZ() maxz++ SSmobs.MaxZChanged() diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm index 033dd3a844..a86f6ddaa3 100644 --- a/code/modules/admin/view_variables/topic_basic.dm +++ b/code/modules/admin/view_variables/topic_basic.dm @@ -69,10 +69,10 @@ lst.Insert(1, result) if(result in componentsubtypes) datumname = "component" - target.AddComponent(arglist(lst)) + target._AddComponent(arglist(lst)) else datumname = "element" - target.AddElement(arglist(lst)) + target._AddElement(arglist(lst)) log_admin("[key_name(usr)] has added [result] [datumname] to [key_name(src)].") message_admins(span_notice("[key_name_admin(usr)] has added [result] [datumname] to [key_name_admin(src)].")) if(href_list[VV_HK_CALLPROC]) diff --git a/code/modules/cargo/packsrogue/mage.dm b/code/modules/cargo/packsrogue/mage.dm index aa8e24956d..d171d442f3 100644 --- a/code/modules/cargo/packsrogue/mage.dm +++ b/code/modules/cargo/packsrogue/mage.dm @@ -12,7 +12,7 @@ /datum/supply_pack/rogue/mage/ruby name = "Ruby" cost = 150 - contains = list(/obj/item/roguegem) + contains = list(/obj/item/roguegem/ruby) /datum/supply_pack/rogue/mage/quartz name = "Quartz" diff --git a/code/modules/clothing/rogueclothes/rings.dm b/code/modules/clothing/rogueclothes/rings.dm index bd26c788bf..40c9d71764 100644 --- a/code/modules/clothing/rogueclothes/rings.dm +++ b/code/modules/clothing/rogueclothes/rings.dm @@ -84,7 +84,7 @@ . = ..() var/datum/component/magcom = GetComponent(/datum/component/anti_magic) if(magcom) - magcom.RemoveComponent() + magcom.ClearFromParent() //gold rings /obj/item/clothing/ring/emerald diff --git a/code/modules/detectivework/detective_work.dm b/code/modules/detectivework/detective_work.dm index 1d96ed3b91..9eddd1351c 100644 --- a/code/modules/detectivework/detective_work.dm +++ b/code/modules/detectivework/detective_work.dm @@ -26,15 +26,21 @@ . = D.fibers /atom/proc/add_fingerprint_list(list/fingerprints) //ASSOC LIST FINGERPRINT = FINGERPRINT + if(QDELETED(src)) + return if(length(fingerprints)) . = AddComponent(/datum/component/forensics, fingerprints) //Set ignoregloves to add prints irrespective of the mob having gloves on. /atom/proc/add_fingerprint(mob/M, ignoregloves = FALSE) + if(QDELETED(src)) + return var/datum/component/forensics/D = AddComponent(/datum/component/forensics) . = D.add_fingerprint(M, ignoregloves) /atom/proc/add_fiber_list(list/fibertext) //ASSOC LIST FIBERTEXT = FIBERTEXT + if(QDELETED(src)) + return if(length(fibertext)) . = AddComponent(/datum/component/forensics, null, null, null, fibertext) @@ -58,6 +64,8 @@ . = AddComponent(/datum/component/forensics, null, hiddenprints) /atom/proc/add_hiddenprint(mob/M) + if(QDELETED(src)) + return var/datum/component/forensics/D = AddComponent(/datum/component/forensics) . = D.add_hiddenprint(M) @@ -66,6 +74,8 @@ /obj/add_blood_DNA(list/dna) . = ..() + if(QDELETED(src)) + return if(length(dna)) . = AddComponent(/datum/component/forensics, null, null, dna) diff --git a/code/modules/mapping/map_template.dm b/code/modules/mapping/map_template.dm index ce83249f26..f091beb502 100644 --- a/code/modules/mapping/map_template.dm +++ b/code/modules/mapping/map_template.dm @@ -78,7 +78,7 @@ y, level.z_value, no_changeturf = (SSatoms.initialized == INITIALIZATION_INSSATOMS), - placeOnTop = TRUE, + place_on_top = TRUE ) var/list/bounds = parsed.bounds if(!bounds) @@ -113,7 +113,7 @@ // ruins clogging up memory for the whole round. var/datum/parsed_map/parsed = cached_map || new(file(mappath)) cached_map = keep_cached_map ? parsed : null - if(!parsed.load(T.x, T.y, T.z, cropMap=TRUE, no_changeturf=(SSatoms.initialized == INITIALIZATION_INSSATOMS), placeOnTop=TRUE)) + if(!parsed.load(T.x, T.y, T.z, crop_map =TRUE, no_changeturf=(SSatoms.initialized == INITIALIZATION_INSSATOMS), place_on_top = TRUE)) return var/list/bounds = parsed.bounds if(!bounds) diff --git a/code/modules/mapping/mapping_helpers.dm b/code/modules/mapping/mapping_helpers.dm index 5d07376c27..1d55ec6c23 100644 --- a/code/modules/mapping/mapping_helpers.dm +++ b/code/modules/mapping/mapping_helpers.dm @@ -99,7 +99,7 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava) if(target_type && !istype(A,target_type)) continue var/cargs = build_args() - A.AddComponent(arglist(cargs)) + A._AddComponent(arglist(cargs)) qdel(src) return @@ -137,3 +137,78 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava) /obj/effect/landmark/map_load_mark/Initialize() . = ..() LAZYADD(SSmapping.map_load_marks,src) + +/obj/effect/mapping_helpers/access + name = "access helper parent" + layer = DOOR_HELPER_LAYER + late = TRUE + +/obj/effect/mapping_helpers/access/LateInitialize() + var/static/list/valid = list( + /obj/structure/mineral_door, \ + /obj/structure/closet, \ + /obj/structure/roguemachine/vendor, \ + ) + + // Get the first thing we find starting with doors and closets + for(var/thing as anything in valid) + var/obj/found = locate(thing) in loc + if(found) + payload(found) + qdel(src) + return + + log_mapping("[src] failed to find a target at [AREACOORD(src)]") + qdel(src) + +/obj/effect/mapping_helpers/access/proc/payload(obj/payload) + return + +/obj/effect/mapping_helpers/access/locker + name = "access lock helper" + icon_state = "door_locker" + +/obj/effect/mapping_helpers/access/locker/payload(obj/payload) + if(istype(payload, /obj/structure/mineral_door)) + var/obj/structure/mineral_door/door = payload + door.lock_toggle() + else if(istype(payload, /obj/structure/closet)) + var/obj/structure/closet/closet = payload + closet.locked = TRUE + else if(istype(payload, /obj/structure/roguemachine/vendor)) + var/obj/structure/roguemachine/vendor/vendor = payload + vendor.locked = TRUE + +/obj/effect/mapping_helpers/secret_door_creator + name = "Secret door creator: Turns the given wall into a hidden door with a random password." + icon = 'icons/effects/hidden_door.dmi' + icon_state = "hidden_door" + + var/redstone_id + + var/obj/structure/mineral_door/secret/door_type = /obj/structure/mineral_door/secret + var/override_floor = TRUE //Will only use the below as the floor tile if true. Source turf have at least 1 baseturf to use false + var/turf/open/floor_turf = /turf/open/floor/rogue/blocks + +/obj/effect/mapping_helpers/secret_door_creator/Initialize() + if(!isclosedturf(get_turf(src))) + return ..() + var/turf/closed/source_turf = get_turf(src) + var/obj/structure/mineral_door/secret/new_door = new door_type(source_turf) + + new_door.name = source_turf.name + new_door.desc = source_turf.desc + new_door.icon = source_turf.icon + new_door.icon_state = source_turf.icon_state + + if(redstone_id) + new_door.redstone_id = redstone_id + GLOB.redstone_objs += new_door + new_door.LateInitialize() + + if(override_floor || length(source_turf.baseturfs) < 1) + source_turf.ChangeTurf(floor_turf) + else + source_turf.ChangeTurf(source_turf.baseturfs[1]) + + . = ..() diff --git a/code/modules/mapping/reader.dm b/code/modules/mapping/reader.dm index dc5776de2c..db878afd7a 100644 --- a/code/modules/mapping/reader.dm +++ b/code/modules/mapping/reader.dm @@ -1,7 +1,68 @@ /////////////////////////////////////////////////////////////// //SS13 Optimized Map loader ////////////////////////////////////////////////////////////// -#define SPACE_KEY "space" +// We support two different map formats +// It is kinda possible to process them together, but if we split them up +// I can make optimization decisions more easily +/** + * DMM SPEC: + * DMM is split into two parts. First we have strings of text linked to lists of paths and their modifications (I will call this the cache) + * We call these strings "keys" and the things they point to members. Keys have a static length + * + * The second part is a list of locations matched to a string of keys. (I'll be calling this the grid) + * These are used to lookup the cache we built earlier. + * We store location lists as grid_sets. the lines represent different things depending on the spec + * + * In standard DMM (which you can treat as the base case, since it also covers weird modifications) each line + * represents an x file, and there's typically only one grid set per z level. + * The meme is you can look at a DMM formatted map and literally see what it should roughly look like + * This differs in TGM, and we can pull some performance from this + * + * Any restrictions here also apply to TGM + * + * /tg/ Restrictions: + * Paths have a specified order. First atoms in the order in which they should be loaded, then a single turf, then the area of the cell + * DMM technically supports turf stacking, but this is deprecated for all formats + */ +#define MAP_DMM "dmm" +/** + * TGM SPEC: + * TGM is a derevation of DMM, with restrictions placed on it + * to make it easier to parse and to reduce merge conflicts/ease their resolution + * + * Requirements: + * Each "statement" in a key's details ends with a new line, and wrapped in (...) + * All paths end with either a comma or occasionally a {, then a new line + * Excepting the area, who is listed last and ends with a ) to mark the end of the key + * + * {} denotes a list of variable edits applied to the path that came before the first { + * the final } is followed by a comma, and then a new line + * Variable edits have the form \tname = value;\n + * Except the last edit, which has no final ;, and just ends in a newline + * No extra padding is permitted + * Many values are supported. See parse_constant() + * Strings must be wrapped in "...", files in '...', and lists in list(...) + * Files are kinda susy, and may not actually work. buyer beware + * Lists support assoc values as expected + * These constants can be further embedded into lists + * One var edited list will be shared among all the things it is applied to + * + * There can be no padding in front of, or behind a path + * + * Therefore: + * "key" = ( + * /path, + * /other/path{ + * var = list("name" = 'filepath'); + * other_var = /path + * }, + * /turf, + * /area) + * + */ +#define MAP_TGM "tgm" +#define MAP_UNKNOWN "unknown" + /datum/grid_set var/xcrd @@ -11,9 +72,20 @@ /datum/parsed_map var/original_path + var/map_format + /// The length of a key in this file. This is promised by the standard to be static var/key_len = 0 + /// The length of a line in this file. Not promised by dmm but standard dmm uses it, so we can trust it + var/line_len = 0 + /// If we've expanded world.maxx + var/expanded_y = FALSE + /// If we've expanded world.maxy + var/expanded_x = FALSE var/list/grid_models = list() var/list/gridSets = list() + /// List of area types we've loaded AS A PART OF THIS MAP + /// We do this to allow non unique areas, so we'll only load one per map + var/list/area/loaded_areas = list() var/list/modelCache @@ -22,33 +94,92 @@ /// Offset bounds. Same as parsed_bounds until load(). var/list/bounds + ///any turf in this list is skipped inside of build_coordinate. Lazy assoc list + var/list/turf_blacklist + // raw strings used to represent regexes more accurately // '' used to avoid confusing syntax highlighting - var/static/regex/dmmRegex = new(@'"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}', "g") - var/static/regex/trimQuotesRegex = new(@'^[\s\n]+"?|"?[\s\n]+$|^"|"$', "g") - var/static/regex/trimRegex = new(@'^[\s\n]+|[\s\n]+$', "g") + var/static/regex/dmm_regex = new(@'"([a-zA-Z]+)" = (?:\(\n|\()((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}', "g") + /// Matches key formats in TMG (IE: newline after the \() + var/static/regex/matches_tgm = new(@'^"[A-z]*"[\s]*=[\s]*\([\s]*\n', "m") + /// Pulls out key value pairs for TGM + var/static/regex/var_edits_tgm = new(@'^\t([A-z]*) = (.*?);?$') + /// Pulls out model paths for DMM + var/static/regex/model_path = new(@'(\/[^\{]*?(?:\{.*?\})?)(?:,|$)', "g") + + /// If we are currently loading this map + var/loading = FALSE #ifdef TESTING var/turfsSkipped = 0 #endif -/// Shortcut function to parse a map and apply it to the world. -/// -/// - `dmm_file`: A .dmm file to load (Required). -/// - `x_offset`, `y_offset`, `z_offset`: Positions representign where to load the map (Optional). -/// - `cropMap`: When true, the map will be cropped to fit the existing world dimensions (Optional). -/// - `measureOnly`: When true, no changes will be made to the world (Optional). -/// - `no_changeturf`: When true, [turf/AfterChange] won't be called on loaded turfs -/// - `x_lower`, `x_upper`, `y_lower`, `y_upper`: Coordinates (relative to the map) to crop to (Optional). -/// - `placeOnTop`: Whether to use [turf/PlaceOnTop] rather than [turf/ChangeTurf] (Optional). -/proc/load_map(dmm_file as file, x_offset as num, y_offset as num, z_offset as num, cropMap as num, measureOnly as num, no_changeturf as num, x_lower = -INFINITY as num, x_upper = INFINITY as num, y_lower = -INFINITY as num, y_upper = INFINITY as num, placeOnTop = FALSE as num) - var/datum/parsed_map/parsed = new(dmm_file, x_lower, x_upper, y_lower, y_upper, measureOnly) - if(parsed.bounds && !measureOnly) - parsed.load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop) - return parsed +/datum/parsed_map/proc/copy() + // Avoids duped work just in case + build_cache() + var/datum/parsed_map/newfriend = new() + newfriend.original_path = original_path + newfriend.map_format = map_format + newfriend.key_len = key_len + newfriend.line_len = line_len + newfriend.grid_models = grid_models.Copy() + newfriend.gridSets = gridSets.Copy() + newfriend.modelCache = modelCache.Copy() + newfriend.parsed_bounds = parsed_bounds.Copy() + // Copy parsed bounds to reset to initial values + newfriend.bounds = parsed_bounds.Copy() + newfriend.turf_blacklist = turf_blacklist?.Copy() + return newfriend + +/** + * Helper and recommened way to load a map file + * - dmm_file: The path to the map file + * - x_offset: The x offset to load the map at + * - y_offset: The y offset to load the map at + * - z_offset: The z offset to load the map at + * - crop_map: If true, the map will be cropped to the world bounds + * - measure_only: If true, the map will not be loaded, but the bounds will be calculated + * - no_changeturf: If true, the map will not call /turf/AfterChange + * - x_lower: The minimum x coordinate to load + * - x_upper: The maximum x coordinate to load + * - y_lower: The minimum y coordinate to load + * - y_upper: The maximum y coordinate to load + * - z_lower: The minimum z coordinate to load + * - z_upper: The maximum z coordinate to load + * - place_on_top: Whether to use /turf/proc/PlaceOnTop rather than /turf/proc/ChangeTurf + * - new_z: If true, a new z level will be created for the map + */ +/proc/load_map( + dmm_file, + x_offset = 0, + y_offset = 0, + z_offset = 0, + crop_map = FALSE, + measure_only = FALSE, + no_changeturf = FALSE, + x_lower = -INFINITY, + x_upper = INFINITY, + y_lower = -INFINITY, + y_upper = INFINITY, + z_lower = -INFINITY, + z_upper = INFINITY, + place_on_top = FALSE, + new_z = FALSE, +) + if(!(dmm_file in GLOB.cached_maps)) + GLOB.cached_maps[dmm_file] = new /datum/parsed_map(dmm_file) + + var/datum/parsed_map/parsed_map = GLOB.cached_maps[dmm_file] + parsed_map = parsed_map.copy() + if(!measure_only && !isnull(parsed_map.bounds)) + parsed_map.load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + return parsed_map /// Parse a map, possibly cropping it. -/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, measureOnly=FALSE) +/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, z_lower = -INFINITY, z_upper=INFINITY, measureOnly=FALSE) + // This proc sleeps for like 6 seconds. why? + // Is it file accesses? if so, can those be done ahead of time, async to save on time here? I wonder. + // Love ya :) if(isfile(tfile)) original_path = "[tfile]" tfile = file2text(tfile) @@ -56,16 +187,30 @@ // create a new datum without loading a map return - bounds = parsed_bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) - var/stored_index = 1 + src.bounds = parsed_bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + + if(findtext(tfile, matches_tgm)) + map_format = MAP_TGM + else + map_format = MAP_DMM // Fallback + + // lists are structs don't you know :) + var/list/bounds = src.bounds + var/list/grid_models = src.grid_models + var/key_len = src.key_len + var/line_len = src.line_len + var/stored_index = 1 + var/list/regexOutput //multiz lool - while(dmmRegex.Find(tfile, stored_index)) - stored_index = dmmRegex.next + while(dmm_regex.Find(tfile, stored_index)) + stored_index = dmm_regex.next + // Datum var lookup is expensive, this isn't + regexOutput = dmm_regex.group // "aa" = (/type{vars=blah}) - if(dmmRegex.group[1]) // Model - var/key = dmmRegex.group[1] + if(regexOutput[1]) // Model + var/key = regexOutput[1] if(grid_models[key]) // Duplicate model keys are ignored in DMMs continue if(key_len != length(key)) @@ -74,85 +219,362 @@ else CRASH("Inconsistent key length in DMM") if(!measureOnly) - grid_models[key] = dmmRegex.group[2] + grid_models[key] = regexOutput[2] // (1,1,1) = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} - else if(dmmRegex.group[3]) // Coords + else if(regexOutput[3]) // Coords if(!key_len) CRASH("Coords before model definition in DMM") - var/curr_x = text2num(dmmRegex.group[3]) + var/curr_x = text2num(regexOutput[3]) if(curr_x < x_lower || curr_x > x_upper) continue + var/curr_y = text2num(regexOutput[4]) + if(curr_y < y_lower || curr_y > y_upper) + continue + + var/curr_z = text2num(regexOutput[5]) + if(curr_z < z_lower || curr_z > z_upper) + continue + var/datum/grid_set/gridSet = new gridSet.xcrd = curr_x - //position of the currently processed square - gridSet.ycrd = text2num(dmmRegex.group[4]) - gridSet.zcrd = text2num(dmmRegex.group[5]) + gridSet.ycrd = curr_y + gridSet.zcrd = curr_z - bounds[MAP_MINX] = min(bounds[MAP_MINX], CLAMP(gridSet.xcrd, x_lower, x_upper)) - bounds[MAP_MINZ] = min(bounds[MAP_MINZ], gridSet.zcrd) - bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], gridSet.zcrd) + bounds[MAP_MINX] = min(bounds[MAP_MINX], curr_x) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], curr_y) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], curr_z) - var/list/gridLines = splittext(dmmRegex.group[6], "\n") + var/list/gridLines = splittext(regexOutput[6], "\n") gridSet.gridLines = gridLines var/leadingBlanks = 0 - while(leadingBlanks < gridLines.len && gridLines[++leadingBlanks] == "") + while(leadingBlanks < length(gridLines) && gridLines[++leadingBlanks] == "") if(leadingBlanks > 1) gridLines.Cut(1, leadingBlanks) // Remove all leading blank lines. - if(!gridLines.len) // Skip it if only blank lines exist. + if(!length(gridLines)) // Skip it if only blank lines exist. continue gridSets += gridSet - if(gridLines.len && gridLines[gridLines.len] == "") - gridLines.Cut(gridLines.len) // Remove only one blank line at the end. + if(gridLines[length(gridLines)] == "") + gridLines.Cut(length(gridLines)) // Remove only one blank line at the end. + + bounds[MAP_MINY] = min(bounds[MAP_MINY], gridSet.ycrd) + gridSet.ycrd += length(gridLines) - 1 // Start at the top and work down + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], gridSet.ycrd) - bounds[MAP_MINY] = min(bounds[MAP_MINY], CLAMP(gridSet.ycrd, y_lower, y_upper)) - gridSet.ycrd += gridLines.len - 1 // Start at the top and work down - bounds[MAP_MAXY] = max(bounds[MAP_MAXY], CLAMP(gridSet.ycrd, y_lower, y_upper)) + if(!line_len) + line_len = length(gridLines[1]) - var/maxx = gridSet.xcrd - if(gridLines.len) //Not an empty map - maxx = max(maxx, gridSet.xcrd + length(gridLines[1]) / key_len - 1) + var/maxx = curr_x + if(length(gridLines)) //Not an empty map + maxx = max(maxx, curr_x + line_len / key_len - 1) - bounds[MAP_MAXX] = CLAMP(max(bounds[MAP_MAXX], maxx), x_lower, x_upper) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], maxx) CHECK_TICK // Indicate failure to parse any coordinates by nulling bounds if(bounds[1] == 1.#INF) - bounds = null - parsed_bounds = bounds + src.bounds = null + else + // Clamp all our mins and maxes down to the proscribed limits + bounds[MAP_MINX] = clamp(bounds[MAP_MINX], x_lower, x_upper) + bounds[MAP_MAXX] = clamp(bounds[MAP_MAXX], x_lower, x_upper) + bounds[MAP_MINY] = clamp(bounds[MAP_MINY], y_lower, y_upper) + bounds[MAP_MAXY] = clamp(bounds[MAP_MAXY], y_lower, y_upper) + bounds[MAP_MINZ] = clamp(bounds[MAP_MINZ], z_lower, z_upper) + bounds[MAP_MAXZ] = clamp(bounds[MAP_MAXZ], z_lower, z_upper) + + parsed_bounds = src.bounds + src.key_len = key_len + src.line_len = line_len + +/// Iterates over all grid sets and returns ones with z values within the given bounds. Inclusive +/datum/parsed_map/proc/filter_grid_sets_based_on_z_bounds(lower_z, upper_z) + var/list/filtered_sets = list() + for(var/datum/grid_set/grid_set as anything in gridSets) + if(grid_set.zcrd < lower_z) + continue + if(grid_set.zcrd > upper_z) + continue + filtered_sets += grid_set + return filtered_sets -/// Load the parsed map into the world. See [/proc/load_map] for arguments. -/datum/parsed_map/proc/load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop) +/// Load the parsed map into the world. You probably want [/proc/load_map]. Keep the signature the same. +/datum/parsed_map/proc/load(x_offset = 0, y_offset = 0, z_offset = 0, crop_map = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, z_lower = -INFINITY, z_upper = INFINITY, place_on_top = FALSE, new_z = FALSE) //How I wish for RAII Master.StartLoadingMap() - . = _load_impl(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop) + . = _load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) Master.StopLoadingMap() +#define MAPLOADING_CHECK_TICK \ + if(TICK_CHECK) { \ + if(loading) { \ + SSatoms.map_loader_stop(REF(src)); \ + stoplag(); \ + SSatoms.map_loader_begin(REF(src)); \ + } else { \ + stoplag(); \ + } \ + } + // Do not call except via load() above. -/datum/parsed_map/proc/_load_impl(x_offset = 1, y_offset = 1, z_offset = world.maxz + 1, cropMap = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, placeOnTop = FALSE) - var/list/areaCache = list() +/datum/parsed_map/proc/_load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + PRIVATE_PROC(TRUE) + // Tell ss atoms that we're doing maploading + // We'll have to account for this in the following tick_checks so it doesn't overflow + loading = TRUE + SSatoms.map_loader_begin(REF(src)) + + // Loading used to be done in this proc + // We make the assumption that if the inner procs runtime, we WANT to do cleanup on them, but we should stil tell our parents we failed + // Since well, we did + var/sucessful = FALSE + switch(map_format) + if(MAP_TGM) + sucessful = _tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + else + sucessful = _dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + + // And we are done lads, call it off + loading = FALSE + SSatoms.map_loader_stop(REF(src)) + loading = FALSE + + if(!no_changeturf) + var/list/turfs = block( + locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), + locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ])) + for(var/turf/T as anything in turfs) + //we do this after we load everything in. if we don't, we'll have weird atmos bugs regarding atmos adjacent turfs + T.AfterChange(CHANGETURF_IGNORE_AIR) + + //if(new_z) + // for(var/z_index in bounds[MAP_MINZ] to bounds[MAP_MAXZ]) + // SSmapping.build_area_turfs(z_index) + + if(expanded_x || expanded_y) + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, expanded_x, expanded_y) + + #ifdef TESTING + if(turfsSkipped) + testing("Skipped loading [turfsSkipped] default turfs") + #endif + + return sucessful + +// Wanna clear something up about maps, talking in 255x255 here +// In the tgm format, each gridset contains 255 lines, each line representing one tile, with 255 total gridsets +// In the dmm format, each gridset contains 255 lines, each line representing one row of tiles, containing 255 * line length characters, with one gridset per z +// You can think of dmm as storing maps in rows, whereas tgm stores them in columns +/datum/parsed_map/proc/_tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + // setup + var/list/modelCache = build_cache(no_changeturf) + var/space_key = modelCache[SPACE_KEY] + var/list/bounds + src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) + + // Building y coordinate ranges + var/y_relative_to_absolute = y_offset - 1 + var/x_relative_to_absolute = x_offset - 1 + + // Ok so like. something important + // We talk in "relative" coords here, so the coordinate system of the map datum + // This is so we can do offsets, but it is NOT the same as positions in game + // That's why there's some uses of - y_relative_to_absolute here, to turn absolute positions into relative ones + // TGM maps process in columns, so the starting y will always be the max size + // We know y starts at 1 + var/datum/grid_set/first_column = gridSets[1] + var/relative_y = first_column.ycrd + var/highest_y = relative_y + y_relative_to_absolute + + if(!crop_map && highest_y > world.maxy) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_y(highest_y, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_y(highest_y) + expanded_y = TRUE + + // Skip Y coords that are above the smallest of the three params + // So maxy and y_upper get to act as thresholds, and relative_y can play + var/y_skip_above = min(world.maxy - y_relative_to_absolute, y_upper, relative_y) + // How many lines to skip because they'd be above the y cuttoff line + var/y_starting_skip = relative_y - y_skip_above + highest_y -= y_starting_skip + + // Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go + var/line_count = length(first_column.gridLines) + var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start + var/y_ending_skip = max(max(y_lower, 1 - y_relative_to_absolute) - lowest_y, 0) + + // X setup + var/x_delta_with = x_upper + if(crop_map) + // Take our smaller crop threshold yes? + x_delta_with = min(x_delta_with, world.maxx) + + // We're gonna skip all the entries above the upper x, or maxx if cropMap is set + // The last column is guarenteed to have the highest x value we;ll encounter + // Even if z scales, this still works + var/datum/grid_set/last_column = gridSets[length(gridSets)] + var/final_x = last_column.xcrd + x_relative_to_absolute + + if(final_x > x_delta_with) + // If our relative x is greater then X upper, well then we've gotta limit our expansion + var/delta = max(final_x - x_delta_with, 0) + final_x -= delta + if(final_x > world.maxx && !crop_map) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_x(final_x) + expanded_x = TRUE + + var/lowest_x = max(x_lower, 1 - x_relative_to_absolute) + + // Amount we offset the grid zcrd to get the true zcrd + var/grid_z_offset = z_offset - 1 + var/z_upper_set = z_upper < INFINITY + var/z_lower_set = z_lower > -INFINITY + + // We make the assumption that the last block of turfs will have the highest embedded z in it + // true max zcrd + var/map_bounds_z_max = last_column.zcrd + var/z_upper_parsed = map_bounds_z_max + z_offset - 1 + if(z_upper_set) + z_upper_parsed -= map_bounds_z_max - z_upper + if(z_lower_set) + var/offset_amount = z_lower - 1 + z_upper_parsed -= offset_amount + grid_z_offset -= offset_amount + + var/list/target_grid_sets = gridSets + if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want + target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper) + + var/z_threshold = world.maxz + if(z_upper_parsed > z_threshold && crop_map) + for(var/i in z_threshold + 1 to z_upper_parsed) //create a new z_level if needed + world.incrementMaxZ() + if(!no_changeturf) + WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called") + + for(var/datum/grid_set/gset as anything in target_grid_sets) + var/true_xcrd = gset.xcrd + x_relative_to_absolute + + // any cutoff of x means we just shouldn't iterate this gridset + if(final_x < true_xcrd || lowest_x > gset.xcrd) + continue + + var/zcrd = gset.zcrd + grid_z_offset + // If we're using changeturf, we disable it if we load into a z level we JUST created + var/no_afterchange = no_changeturf || zcrd > z_threshold + + // We're gonna track the first and last pairs of coords we find + // Since x is always incremented in steps of 1, we only need to deal in y + // The first x is guarenteed to be the lowest, the first y the highest, and vis versa + // This is faster then doing mins and maxes inside the hot loop below + var/first_found = FALSE + var/first_y = 0 + var/last_y = 0 + + var/ycrd = highest_y + // Everything following this line is VERY hot. + for(var/i in 1 + y_starting_skip to line_count - y_ending_skip) + if(gset.gridLines[i] == space_key && no_afterchange) + #ifdef TESTING + ++turfsSkipped + #endif + ycrd-- + MAPLOADING_CHECK_TICK + continue + + var/list/cache = modelCache[gset.gridLines[i]] + if(!cache) + SSatoms.map_loader_stop(REF(src)) + CRASH("Undefined model key in DMM: [gset.gridLines[i]]") + build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z) + + // only bother with bounds that actually exist + if(!first_found) + first_found = TRUE + first_y = ycrd + last_y = ycrd + ycrd-- + MAPLOADING_CHECK_TICK + + // The x coord never changes, so not tracking first x is safe + // If no ycrd is found, we assume this row is totally empty and just continue on + if(first_found) + bounds[MAP_MINX] = min(bounds[MAP_MINX], true_xcrd) + bounds[MAP_MINY] = min(bounds[MAP_MINY], last_y) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], true_xcrd) + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], first_y) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) + return TRUE + +/// Stanrdard loading, not used in production +/// Doesn't take advantage of any tgm optimizations, which makes it slower but also more general +/// Use this if for some reason your map format is messy +/datum/parsed_map/proc/_dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z) + // setup var/list/modelCache = build_cache(no_changeturf) var/space_key = modelCache[SPACE_KEY] var/list/bounds + var/key_len = src.key_len src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) - for(var/I in gridSets) - var/datum/grid_set/gset = I - var/ycrd = gset.ycrd + y_offset - 1 - var/zcrd = gset.zcrd + z_offset - 1 - if(!cropMap && ycrd > world.maxy) - world.maxy = ycrd // Expand Y here. X is expanded in the loop below + var/y_relative_to_absolute = y_offset - 1 + var/x_relative_to_absolute = x_offset - 1 + var/line_len = src.line_len + + // Amount we offset the grid zcrd to get the true zcrd + var/grid_z_offset = z_offset - 1 + var/z_upper_set = z_upper < INFINITY + var/z_lower_set = z_lower > -INFINITY + + // we now need to find the maximum z, fun! + var/map_bounds_z_max = 1 + for(var/datum/grid_set/grid_set as anything in gridSets) + map_bounds_z_max = max(map_bounds_z_max, grid_set.zcrd) + + var/z_upper_parsed = map_bounds_z_max + z_offset - 1 + if(z_upper_set) + z_upper_parsed -= map_bounds_z_max - z_upper + if(z_lower_set) + var/offset_amount = z_lower - 1 + z_upper_parsed -= offset_amount + grid_z_offset -= offset_amount + + var/list/target_grid_sets = gridSets + if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want + target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper) + + for(var/datum/grid_set/gset as anything in target_grid_sets) + var/relative_x = gset.xcrd + var/relative_y = gset.ycrd + var/true_xcrd = relative_x + x_relative_to_absolute + var/ycrd = relative_y + y_relative_to_absolute + var/zcrd = gset.zcrd + grid_z_offset + if(!crop_map && ycrd > world.maxy) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_y(ycrd, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_y(ycrd) var/zexpansion = zcrd > world.maxz + var/no_afterchange = no_changeturf if(zexpansion) - if(cropMap) + if(crop_map) continue else while (zcrd > world.maxz) //create a new z_level if needed @@ -160,126 +582,301 @@ if(!no_changeturf) WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called") - for(var/line in gset.gridLines) - if((ycrd - y_offset + 1) < y_lower || (ycrd - y_offset + 1) > y_upper) //Reverse operation and check if it is out of bounds of cropping. - --ycrd - continue - if(ycrd <= world.maxy && ycrd >= 1) - var/xcrd = gset.xcrd + x_offset - 1 - for(var/tpos = 1 to length(line) - key_len + 1 step key_len) - if((xcrd - x_offset + 1) < x_lower || (xcrd - x_offset + 1) > x_upper) //Same as above. - ++xcrd - continue //X cropping. - if(xcrd > world.maxx) - if(cropMap) - break - else - world.maxx = xcrd - - if(xcrd >= 1) - var/model_key = copytext(line, tpos, tpos + key_len) - var/no_afterchange = no_changeturf || zexpansion - if(!no_afterchange || (model_key != space_key)) - var/list/cache = modelCache[model_key] - if(!cache) - CRASH("Undefined model key in DMM: [model_key]") - build_coordinate(areaCache, cache, locate(xcrd, ycrd, zcrd), no_afterchange, placeOnTop) - - // only bother with bounds that actually exist - bounds[MAP_MINX] = min(bounds[MAP_MINX], xcrd) - bounds[MAP_MINY] = min(bounds[MAP_MINY], ycrd) - bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) - bounds[MAP_MAXX] = max(bounds[MAP_MAXX], xcrd) - bounds[MAP_MAXY] = max(bounds[MAP_MAXY], ycrd) - bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) - #ifdef TESTING - else - ++turfsSkipped - #endif - CHECK_TICK + no_afterchange = TRUE + // Ok so like. something important + // We talk in "relative" coords here, so the coordinate system of the map datum + // This is so we can do offsets, but it is NOT the same as positions in game + // That's why there's some uses of - y_relative_to_absolute here, to turn absolute positions into relative ones + + // Skip Y coords that are above the smallest of the three params + // So maxy and y_upper get to act as thresholds, and relative_y can play + var/y_skip_above = min(world.maxy - y_relative_to_absolute, y_upper, relative_y) + // How many lines to skip because they'd be above the y cuttoff line + var/y_starting_skip = relative_y - y_skip_above + ycrd += y_starting_skip + + // Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go + var/line_count = length(gset.gridLines) + var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start + var/y_ending_skip = max(max(y_lower, 1 - y_relative_to_absolute) - lowest_y, 0) + + // Now we're gonna precompute the x thresholds + // We skip all the entries below the lower x, or 1 + var/starting_x_delta = max(max(x_lower, 1 - x_relative_to_absolute) - relative_x, 0) + // The x loop counts by key length, so we gotta multiply here + var/x_starting_skip = starting_x_delta * key_len + true_xcrd += starting_x_delta + + // We're gonna skip all the entries above the upper x, or maxx if cropMap is set + var/x_target = line_len - key_len + 1 + var/x_step_count = ROUND_UP(x_target / key_len) + var/final_x = relative_x + (x_step_count - 1) + var/x_delta_with = x_upper + if(crop_map) + // Take our smaller crop threshold yes? + x_delta_with = min(x_delta_with, world.maxx) + if(final_x > x_delta_with) + // If our relative x is greater then X upper, well then we've gotta limit our expansion + var/delta = max(final_x - x_delta_with, 0) + x_step_count -= delta + final_x -= delta + x_target = x_step_count * key_len + if(final_x > world.maxx && !crop_map) + if(new_z) + // Need to avoid improperly loaded area/turf_contents + world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1) + else + world.increase_max_x(final_x) + expanded_x = TRUE + + // We're gonna track the first and last pairs of coords we find + // The first x is guarenteed to be the lowest, the first y the highest, and vis versa + // This is faster then doing mins and maxes inside the hot loop below + var/first_found = FALSE + var/first_x = 0 + var/first_y = 0 + var/last_x = 0 + var/last_y = 0 + + // Everything following this line is VERY hot. How hot depends on the map format + // (Yes this does mean dmm is technically faster to parse. shut up) + for(var/i in 1 + y_starting_skip to line_count - y_ending_skip) + var/line = gset.gridLines[i] + + var/xcrd = true_xcrd + for(var/tpos in 1 + x_starting_skip to x_target step key_len) + var/model_key = copytext(line, tpos, tpos + key_len) + if(model_key == space_key && no_afterchange) + #ifdef TESTING + ++turfsSkipped + #endif + MAPLOADING_CHECK_TICK ++xcrd - --ycrd - - CHECK_TICK - - if(!no_changeturf) - for(var/t in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))) - var/turf/T = t - //we do this after we load everything in. if we don't; we'll have weird atmos bugs regarding atmos adjacent turfs - T.AfterChange(CHANGETURF_IGNORE_AIR) - - #ifdef TESTING - if(turfsSkipped) - testing("Skipped loading [turfsSkipped] default turfs") - #endif + continue + var/list/cache = modelCache[model_key] + if(!cache) + SSatoms.map_loader_stop(REF(src)) + CRASH("Undefined model key in DMM: [model_key]") + build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z) + + // only bother with bounds that actually exist + if(!first_found) + first_found = TRUE + first_x = xcrd + first_y = ycrd + last_x = xcrd + last_y = ycrd + MAPLOADING_CHECK_TICK + ++xcrd + ycrd-- + MAPLOADING_CHECK_TICK + bounds[MAP_MINX] = min(bounds[MAP_MINX], first_x) + bounds[MAP_MINY] = min(bounds[MAP_MINY], last_y) + bounds[MAP_MINZ] = min(bounds[MAP_MINZ], zcrd) + bounds[MAP_MAXX] = max(bounds[MAP_MAXX], last_x) + bounds[MAP_MAXY] = max(bounds[MAP_MAXY], first_y) + bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd) return TRUE -/datum/parsed_map/proc/build_cache(no_changeturf, bad_paths=null) +GLOBAL_LIST_EMPTY(map_model_default) +/datum/parsed_map/proc/build_cache(no_changeturf, bad_paths) + if(map_format == MAP_TGM) + return tgm_build_cache(no_changeturf, bad_paths) + return dmm_build_cache(no_changeturf, bad_paths) + +/datum/parsed_map/proc/tgm_build_cache(no_changeturf, bad_paths=null) if(modelCache && !bad_paths) return modelCache . = modelCache = list() var/list/grid_models = src.grid_models + var/set_space = FALSE + // Use where a list is needed, but where it will not be modified + // Used here to remove the cost of needing to make a new list for each fields entry when it's set manually later + var/static/list/default_list = GLOB.map_model_default // It's stupid, but it saves += list(list) + var/static/list/wrapped_default_list = list(default_list) // It's stupid, but it saves += list(list) + var/static/regex/var_edits = var_edits_tgm + + var/path_to_init = "" + // Reference to the attributes list we're currently filling, if any + var/list/current_attributes + // If we are currently editing a path or not + var/editing = FALSE for(var/model_key in grid_models) - var/model = grid_models[model_key] - var/list/members = list() //will contain all members (paths) in model (in our example : /turf/unsimulated/wall and /area/mine/explored) - var/list/members_attributes = list() //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list()) + // We're going to split models by newline + // This guarentees that each entry will be of interest to us + // Then we'll process them step by step + // Hopefully this reduces the cost from read_list that we'd otherwise have + var/list/lines = splittext(grid_models[model_key], "\n") + // Builds list of path/edits for later + // Of note: we cannot preallocate them to save time in list expansion later + // But fortunately lists allocate at least 8 entries normally anyway, and + // We are unlikely to have more then that many members + //will contain all members (paths) in model (in our example : /turf/unsimulated/wall) + var/list/members = list() + //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list()) + var/list/members_attributes = list() ///////////////////////////////////////////////////////// //Constructing members and corresponding variables lists //////////////////////////////////////////////////////// + // string representation of the path to init + for(var/line in lines) + // We do this here to avoid needing to check at each return statement + // No harm in it anyway + MAPLOADING_CHECK_TICK + + switch(line[length(line)]) + if(";") // Var edit, we'll apply it + // Var edits look like \tname = value; + // I'm gonna try capturing them with regex, since it ought to be the fastest here + // Should hand back key = value + var_edits.Find(line) + var/value = parse_constant(var_edits.group[2]) + if(istext(value)) + value = apply_text_macros(value) + current_attributes[var_edits.group[1]] = value + continue // Keep on keeping on brother + if("{") // Start of an edit, and so also the start of a path + editing = TRUE + current_attributes = list() // Init the list we'll be filling + members_attributes += list(current_attributes) + path_to_init = copytext(line, 1, -1) + if(",") // Either the end of a path, or the end of an edit + if(editing) // it was the end of a path + editing = FALSE + continue + members_attributes += wrapped_default_list // We know this is a path, and we also know it has no vv's. so we'll just set this to the default list + // Drop the last char mind + path_to_init = copytext(line, 1, -1) + if("}") // Gotta be the end of an area edit, let's check to be sure + if(editing) // it was the end of an area edit (shouldn't do those anyhow) + editing = FALSE + continue + stack_trace("ended a line on JUST a }, with no ongoing edit. What? Area shit?") + else // If we're editing, this is a var edit entry. the last one in a stack, cause god hates me. Otherwise, it's an area + if(editing) // I want inline I want inline I want inline + // Var edits look like \tname = value; + // I'm gonna try capturing them with regex, since it ought to be the fastest here + // Should hand back key = value + var_edits.Find(line) + var/value = parse_constant(var_edits.group[2]) + if(istext(value)) + value = apply_text_macros(value) + current_attributes[var_edits.group[1]] = value + continue // Keep on keeping on brother + + members_attributes += wrapped_default_list // We know this is a path, and we also know it has no vv's. so we'll just set this to the default list + path_to_init = line + + + // Alright, if we've gotten to this point, our string is a path + // Oh and we don't trim it, because we require no padding for these + // Saves like 1.5 deciseconds + var/atom_def = text2path(path_to_init) //path definition, e.g /obj/foo/bar + + if(!ispath(atom_def, /atom)) // Skip the item if the path does not exist. Fix your crap, mappers! + if(bad_paths) + // Rare case, avoid the var to save time most of the time + LAZYOR(bad_paths[copytext(line, 1, -1)], model_key) + continue + // Index is already incremented either way, just gotta set the path and all + members += atom_def + + //check and see if we can just skip this turf + //So you don't have to understand this horrid statement, we can do this if + // 1. the space_key isn't set yet + // 2. no_changeturf is set + // 3. there are exactly 2 members + // 4. with no attributes + // 5. and the members are world.turf and world.area + // Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default) + // We can skip calling this proc every time we see XXX + if(!set_space \ + && no_changeturf \ + && members_attributes.len == 2 \ + && members.len == 2 \ + && members_attributes[1] == default_list \ + && members_attributes[2] == default_list \ + && members[2] == world.area \ + && members[1] == world.turf + ) + set_space = TRUE + .[SPACE_KEY] = model_key + continue - var/index = 1 - var/old_position = 1 - var/dpos + .[model_key] = list(members, members_attributes) + return . - while(dpos != 0) - //finding next member (e.g /turf/unsimulated/wall{icon_state = "rock"} or /area/mine/explored) - dpos = find_next_delimiter_position(model, old_position, ",", "{", "}") //find next delimiter (comma here) that's not within {...} +/// Builds key caches for general formats +/// Slower then the proc above, tho it could still be optimized slightly. it's just not a priority +/// Since we don't run DMM maps, ever. +/datum/parsed_map/proc/dmm_build_cache(no_changeturf, bad_paths=null) + if(modelCache && !bad_paths) + return modelCache + . = modelCache = list() + var/list/grid_models = src.grid_models + var/set_space = FALSE + // Use where a list is needed, but where it will not be modified + // Used here to remove the cost of needing to make a new list for each fields entry when it's set manually later + var/static/list/default_list = list(GLOB.map_model_default) + for(var/model_key in grid_models) + //will contain all members (paths) in model (in our example : /turf/unsimulated/wall) + var/list/members = list() + //will contain lists filled with corresponding variables, if any (in our example : list(icon_state = "rock") and list()) + var/list/members_attributes = list() - var/full_def = trim_text(copytext(model, old_position, dpos)) //full definition, e.g : /obj/foo/bar{variables=derp} - var/variables_start = findtext(full_def, "{") - var/path_text = trim_text(copytext(full_def, 1, variables_start)) + var/model = grid_models[model_key] + ///////////////////////////////////////////////////////// + //Constructing members and corresponding variables lists + //////////////////////////////////////////////////////// + + var/model_index = 1 + while(model_path.Find(model, model_index)) + var/variables_start = 0 + var/member_string = model_path.group[1] + model_index = model_path.next + //findtext is a bit expensive, lets only do this if the last char of our string is a } (IE: we know we have vars) + //this saves about 25 miliseconds on my machine. Not a major optimization + if(member_string[length(member_string)] == "}") + variables_start = findtext(member_string, "{") + + var/path_text = trimtext(copytext(member_string, 1, variables_start)) var/atom_def = text2path(path_text) //path definition, e.g /obj/foo/bar - old_position = dpos + 1 if(!ispath(atom_def, /atom)) // Skip the item if the path does not exist. Fix your crap, mappers! if(bad_paths) LAZYOR(bad_paths[path_text], model_key) continue - members.Add(atom_def) + members += atom_def //transform the variables in text format into a list (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7)) - var/list/fields = list() - + // OF NOTE: this could be made faster by replacing readlist with a progressive regex + // I'm just too much of a bum to do it rn, especially since we mandate tgm format for any maps in repo + var/list/fields = default_list if(variables_start)//if there's any variable - full_def = copytext(full_def,variables_start+1,length(full_def))//removing the last '}' - fields = readlist(full_def, ";") - if(fields.len) - if(!trim(fields[fields.len])) - --fields.len - for(var/I in fields) - var/value = fields[I] - if(istext(value)) - fields[I] = apply_text_macros(value) + member_string = copytext(member_string, variables_start + length(member_string[variables_start]), -length(copytext_char(member_string, -1))) //removing the last '}' + fields = list(readlist(member_string, ";")) + for(var/I in fields) + var/value = fields[I] + if(istext(value)) + fields[I] = apply_text_macros(value) //then fill the members_attributes list with the corresponding variables - members_attributes.len++ - members_attributes[index++] = fields - - CHECK_TICK + members_attributes += fields + MAPLOADING_CHECK_TICK //check and see if we can just skip this turf //So you don't have to understand this horrid statement, we can do this if - // 1. no_changeturf is set - // 2. the space_key isn't set yet + // 1. the space_key isn't set yet + // 2. no_changeturf is set // 3. there are exactly 2 members // 4. with no attributes // 5. and the members are world.turf and world.area // Basically, if we find an entry like this: "XXX" = (/turf/default, /area/default) // We can skip calling this proc every time we see XXX - if(no_changeturf \ - && !(.[SPACE_KEY]) \ + if(!set_space \ + && no_changeturf \ && members.len == 2 \ && members_attributes.len == 2 \ && length(members_attributes[1]) == 0 \ @@ -290,105 +887,98 @@ .[SPACE_KEY] = model_key continue - .[model_key] = list(members, members_attributes) + return . -/datum/parsed_map/proc/build_coordinate(list/areaCache, list/model, turf/crds, no_changeturf as num, placeOnTop as num) +/datum/parsed_map/proc/build_coordinate(list/model, turf/crds, no_changeturf as num, placeOnTop as num, new_z) + // If we don't have a turf, nothing we will do next will actually acomplish anything, so just go back + // Note, this would actually drop area vvs in the tile, but like, why tho + if(!crds) + return var/index var/list/members = model[1] var/list/members_attributes = model[2] + // We use static lists here because it's cheaper then passing them around + var/static/list/default_list = GLOB.map_model_default //////////////// //Instanciation //////////////// + if(turf_blacklist?[crds]) + return + //The next part of the code assumes there's ALWAYS an /area AND a /turf on a given tile //first instance the /area and remove it from the members list - index = length(members) - if(members[index] != /area/template_noop) - var/atype = members[index] - world.preloader_setup(members_attributes[index], atype)//preloader for assigning set variables on atom creation - var/atom/instance = areaCache[atype] - if (!instance) - instance = GLOB.areas_by_type[atype] - if (!instance) - instance = new atype(null) - areaCache[atype] = instance - if(crds) - instance.contents.Add(crds) - - if(GLOB.use_preloader && instance) - world.preloader_load(instance) - - //then instance the /turf and, if multiple tiles are presents, simulates the DMM underlays piling effect + index = members.len + if(members[index] != /area/template_noop) + if(members_attributes[index] != default_list) + world.preloader_setup(members_attributes[index], members[index])//preloader for assigning set variables on atom creation + var/area/area_instance = loaded_areas[members[index]] + if(!area_instance) + var/area_type = members[index] + // If this parsed map doesn't have that area already, we check the global cache + area_instance = GLOB.areas_by_type[area_type] + // If the global list DOESN'T have this area it's either not a unique area, or it just hasn't been created yet + if (!area_instance) + area_instance = new area_type(null) + if(!area_instance) + CRASH("[area_type] failed to be new'd, what'd you do?") + loaded_areas[area_type] = area_instance + + //if(!new_z) + // old_area = crds.loc + // LISTASSERTLEN(old_area.turfs_to_uncontain_by_zlevel, crds.z, list()) + // LISTASSERTLEN(area_instance.turfs_by_zlevel, crds.z, list()) + // old_area.turfs_to_uncontain_by_zlevel[crds.z] += crds + // area_instance.turfs_by_zlevel[crds.z] += crds + area_instance.contents.Add(crds) + + if(GLOB.use_preloader) + world.preloader_load(area_instance) + + // Index right before /area is /turf + index-- + var/atom/instance + //then instance the /turf + //NOTE: this used to place any turfs before the last "underneath" it using .appearance and underlays + //We don't actually use this, and all it did was cost cpu, so we don't do this anymore + if(members[index] != /turf/template_noop) + if(members_attributes[index] != default_list) + world.preloader_setup(members_attributes[index], members[index]) + + // Note: we make the assertion that the last path WILL be a turf. if it isn't, this will fail. + if(placeOnTop) + instance = crds.PlaceOnTop(null, members[index], CHANGETURF_DEFER_CHANGE | (no_changeturf ? CHANGETURF_SKIP : NONE)) + else if(no_changeturf) + instance = create_atom(members[index], crds)//first preloader pass + else + instance = crds.ChangeTurf(members[index], null, CHANGETURF_DEFER_CHANGE) - var/first_turf_index = 1 - while(!ispath(members[first_turf_index], /turf)) //find first /turf object in members - first_turf_index++ + if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New() + world.preloader_load(instance) + MAPLOADING_CHECK_TICK - //turn off base new Initialization until the whole thing is loaded - SSatoms.map_loader_begin() - //instanciate the first /turf - var/turf/T - if(members[first_turf_index] != /turf/template_noop) - T = instance_atom(members[first_turf_index],members_attributes[first_turf_index],crds,no_changeturf,placeOnTop) + //finally instance all remainings objects/mobs + for(var/atom_index in 1 to index-1) + if(members_attributes[atom_index] != default_list) + world.preloader_setup(members_attributes[atom_index], members[atom_index]) - if(T) - //if others /turf are presents, simulates the underlays piling effect - index = first_turf_index + 1 - while(index <= members.len - 1) // Last item is an /area - var/underlay = T.appearance - T = instance_atom(members[index],members_attributes[index],crds,no_changeturf,placeOnTop)//instance new turf - T.underlays += underlay - index++ + // We make the assertion that only /atom s will be in this portion of the code. if that isn't true, this will fail + instance = create_atom(members[atom_index], crds)//first preloader pass - //finally instance all remainings objects/mobs - for(index in 1 to first_turf_index-1) - instance_atom(members[index],members_attributes[index],crds,no_changeturf,placeOnTop) - //Restore initialization to the previous value - SSatoms.map_loader_stop() + if(GLOB.use_preloader && instance)//second preloader pass, for those atoms that don't ..() in New() + world.preloader_load(instance) + MAPLOADING_CHECK_TICK //////////////// //Helpers procs //////////////// -//Instance an atom at (x,y,z) and gives it the variables in attributes -/datum/parsed_map/proc/instance_atom(path,list/attributes, turf/crds, no_changeturf, placeOnTop) - world.preloader_setup(attributes, path) - - if(crds) - if(ispath(path, /turf)) - if(placeOnTop) - . = crds.PlaceOnTop(null, path, CHANGETURF_DEFER_CHANGE | (no_changeturf ? CHANGETURF_SKIP : NONE)) - else if(!no_changeturf) - . = crds.ChangeTurf(path, null, CHANGETURF_DEFER_CHANGE) - else - . = create_atom(path, crds)//first preloader pass - else - . = create_atom(path, crds)//first preloader pass - - if(GLOB.use_preloader && .)//second preloader pass, for those atoms that don't ..() in New() - world.preloader_load(.) - - //custom CHECK_TICK here because we don't want things created while we're sleeping to not initialize - if(TICK_CHECK) - SSatoms.map_loader_stop() - stoplag() - SSatoms.map_loader_begin() - /datum/parsed_map/proc/create_atom(path, crds) set waitfor = FALSE . = new path (crds) -//text trimming (both directions) helper proc -//optionally removes quotes before and after the text (for variable name) -/datum/parsed_map/proc/trim_text(what as text,trim_quotes=0) - if(trim_quotes) - return trimQuotesRegex.Replace(what, "") - else - return trimRegex.Replace(what, "") - - //find the position of the next delimiter,skipping whatever is comprised between opening_escape and closing_escape //returns 0 if reached the last delimiter /datum/parsed_map/proc/find_next_delimiter_position(text as text,initial_position as num, delimiter=",",opening_escape="\"",closing_escape="\"") @@ -403,7 +993,6 @@ return next_delimiter - //build a list from variables in text form (e.g {var1="derp"; var2; var3=7} => list(var1="derp", var2, var3=7)) //return the filled list /datum/parsed_map/proc/readlist(text as text, delimiter=",") @@ -413,25 +1002,25 @@ var/position var/old_position = 1 - while(position != 0) // find next delimiter that is not within "..." position = find_next_delimiter_position(text,old_position,delimiter) // check if this is a simple variable (as in list(var1, var2)) or an associative one (as in list(var1="foo",var2=7)) var/equal_position = findtext(text,"=",old_position, position) - - var/trim_left = trim_text(copytext(text,old_position,(equal_position ? equal_position : position))) - var/left_constant = delimiter == ";" ? trim_left : parse_constant(trim_left) - old_position = position + 1 + var/trim_left = trimtext(copytext(text,old_position,(equal_position ? equal_position : position))) + var/left_constant = parse_constant(trim_left) + if(position) + old_position = position + length(text[position]) + if(!left_constant) // damn newlines man. Exists to provide behavior consistency with the above loop. not a major cost becuase this path is cold + continue if(equal_position && !isnum(left_constant)) // Associative var, so do the association. // Note that numbers cannot be keys - the RHS is dropped if so. - var/trim_right = trim_text(copytext(text,equal_position+1,position)) + var/trim_right = trimtext(copytext(text, equal_position + length(text[equal_position]), position)) var/right_constant = parse_constant(trim_right) .[left_constant] = right_constant - else // simple var . += list(left_constant) @@ -442,12 +1031,15 @@ return num // string - if(findtext(text,"\"",1,2)) - return copytext(text,2,findtext(text,"\"",3,0)) + if(text[1] == "\"") + // insert implied locate \" and length("\"") here + // It's a minimal timesave but it is a timesave + // Safe becuase we're guarenteed trimmed constants + return copytext(text, 2, -1) // list - if(copytext(text,1,6) == "list(") - return readlist(copytext(text,6,length(text))) + if(copytext(text, 1, 6) == "list(")//6 == length("list(") + 1 + return readlist(copytext(text, 6, -1)) // typepath var/path = text2path(text) @@ -455,8 +1047,8 @@ return path // file - if(copytext(text,1,2) == "'") - return file(copytext(text,2,length(text))) + if(text[1] == "'") + return file(copytext_char(text, 2, -1)) // null if(text == "null") @@ -470,5 +1062,16 @@ return text /datum/parsed_map/Destroy() - ..() - return QDEL_HINT_HARDDEL_NOW + SSatoms.map_loader_stop(REF(src)) // Just in case, I don't want to double up here + if(turf_blacklist) + turf_blacklist.Cut() + parsed_bounds.Cut() + bounds.Cut() + grid_models.Cut() + gridSets.Cut() + return ..() + +#undef MAP_DMM +#undef MAP_TGM +#undef MAP_UNKNOWN +#undef MAPLOADING_CHECK_TICK diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm index eae9dd2236..c87e230143 100644 --- a/code/modules/mob/living/simple_animal/hostile/hostile.dm +++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm @@ -370,10 +370,13 @@ /mob/living/simple_animal/hostile/proc/AttackingTarget() + if(SEND_SIGNAL(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, target) & COMPONENT_HOSTILE_NO_PREATTACK) + return FALSE //but more importantly return before attack_animal called SEND_SIGNAL(src, COMSIG_HOSTILE_ATTACKINGTARGET, target) in_melee = TRUE - return target.attack_animal(src) + if(!QDELETED(target)) + return target.attack_animal(src) /mob/living/simple_animal/hostile/proc/Aggro() vision_range = aggro_vision_range diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 9e443e66d5..5385090d4d 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -293,7 +293,8 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) SEND_SIGNAL(src, COMSIG_MOB_STATCHANGE, DEAD) return if(footstep_type) - AddComponent(/datum/component/footstep, footstep_type) + if(!QDELING(src)) + AddComponent(/datum/component/footstep, footstep_type) /mob/living/simple_animal/handle_status_effects() ..() diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 04cd1dfb25..029be1b100 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -88,6 +88,7 @@ SEND_SIGNAL(src, COMSIG_MOB_LOGIN) log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP) + enable_client_mobs_in_contents(client) SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client) /** diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index afce43ecdf..a0dbc989cd 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -15,5 +15,5 @@ for(var/foo in client.player_details.post_logout_callbacks) var/datum/callback/CB = foo CB.Invoke() - + clear_important_client_contents(client) return TRUE diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 3b18e03d96..73f13b1774 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -84,6 +84,7 @@ GLOBAL_VAR_INIT(mobids, 1) set_nutrition(rand(NUTRITION_LEVEL_START_MIN, NUTRITION_LEVEL_START_MAX)) set_hydration(rand(HYDRATION_LEVEL_START_MIN, HYDRATION_LEVEL_START_MAX)) . = ..() + become_hearing_sensitive() update_config_movespeed() update_movespeed(TRUE) diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 88bd113582..0691319a64 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -11,7 +11,6 @@ density = TRUE layer = MOB_LAYER animate_movement = SLIDE_STEPS - flags_1 = HEAR_1 hud_possible = list(ANTAG_HUD) mouse_drag_pointer = MOUSE_ACTIVE_POINTER throwforce = 10 diff --git a/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/valuables.dm b/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/valuables.dm index fd3cb6d176..ef8e1b92e7 100644 --- a/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/valuables.dm +++ b/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/valuables.dm @@ -83,7 +83,7 @@ name = "Ruby Ring (+1 Ruby)" category = "Valuables" req_bar = /obj/item/ingot/gold - additional_items = list(/obj/item/roguegem) + additional_items = list(/obj/item/roguegem/ruby) created_item = /obj/item/clothing/ring/ruby /datum/anvil_recipe/valuables/topazg @@ -134,7 +134,7 @@ name = "Ruby Ring (+1 Ruby)" category = "Valuables" req_bar = /obj/item/ingot/silver - additional_items = list(/obj/item/roguegem) + additional_items = list(/obj/item/roguegem/ruby) created_item = /obj/item/clothing/ring/rubys /datum/anvil_recipe/valuables/topazs @@ -169,7 +169,7 @@ name = "Terminus Est (+1 Gold Bar, +1 Steel, +1 Ruby)" category = "Valuables" req_bar = /obj/item/ingot/gold - additional_items = list(/obj/item/ingot/gold, /obj/item/ingot/steel, /obj/item/roguegem) + additional_items = list(/obj/item/ingot/gold, /obj/item/ingot/steel, /obj/item/roguegem/ruby) created_item = /obj/item/rogueweapon/sword/long/exe/cloth craftdiff = 3 i_type = "Weapons" diff --git a/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm b/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm index 9ada99f8d3..8af8cec708 100644 --- a/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm +++ b/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm @@ -581,7 +581,7 @@ name = "Flamberge" category = "Weapons" req_bar = /obj/item/ingot/blacksteel - additional_items = list(/obj/item/ingot/blacksteel, /obj/item/roguegem) + additional_items = list(/obj/item/ingot/blacksteel, /obj/item/roguegem/ruby) created_item = /obj/item/rogueweapon/sword/long/blackflamb craftdiff = 5 diff --git a/code/modules/roguetown/roguemachine/scomm.dm b/code/modules/roguetown/roguemachine/scomm.dm index 95d67aa9be..600defc17e 100644 --- a/code/modules/roguetown/roguemachine/scomm.dm +++ b/code/modules/roguetown/roguemachine/scomm.dm @@ -8,7 +8,6 @@ density = FALSE blade_dulling = DULLING_BASH max_integrity = 0 - flags_1 = HEAR_1 anchored = TRUE var/next_decree = 0 var/listening = TRUE @@ -20,6 +19,14 @@ var/spawned_rat = FALSE var/garrisonline = FALSE +/obj/structure/roguemachine/scomm/Initialize() + . = ..() + become_hearing_sensitive() + +/obj/structure/roguemachine/scomm/Destroy() + lose_hearing_sensitivity() + return ..() + /obj/structure/roguemachine/scomm/OnCrafted(dirin, mob/user) . = ..() loc = user.loc @@ -317,7 +324,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/roguemachine/scomm, 32) icon = 'icons/roguetown/items/misc.dmi' w_class = WEIGHT_CLASS_SMALL experimental_inhand = FALSE - flags_1 = HEAR_1 muteinmouth = TRUE var/listening = TRUE var/speaking = TRUE @@ -357,10 +363,12 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/roguemachine/scomm, 32) /obj/item/scomstone/Destroy() SSroguemachine.scomm_machines -= src + lose_hearing_sensitivity() return ..() /obj/item/scomstone/Initialize() . = ..() + become_hearing_sensitive() update_icon() SSroguemachine.scomm_machines += src @@ -416,7 +424,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/roguemachine/scomm, 32) icon = 'icons/roguetown/clothing/neck.dmi' w_class = WEIGHT_CLASS_SMALL experimental_inhand = FALSE - flags_1 = HEAR_1 muteinmouth = TRUE var/listening = TRUE var/speaking = TRUE @@ -440,9 +447,14 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/roguemachine/scomm, 32) /obj/item/listenstone/Initialize() . = ..() + become_hearing_sensitive() update_icon() SSroguemachine.scomm_machines += src//dont know what this is for +/obj/item/listenstone/Destroy() + lose_hearing_sensitivity() + SSroguemachine.scomm_machines -= src + return ..() /obj/item/listenstone/proc/repeat_message(message, atom/A, tcolor, message_language) if(A == src) @@ -552,7 +564,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/roguemachine/scomm, 32) grid_width = 32 grid_height = 32 - /obj/item/listeningdevice/attack_self(mob/living/user) var/turf/step_turf = get_step(get_turf(user), user.dir) to_chat(user, span_tinynotice("I begin planting the listen-stone...")) @@ -570,10 +581,16 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/roguemachine/scomm, 32) var/listening = TRUE density = FALSE anchored = TRUE - flags_1 = HEAR_1 alpha = 0 layer = PROJECTILE_HIT_THRESHHOLD_LAYER +/obj/structure/listeningdeviceactive/Initialize() + . = ..() + become_hearing_sensitive() + +/obj/structure/listeningdeviceactive/Destroy() + lose_hearing_sensitivity() + return ..() /obj/structure/listeningdeviceactive/attack_right(mob/user) to_chat(user, span_info("I begin dismounting the listen-stone...")) diff --git a/code/modules/roguetown/roguemachine/titan.dm b/code/modules/roguetown/roguemachine/titan.dm index 28d8ae7bd1..d05a4cce8f 100644 --- a/code/modules/roguetown/roguemachine/titan.dm +++ b/code/modules/roguetown/roguemachine/titan.dm @@ -21,11 +21,9 @@ GLOBAL_LIST_INIT(laws_of_the_land, initialize_laws_of_the_land()) blade_dulling = DULLING_BASH integrity_failure = 0.5 max_integrity = 0 - flags_1 = HEAR_1 anchored = TRUE var/mode = 0 - /obj/structure/roguemachine/titan/obj_break(damage_flag) ..() cut_overlays() @@ -34,12 +32,14 @@ GLOBAL_LIST_INIT(laws_of_the_land, initialize_laws_of_the_land()) return /obj/structure/roguemachine/titan/Destroy() + lose_hearing_sensitivity() set_light(0) - ..() + return ..() /obj/structure/roguemachine/titan/Initialize() . = ..() icon_state = null + become_hearing_sensitive() // var/mutable_appearance/eye_lights = mutable_appearance(icon, "titan-eyes") // eye_lights.plane = ABOVE_LIGHTING_PLANE //glowy eyes // eye_lights.layer = ABOVE_LIGHTING_LAYER diff --git a/code/modules/roguetown/roguestock/bounties.dm b/code/modules/roguetown/roguestock/bounties.dm index 61703ebd4a..a3ca1ca066 100644 --- a/code/modules/roguetown/roguestock/bounties.dm +++ b/code/modules/roguetown/roguestock/bounties.dm @@ -31,5 +31,5 @@ return TRUE if(istype(I, /obj/item/reagent_containers/glass/cup)) return TRUE - if(istype(I, /obj/item/roguegem)) + if(istype(I, /obj/item/roguegem/ruby)) return TRUE diff --git a/icons/effects/dungeon_helper.dmi b/icons/effects/dungeon_helper.dmi new file mode 100644 index 0000000000..006d0634eb Binary files /dev/null and b/icons/effects/dungeon_helper.dmi differ diff --git a/icons/effects/hidden_door.dmi b/icons/effects/hidden_door.dmi new file mode 100644 index 0000000000..cbb22f443f Binary files /dev/null and b/icons/effects/hidden_door.dmi differ diff --git a/icons/effects/sigils.dmi b/icons/effects/sigils.dmi new file mode 100644 index 0000000000..a1ee6c84ad Binary files /dev/null and b/icons/effects/sigils.dmi differ diff --git a/icons/effects/waterfall.dmi b/icons/effects/waterfall.dmi new file mode 100644 index 0000000000..d3b25fa458 Binary files /dev/null and b/icons/effects/waterfall.dmi differ diff --git a/icons/roguetown/items/AP misc.dmi b/icons/roguetown/items/AP misc.dmi new file mode 100644 index 0000000000..c76b6f0cbc Binary files /dev/null and b/icons/roguetown/items/AP misc.dmi differ diff --git a/icons/roguetown/items/misc.dmi b/icons/roguetown/items/misc.dmi index 70af394007..107ddcafdc 100644 Binary files a/icons/roguetown/items/misc.dmi and b/icons/roguetown/items/misc.dmi differ diff --git a/icons/roguetown/misc/AP decoration.dmi b/icons/roguetown/misc/AP decoration.dmi new file mode 100644 index 0000000000..77a713d611 Binary files /dev/null and b/icons/roguetown/misc/AP decoration.dmi differ diff --git a/icons/roguetown/misc/decoration.dmi b/icons/roguetown/misc/decoration.dmi index da5f9882ed..bedbeb5446 100644 Binary files a/icons/roguetown/misc/decoration.dmi and b/icons/roguetown/misc/decoration.dmi differ diff --git a/icons/roguetown/topadd/tril/AP roguewall.dmi b/icons/roguetown/topadd/tril/AP roguewall.dmi new file mode 100644 index 0000000000..6250151ba3 Binary files /dev/null and b/icons/roguetown/topadd/tril/AP roguewall.dmi differ diff --git a/icons/roguetown/topadd/tril/roguewall.dmi b/icons/roguetown/topadd/tril/roguewall.dmi index 5b484b4e3b..6250151ba3 100644 Binary files a/icons/roguetown/topadd/tril/roguewall.dmi and b/icons/roguetown/topadd/tril/roguewall.dmi differ diff --git a/icons/turf/walls/brick_wall.dmi b/icons/turf/walls/brick_wall.dmi new file mode 100644 index 0000000000..dbe2e06f71 Binary files /dev/null and b/icons/turf/walls/brick_wall.dmi differ diff --git a/modular_azurepeak/code/datums/slapcrafting.dm b/modular_azurepeak/code/datums/slapcrafting.dm new file mode 100644 index 0000000000..ab5e79cfb1 --- /dev/null +++ b/modular_azurepeak/code/datums/slapcrafting.dm @@ -0,0 +1,202 @@ +/* ported from tgstation: https://github.com/tgstation/tgstation/blob/master/code/datums/elements/slapcrafting.dm +with edits to work for roguecode */ + +/// Slapcrafting component! +/datum/element/slapcrafting + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + var/list/slapcraft_recipes = list() + +/** + * Slapcraft element + * + * Slap it onto a item to be able to slapcraft with it + * + * args: + * * slapcraft_recipes (required) = The recipe to attempt crafting. + * Hit it with an ingredient of the recipe to attempt crafting. + * It will check the area near the user for the rest of the ingredients and tools. + * * +**/ +/datum/element/slapcrafting/Attach(datum/target, slapcraft_recipes = null) + ..() + if(!isitem(target)) + return ELEMENT_INCOMPATIBLE + + var/obj/item/target_item = target + + if((target_item.item_flags & ABSTRACT) || (target_item.item_flags & DROPDEL)) + return //Don't do anything, it just shouldn't be used in crafting. + + RegisterSignal(target, COMSIG_ATOM_ATTACKBY, PROC_REF(attempt_slapcraft)) + RegisterSignal(target, COMSIG_ATOM_EXAMINE_TAGS, PROC_REF(get_examine_info)) + RegisterSignal(target, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(get_examine_more_info)) + RegisterSignal(target, COMSIG_TOPIC, PROC_REF(topic_handler)) + + src.slapcraft_recipes = slapcraft_recipes + +/datum/element/slapcrafting/Detach(datum/source, ...) + . = ..() + UnregisterSignal(source, list(COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_EXAMINE, COMSIG_ATOM_EXAMINE_MORE)) + +/datum/element/slapcrafting/proc/attempt_slapcraft(obj/item/parent_item, obj/item/slapper, mob/user) + + if(isnull(slapcraft_recipes)) + CRASH("NULL SLAPCRAFT RECIPES?") + + var/datum/component/personal_crafting/craft_sheet = user.GetComponent(/datum/component/personal_crafting) + if(!craft_sheet) + CRASH("No craft sheet on user ??") + + var/list/valid_recipes + for(var/datum/crafting_recipe/recipe as anything in slapcraft_recipes) + // Gotta instance it to copy the list over. + recipe = new recipe() + var/list/type_ingredient_list = recipe.reqs + qdel(recipe) + if(length(type_ingredient_list) == 1) // No ingredients besides itself? We use one of the tools then + type_ingredient_list = recipe.tools + // Check the tool behaviours differently as they aren't types + for(var/behaviour in initial(recipe.tool_behaviors)) + if(slapper.tool_behaviour == behaviour) + LAZYADD(valid_recipes, recipe) + break + if(is_type_in_list(slapper, type_ingredient_list)) + LAZYADD(valid_recipes, recipe) + + if(!valid_recipes) + return + + // We might use radials so we need to split the proc chain + INVOKE_ASYNC(src, PROC_REF(slapcraft_async), parent_item, valid_recipes, user, craft_sheet) + +/datum/element/slapcrafting/proc/slapcraft_async(obj/parent_item, list/valid_recipes, mob/user, datum/component/personal_crafting/craft_sheet) + + var/list/recipe_choices = list() + + var/list/result_to_recipe = list() + + var/final_recipe = valid_recipes[1] + var/string_chosen_recipe + if(length(valid_recipes) > 1) + for(var/datum/crafting_recipe/recipe as anything in valid_recipes) + var/atom/recipe_result = initial(recipe.result) + result_to_recipe[initial(recipe_result.name)] = recipe + recipe_choices += list("[initial(recipe_result.name)]" = image(icon = initial(recipe_result.icon), icon_state = initial(recipe_result.icon_state))) + + if(!recipe_choices) + CRASH("No recipe choices despite validating in earlier proc") + + string_chosen_recipe = show_radial_menu(user, parent_item, recipe_choices, require_near = TRUE) + if(isnull(string_chosen_recipe)) + return // they closed the thing + + if(string_chosen_recipe) + final_recipe = result_to_recipe[string_chosen_recipe] + + + var/datum/crafting_recipe/actual_recipe = final_recipe + + if(istype(actual_recipe, /datum/crafting_recipe/food)) + actual_recipe = locate(final_recipe) in GLOB.cooking_recipes + else + actual_recipe = locate(final_recipe) in GLOB.crafting_recipes + + if(!actual_recipe) + CRASH("Recipe not located in cooking or crafting recipes: [final_recipe]") + + var/atom/final_result = initial(actual_recipe.result) + + to_chat(user, span_notice("You start crafting \a [initial(final_result.name)]...")) + + var/error_string = craft_sheet.construct_item(user, actual_recipe) + + if(istext(error_string)) + to_chat(user, span_warning("Crafting failed[error_string]")) + +/// Alerts any examiners to the recipe, if they wish to know more. +/datum/element/slapcrafting/proc/get_examine_info(atom/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + var/list/string_results = list() + // This list saves the recipe result names we've already used to cross-check other recipes so we don't have ', a spear, or a spear!' in the desc. + var/list/already_used_names + for(var/datum/crafting_recipe/recipe as anything in slapcraft_recipes) + // Identical name to a previous recipe's result? Skip in description. + var/atom/result = initial(recipe.result) + if(locate(initial(result.name)) in already_used_names) + continue + already_used_names += initial(result.name) + string_results += list("\a [initial(result.name)]") + + examine_list["crafting component"] = "You think [source] could be used to make [english_list(string_results)]! Examine again to look at the details..." + +/// Alerts any examiners to the details of the recipe. +/datum/element/slapcrafting/proc/get_examine_more_info(atom/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + for(var/datum/crafting_recipe/recipe as anything in slapcraft_recipes) + var/atom/result = initial(recipe.result) + examine_list += "See Recipe For [initial(result.name)]" + +/datum/element/slapcrafting/proc/topic_handler(atom/source, user, href_list) + SIGNAL_HANDLER + + if(!href_list["check_recipe"]) + return + + var/datum/crafting_recipe/cur_recipe = locate(href_list["check_recipe"]) in slapcraft_recipes + + if(isnull(cur_recipe)) + CRASH("null recipe!") + + var/atom/result = initial(cur_recipe.result) + + to_chat(user, span_notice("You could craft \a [initial(result.name)] by applying one of these items to it!")) + + // Gotta instance it to copy the lists over. + cur_recipe = new cur_recipe() + var/list/type_ingredient_list = cur_recipe.reqs + + // Final return string. + var/string_ingredient_list = "" + + // Check the ingredients of the crafting recipe. + for(var/valid_type in type_ingredient_list) + // Check if they're datums, specifically reagents. + var/datum/reagent/reagent_ingredient = valid_type + if(istype(reagent_ingredient)) + var/amount = initial(cur_recipe.reqs[reagent_ingredient]) + string_ingredient_list += "[amount] unit[amount > 1 ? "s" : ""] of [initial(reagent_ingredient.name)]\n" + + var/atom/ingredient = valid_type + var/amount = initial(cur_recipe.reqs[ingredient]) + + // If we're about to describe the ingredient that the component is based on, lower the described amount by 1 or remove it outright. + if(source.type == valid_type) + if(amount > 1) + amount-- + else + continue + string_ingredient_list += "[amount > 1 ? ("[amount]" + " of") : "a"] [initial(ingredient.name)]\n" + + // If we did find ingredients then add them onto the list. + if(length(string_ingredient_list)) + to_chat(user, span_boldnotice("Extra Ingredients:")) + to_chat(user, span_danger(span_notice(string_ingredient_list))) + + var/list/tool_list = "" + + // Paste the required tools. + for(var/valid_type in cur_recipe.tools) + var/atom/tool = valid_type + tool_list += "\a [initial(tool.name)]\n" + + for(var/string in cur_recipe.tool_behaviors) + tool_list += "\a [string]\n" + + if(length(tool_list)) + to_chat(user, span_boldnotice("Required Tools:")) + to_chat(user, span_danger(span_notice(tool_list))) + + qdel(cur_recipe) diff --git a/modular_azurepeak/code/game/objects/items/spellbooks.dm b/modular_azurepeak/code/game/objects/items/spellbooks.dm index b5f51b8ba1..8f555ff36d 100644 --- a/modular_azurepeak/code/game/objects/items/spellbooks.dm +++ b/modular_azurepeak/code/game/objects/items/spellbooks.dm @@ -316,12 +316,6 @@ decreases charge time if held opened in hand, for pure mage build + aesthetics. else return ..() -/obj/item/roguegem/amethyst - name = "amethyst" - icon_state = "amethyst" - sellprice = 18 - desc = "A deep lavender crystal, it surges with magical energy, yet it's artificial nature means it is worth little." - // Leaving this in for now for aesthetics, but they're now useless /obj/effect/roguerune name = "arcane rune" diff --git a/roguetown.dme b/roguetown.dme index be13de7313..64e540eff9 100644 --- a/roguetown.dme +++ b/roguetown.dme @@ -10,6 +10,7 @@ #include "_maps\_basemap.dm" #include "_maps\map_files\generic\CentCom.dmm" #include "_maps\templates\bog_shack_small.dm" +#include "_maps\templates\mining.dm" #include "_maps\templates\sewers.dm" #include "code\__byond_version_compat.dm" #include "code\_compile_options.dm" @@ -49,6 +50,7 @@ #include "code\__DEFINES\footsteps.dm" #include "code\__DEFINES\forensics.dm" #include "code\__DEFINES\hud.dm" +#include "code\__DEFINES\important_recursive_contents.dm" #include "code\__DEFINES\industrial_lifts.dm" #include "code\__DEFINES\interaction_flags.dm" #include "code\__DEFINES\inventory.dm" @@ -112,6 +114,7 @@ #include "code\__DEFINES\sound.dm" #include "code\__DEFINES\spaceman_dmm.dm" #include "code\__DEFINES\spans.dm" +#include "code\__DEFINES\spatial_gridmap.dm" #include "code\__DEFINES\spells.dm" #include "code\__DEFINES\stat.dm" #include "code\__DEFINES\stat_tracking.dm" @@ -139,9 +142,11 @@ #include "code\__DEFINES\customization\organ_customization.dm" #include "code\__DEFINES\customization\sprite_accessory.dm" #include "code\__DEFINES\dcs\flags.dm" +#include "code\__DEFINES\dcs\helpers.dm" #include "code\__DEFINES\dcs\signals\signals_mob.dm" #include "code\__DEFINES\dcs\signals\signals_movable.dm" #include "code\__DEFINES\dcs\signals\signals_moveloop.dm" +#include "code\__DEFINES\dcs\signals\signals_spatial_grid.dm" #include "code\__DEFINES\dcs\signals\signals_tram.dm" #include "code\__DEFINES\dcs\signals_atoms\appearance.dm" #include "code\__DEFINES\dcs\signals_atoms\lighting.dm" @@ -189,6 +194,7 @@ #include "code\__HELPERS\roundend.dm" #include "code\__HELPERS\sanitize_values.dm" #include "code\__HELPERS\shell.dm" +#include "code\__HELPERS\spatial_info.dm" #include "code\__HELPERS\spells.dm" #include "code\__HELPERS\stat_tracking.dm" #include "code\__HELPERS\text.dm" @@ -289,6 +295,7 @@ #include "code\controllers\subsystem\dbcore.dm" #include "code\controllers\subsystem\dcs.dm" #include "code\controllers\subsystem\discord.dm" +#include "code\controllers\subsystem\dungeon_generator.dm" #include "code\controllers\subsystem\economy.dm" #include "code\controllers\subsystem\events.dm" #include "code\controllers\subsystem\fire_burning.dm" @@ -328,6 +335,7 @@ #include "code\controllers\subsystem\skills.dm" #include "code\controllers\subsystem\sounds.dm" #include "code\controllers\subsystem\spacedrift.dm" +#include "code\controllers\subsystem\spatial_gridmap.dm" #include "code\controllers\subsystem\stickyban.dm" #include "code\controllers\subsystem\sun.dm" #include "code\controllers\subsystem\tgui.dm" @@ -568,12 +576,18 @@ #include "code\datums\components\storage\storage.dm" #include "code\datums\components\storage\storage_types.dm" #include "code\datums\components\storage\concrete\_concrete.dm" +#include "code\datums\dungeon_generator\dungeon_direction_helpers.dm" +#include "code\datums\dungeon_generator\dungeon_entry_exit.dm" +#include "code\datums\dungeon_generator\dungeon_templates\_base.dm" +#include "code\datums\dungeon_generator\dungeon_templates\entry\common.dm" +#include "code\datums\dungeon_generator\dungeon_templates\hallways\common.dm" +#include "code\datums\dungeon_generator\dungeon_templates\rest\common.dm" +#include "code\datums\dungeon_generator\dungeon_templates\rooms\common.dm" #include "code\datums\elements\_element.dm" #include "code\datums\elements\ai_flee_when_hurt.dm" #include "code\datums\elements\bed_tuckable.dm" #include "code\datums\elements\bsa_blocker.dm" #include "code\datums\elements\cleaning.dm" -#include "code\datums\elements\digitalcamo.dm" #include "code\datums\elements\earhealing.dm" #include "code\datums\elements\firestacker.dm" #include "code\datums\elements\noisy_movement.dm" @@ -750,6 +764,7 @@ #include "code\game\area\areas\overworld\outdoors\rivers.dm" #include "code\game\area\areas\rasurian_heartland\indoors.dm" #include "code\game\area\areas\rasurian_heartland\outdoors.dm" +#include "code\game\area\areas\ruins\tomb.dm" #include "code\game\area\areas\town\indoors.dm" #include "code\game\area\areas\town\outdoors.dm" #include "code\game\area\areas\town\church\indoors.dm" @@ -822,6 +837,7 @@ #include "code\game\objects\effects\portals.dm" #include "code\game\objects\effects\proximity.dm" #include "code\game\objects\effects\step_triggers.dm" +#include "code\game\objects\effects\waterfall_effect.dm" #include "code\game\objects\effects\decals\cleanable.dm" #include "code\game\objects\effects\decals\crayon.dm" #include "code\game\objects\effects\decals\decal.dm" @@ -831,6 +847,7 @@ #include "code\game\objects\effects\decals\cleanable\food.dm" #include "code\game\objects\effects\decals\cleanable\humans.dm" #include "code\game\objects\effects\decals\cleanable\misc.dm" +#include "code\game\objects\effects\decals\cleanable\sigil.dm" #include "code\game\objects\effects\decals\turfdecal\dirt.dm" #include "code\game\objects\effects\decals\turfdecal\markings.dm" #include "code\game\objects\effects\decals\turfdecal\tilecoloring.dm" @@ -1007,6 +1024,7 @@ #include "code\game\objects\structures\roguetown\bell.dm" #include "code\game\objects\structures\roguetown\gate.dm" #include "code\game\objects\structures\roguetown\handcart.dm" +#include "code\game\objects\structures\roguetown\hidden_doors.dm" #include "code\game\objects\structures\roguetown\loot.dm" #include "code\game\objects\structures\roguetown\musicbox.dm" #include "code\game\objects\structures\roguetown\newtree.dm" @@ -2186,6 +2204,7 @@ #include "modular_azurepeak\code\datums\item_equipped_movement_rustle.dm" #include "modular_azurepeak\code\datums\loadout.dm" #include "modular_azurepeak\code\datums\mind.dm" +#include "modular_azurepeak\code\datums\slapcrafting.dm" #include "modular_azurepeak\code\game\area\areas.dm" #include "modular_azurepeak\code\game\objects\effects\temporary_visuals\miracles.dm" #include "modular_azurepeak\code\game\objects\effects\temporary_visuals\music.dm"