diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 678486f019..1b5698ba5e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,8 +4,9 @@ # In the event that multiple org members are to be informed of changes # to the same file or dir, add them to the end under Multiple Owners -# ShadowLarkens -/code/__DEFINES/tgui.dm @ShadowLarkens -/code/controllers/subsystem/tgui.dm @ShadowLarkens -/code/modules/tgui @ShadowLarkens -/tgui @ShadowLarkens \ No newline at end of file +# ItsSelis + +/code/__DEFINES/tgui.dm @ItsSelis +/code/controllers/subsystem/tgui.dm @ItsSelis +/code/modules/tgui @ItsSelis +/tgui @ItsSelis diff --git a/code/__defines/cooldowns.dm b/code/__defines/cooldowns.dm new file mode 100644 index 0000000000..c3855d7ab3 --- /dev/null +++ b/code/__defines/cooldowns.dm @@ -0,0 +1,15 @@ +/* + * Cooldown system based on storing world.time on a variable, plus the cooldown time. + * Better performance over timer cooldowns, lower control. Same functionality. +*/ + +#define COOLDOWN_DECLARE(cd_index) var/##cd_index = 0 + +#define COOLDOWN_START(cd_source, cd_index, cd_time) (cd_source.cd_index = world.time + (cd_time)) + +//Returns true if the cooldown has run its course, false otherwise +#define COOLDOWN_FINISHED(cd_source, cd_index) (cd_source.cd_index < world.time) + +#define COOLDOWN_RESET(cd_source, cd_index) cd_source.cd_index = 0 + +#define COOLDOWN_TIMELEFT(cd_source, cd_index) (max(0, cd_source.cd_index - world.time)) \ No newline at end of file diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm index d9a5f7a847..df7a90632c 100644 --- a/code/__defines/mobs.dm +++ b/code/__defines/mobs.dm @@ -447,3 +447,7 @@ #define DEATHGASP_NO_MESSAGE "no message" #define RESIST_COOLDOWN 2 SECONDS + +#define VISIBLE_GENDER_FORCE_PLURAL 1 // Used by get_visible_gender to return PLURAL +#define VISIBLE_GENDER_FORCE_IDENTIFYING 2 // Used by get_visible_gender to return the mob's identifying gender +#define VISIBLE_GENDER_FORCE_BIOLOGICAL 3 // Used by get_visible_gender to return the mob's biological gender diff --git a/code/__defines/tgui.dm b/code/__defines/tgui.dm index c261e5ecdc..1215382ce7 100644 --- a/code/__defines/tgui.dm +++ b/code/__defines/tgui.dm @@ -5,6 +5,8 @@ /// Maximum ping timeout allowed to detect zombie windows #define TGUI_PING_TIMEOUT 4 SECONDS +/// Used for rate-limiting to prevent DoS by excessively refreshing a TGUI window +#define TGUI_REFRESH_FULL_UPDATE_COOLDOWN 5 SECONDS /// Window does not exist #define TGUI_WINDOW_CLOSED 0 diff --git a/code/datums/supplypacks/materials.dm b/code/datums/supplypacks/materials.dm index 5e181d8af8..45951c516d 100644 --- a/code/datums/supplypacks/materials.dm +++ b/code/datums/supplypacks/materials.dm @@ -66,17 +66,18 @@ /obj/fiftyspawner/tealcarpet ) -/datum/supply_pack/materials/arcade_carpet - name = "Retro carpets" +/datum/supply_pack/materials/retrocarpet + name = "Retro carpet" containertype = /obj/structure/closet/crate/grayson - containername = "Retro carpets crate" + containername = "Retro carpet crate" cost = 15 contains = list( - /obj/fiftyspawner/decocarpet, - /obj/fiftyspawner/retrocarpet + /obj/fiftyspawner/geocarpet, + /obj/fiftyspawner/retrocarpet, + /obj/fiftyspawner/retrocarpet_red ) -/datum/supply_pack/misc/linoleum +/datum/supply_pack/materials/linoleum name = "Linoleum" containertype = /obj/structure/closet/crate/grayson containername = "Linoleum crate" diff --git a/code/datums/supplypacks/supply.dm b/code/datums/supplypacks/supply.dm index eff6fb84fd..70f8386a91 100644 --- a/code/datums/supplypacks/supply.dm +++ b/code/datums/supplypacks/supply.dm @@ -9,18 +9,30 @@ /datum/supply_pack/supply/food name = "Kitchen supply crate" contains = list( - /obj/item/weapon/reagent_containers/food/condiment/flour = 6, + /obj/item/weapon/reagent_containers/food/condiment/carton/flour = 6, /obj/item/weapon/reagent_containers/food/drinks/milk = 3, /obj/item/weapon/reagent_containers/food/drinks/soymilk = 2, /obj/item/weapon/storage/fancy/egg_box = 2, /obj/item/weapon/reagent_containers/food/snacks/tofu = 4, /obj/item/weapon/reagent_containers/food/snacks/meat = 4, - /obj/item/weapon/reagent_containers/food/condiment/yeast = 3 + /obj/item/weapon/reagent_containers/food/condiment/yeast = 3, + /obj/item/weapon/reagent_containers/food/condiment/sprinkles = 1 ) cost = 10 containertype = /obj/structure/closet/crate/freezer/centauri containername = "Food crate" +/datum/supply_pack/supply/fancyfood + name = "Artisanal food delivery" + contains = list( + /obj/item/weapon/reagent_containers/food/condiment/carton/flour/rustic = 6, + /obj/item/weapon/reagent_containers/food/condiment/carton/sugar/rustic = 6 + ) + cost = 25 + containertype = /obj/structure/closet/crate/freezer/centauri + containername = "Artisanal food crate" + + /datum/supply_pack/supply/toner name = "Toner cartridges" contains = list(/obj/item/device/toner = 6) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index d56186f5b0..5ef2fd6818 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -27,7 +27,7 @@ // Overlays ///Our local copy of (non-priority) overlays without byond magic. Use procs in SSoverlays to manipulate - var/list/our_overlays + var/list/our_overlays ///Overlays that should remain on top and not normally removed when using cut_overlay functions, like c4. var/list/priority_overlays ///vis overlays managed by SSvis_overlays to automaticaly turn them like other overlays @@ -35,7 +35,7 @@ ///Our local copy of filter data so we can add/remove it var/list/filter_data - + //Detective Work, used for the duplicate data points kept in the scanners var/list/original_atom // Track if we are already had initialize() called to prevent double-initialization. @@ -681,7 +681,7 @@ if(!isnull(.)) datum_flags |= DF_VAR_EDITED return - + . = ..() /atom/proc/atom_say(message) @@ -716,7 +716,7 @@ . = ..() SEND_SIGNAL(src, COMSIG_ATOM_EXITED, AM, new_loc) -/atom/proc/get_visible_gender() +/atom/proc/get_visible_gender(mob/user, force) return gender /atom/proc/interact(mob/user) diff --git a/code/game/machinery/newscaster.dm b/code/game/machinery/newscaster.dm index 4a40ad8ed2..eab08e2e43 100644 --- a/code/game/machinery/newscaster.dm +++ b/code/game/machinery/newscaster.dm @@ -447,7 +447,7 @@ GLOBAL_LIST_BOILERPLATE(allCasters, /obj/machinery/newscaster) return TRUE if("set_new_message") - msg = sanitize(tgui_input_message(usr, "Write your Feed story", "Network Channel Handler")) + msg = sanitize(tgui_input_text(usr, "Write your Feed story", "Network Channel Handler", multiline = TRUE)) return TRUE if("set_new_title") diff --git a/code/game/objects/items/stacks/tiles/fifty_spawner_tiles.dm b/code/game/objects/items/stacks/tiles/fifty_spawner_tiles.dm index a4a2f7739b..134fb9baf9 100644 --- a/code/game/objects/items/stacks/tiles/fifty_spawner_tiles.dm +++ b/code/game/objects/items/stacks/tiles/fifty_spawner_tiles.dm @@ -28,14 +28,18 @@ name = "stack of teal carpet" type_to_spawn = /obj/item/stack/tile/carpet/teal -/obj/fiftyspawner/decocarpet - name = "stack of deco carpet" - type_to_spawn = /obj/item/stack/tile/carpet/deco +/obj/fiftyspawner/geocarpet + name = "stack of geometric carpet" + type_to_spawn = /obj/item/stack/tile/carpet/geo /obj/fiftyspawner/retrocarpet - name = "stack of retro carpet" + name = "stack of blue retro carpet" type_to_spawn = /obj/item/stack/tile/carpet/retro +/obj/fiftyspawner/retrocarpet_red + name = "stack of red retro carpet" + type_to_spawn = /obj/item/stack/tile/carpet/retro_red + /obj/fiftyspawner/floor name = "stack of floor tiles" type_to_spawn = /obj/item/stack/tile/floor diff --git a/code/game/objects/items/stacks/tiles/tile_types.dm b/code/game/objects/items/stacks/tiles/tile_types.dm index 286f368740..cabc1429f8 100644 --- a/code/game/objects/items/stacks/tiles/tile_types.dm +++ b/code/game/objects/items/stacks/tiles/tile_types.dm @@ -85,6 +85,26 @@ desc = "An easy to fit wooden floor tile. It's blue!" icon_state = "tile-sifwood" +/obj/item/stack/tile/wood/alt + name = "wood floor tile" + singular_name = "wood floor tile" + icon_state = "tile-wood_tile" + +/obj/item/stack/tile/wood/parquet + name = "parquet wood floor tile" + singular_name = "parquet wood floor tile" + icon_state = "tile-wood_parquet" + +/obj/item/stack/tile/wood/panel + name = "large wood floor tile" + singular_name = "large wood floor tile" + icon_state = "tile-wood_large" + +/obj/item/stack/tile/wood/tile + name = "tiled wood floor tile" + singular_name = "tiled wood floor tile" + icon_state = "tile-wood_tile" + /obj/item/stack/tile/wood/cyborg name = "wood floor tile synthesizer" desc = "A device that makes wood floor tiles." @@ -117,8 +137,19 @@ icon_state = "tile-tealcarpet" no_variants = FALSE -/obj/item/stack/tile/carpet/bcarpet //YW EDIT: Commented out to help with upstream merging. Get on this you fucking virgo bois. -yw //CHOMP Comment: Yawn commented out this block, but CHOMP already commented out this stuff so I just removed theirs. +/obj/item/stack/tile/carpet/geo + icon_state = "tile-carpet-deco" + desc = "A piece of carpet with a gnarly geometric design. It is the same size as a normal floor tile!" +/obj/item/stack/tile/carpet/retro + icon_state = "tile-carpet-retro" + desc = "A piece of carpet with totally wicked blue space patterns. It is the same size as a normal floor tile!" + +/obj/item/stack/tile/carpet/retro_red + icon_state = "tile-carpet-retro-red" + desc = "A piece of carpet with red-ical space patterns. It is the same size as a normal floor tile!" + +/obj/item/stack/tile/carpet/bcarpet //YW EDIT: Commented out to help with upstream merging. Get on this you fucking virgo bois. -yw //CHOMP Comment: Yawn commented out this block, but CHOMP already commented out this stuff so I just removed theirs. icon_state = "tile-carpet" /obj/item/stack/tile/carpet/blucarpet icon_state = "tile-carpet" @@ -133,10 +164,6 @@ /obj/item/stack/tile/carpet/oracarpet icon_state = "tile-carpet" */ -/obj/item/stack/tile/carpet/deco - icon_state = "tile-carpet-deco" -/obj/item/stack/tile/carpet/retro - icon_state = "tile-carpet-retro" /obj/item/stack/tile/floor name = "floor tile" diff --git a/code/game/objects/items/stacks/tiles/tile_types_ch.dm b/code/game/objects/items/stacks/tiles/tile_types_ch.dm index 06afcec73b..348423536d 100644 --- a/code/game/objects/items/stacks/tiles/tile_types_ch.dm +++ b/code/game/objects/items/stacks/tiles/tile_types_ch.dm @@ -101,4 +101,16 @@ singular_name = "orange carpet" desc = "A piece of orange carpet. It is the same size as a normal floor tile!" icon_state = "tile-carpet" - default_type = MAT_CARPET_ORANGE \ No newline at end of file + default_type = MAT_CARPET_ORANGE + +/obj/item/stack/tile/carpet/geo + icon_state = "tile-carpet-deco" + desc = "A piece of carpet with a gnarly geometric design. It is the same size as a normal floor tile!" + +/obj/item/stack/tile/carpet/retro + icon_state = "tile-carpet-retro" + desc = "A piece of carpet with totally wicked blue space patterns. It is the same size as a normal floor tile!" + +/obj/item/stack/tile/carpet/retro_red + icon_state = "tile-carpet-retro-red" + desc = "A piece of carpet with red-ical space patterns. It is the same size as a normal floor tile!" diff --git a/code/game/objects/items/surplus_voucher_ch.dm b/code/game/objects/items/surplus_voucher_ch.dm index 818bda9bbc..1b7fde05fd 100644 --- a/code/game/objects/items/surplus_voucher_ch.dm +++ b/code/game/objects/items/surplus_voucher_ch.dm @@ -161,7 +161,7 @@ /obj/item/surplus_voucher/ser/proc/spawn_item(var/turf/T) var/path = pick(prob(4);/obj/item/weapon/reagent_containers/food/drinks/milk, - prob(4);/obj/item/weapon/reagent_containers/food/condiment/flour, + prob(4);/obj/item/weapon/reagent_containers/food/condiment/carton/flour, prob(4);/obj/item/weapon/reagent_containers/food/drinks/soymilk, prob(4);/obj/item/weapon/storage/fancy/egg_box, prob(3);/obj/item/weapon/reagent_containers/food/snacks/meat, diff --git a/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm b/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm index 63a5613c88..66695f3511 100644 --- a/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm +++ b/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm @@ -3,8 +3,10 @@ req_access = list(access_kitchen) starts_with = list( - /obj/item/weapon/reagent_containers/food/condiment/flour = 7, - /obj/item/weapon/reagent_containers/food/condiment/sugar = 2, + /obj/item/weapon/reagent_containers/food/condiment/carton/flour = 6, + /obj/item/weapon/reagent_containers/food/condiment/carton/sugar = 1, + /obj/item/weapon/reagent_containers/food/condiment/carton/flour/rustic = 1, + /obj/item/weapon/reagent_containers/food/condiment/carton/sugar/rustic = 1, /obj/item/weapon/reagent_containers/food/condiment/spacespice = 2 ) diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm index 45cb599429..330c4e4046 100644 --- a/code/game/objects/structures/crates_lockers/crates.dm +++ b/code/game/objects/structures/crates_lockers/crates.dm @@ -90,7 +90,9 @@ src.set_dir(turn(src.dir, 90)) /obj/structure/closet/crate/attackby(obj/item/weapon/W as obj, mob/user as mob) - if(opened) + if(W.is_wrench() && istype(src,/obj/structure/closet/crate/bin)) + return ..() + else if(opened) if(isrobot(user)) return if(W.loc != user) // This should stop mounted modules ending up outside the module. diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 760c111466..b18feb7f88 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -3,7 +3,7 @@ icon_state = "girder" anchored = TRUE density = TRUE - plane = PLATING_PLANE + layer = TABLE_LAYER //CHOMPEdit - moved so that they render above catwalks. w_class = ITEMSIZE_HUGE var/state = 0 var/health = 200 diff --git a/code/game/turfs/flooring/flooring.dm b/code/game/turfs/flooring/flooring.dm index ec821278a8..03fe809961 100644 --- a/code/game/turfs/flooring/flooring.dm +++ b/code/game/turfs/flooring/flooring.dm @@ -332,18 +332,24 @@ var/list/flooring_types icon_base = "tealcarpet" build_type = /obj/item/stack/tile/carpet/teal -/decl/flooring/carpet/deco - name = "deco carpet" - icon_base = "decocarpet" - build_type = /obj/item/stack/tile/carpet/deco +/decl/flooring/carpet/geo + name = "geometric carpet" + icon_base = "geocarpet" + build_type = /obj/item/stack/tile/carpet/geo flags = TURF_REMOVE_CROWBAR | TURF_CAN_BURN /decl/flooring/carpet/retro - name = "retro carpet" + name = "blue retro carpet" icon_base = "retrocarpet" build_type = /obj/item/stack/tile/carpet/retro flags = TURF_REMOVE_CROWBAR | TURF_CAN_BURN +/decl/flooring/carpet/retro_red + name = "red retro carpet" + icon_base = "retrocarpet_red" + build_type = /obj/item/stack/tile/carpet/retro_red + flags = TURF_REMOVE_CROWBAR | TURF_CAN_BURN + /decl/flooring/tiling name = "floor" desc = "Scuffed from the passage of countless greyshirts." @@ -507,6 +513,31 @@ var/list/flooring_types icon_base = "sifwood" build_type = /obj/item/stack/tile/wood/sif +/decl/flooring/wood/alt + icon = 'icons/turf/flooring/wood.dmi' + icon_base = "wood" + build_type = /obj/item/stack/tile/wood/alt + +/decl/flooring/wood/alt/panel + desc = "Polished wooden panels." + icon = 'icons/turf/flooring/wood.dmi' + icon_base = "wood_panel" + has_damage_range = 2 + build_type = /obj/item/stack/tile/wood/panel + +/decl/flooring/wood/alt/parquet + desc = "Polished wooden tiles." + icon = 'icons/turf/flooring/wood.dmi' + icon_base = "wood_parquet" + build_type = /obj/item/stack/tile/wood/parquet + +/decl/flooring/wood/alt/tile + desc = "Polished wooden tiles." + icon = 'icons/turf/flooring/wood.dmi' + icon_base = "wood_tile" + has_damage_range = 2 + build_type = /obj/item/stack/tile/wood/tile + /decl/flooring/reinforced name = "reinforced floor" desc = "Heavily reinforced with steel rods." diff --git a/code/game/turfs/flooring/flooring_premade.dm b/code/game/turfs/flooring/flooring_premade.dm index 685f2ee374..a6e6d7b4b7 100644 --- a/code/game/turfs/flooring/flooring_premade.dm +++ b/code/game/turfs/flooring/flooring_premade.dm @@ -19,10 +19,10 @@ icon_state = "tealcarpet" initial_flooring = /decl/flooring/carpet/tealcarpet -/turf/simulated/floor/carpet/deco +/turf/simulated/floor/carpet/geo name = "deco carpet" icon_state = "decocarpet" - initial_flooring = /decl/flooring/carpet/deco + initial_flooring = /decl/flooring/carpet/geo /turf/simulated/floor/carpet/retro name = "retro carpet" @@ -60,6 +60,21 @@ icon_state = "oracarpet" initial_flooring = /decl/flooring/carpet/oracarpet +/turf/simulated/floor/carpet/geo + name = "geometric carpet" + icon_state = "geocarpet" + initial_flooring = /decl/flooring/carpet/geo + +/turf/simulated/floor/carpet/retro + name = "blue retro carpet" + icon_state = "retrocarpet" + initial_flooring = /decl/flooring/carpet/retro + +/turf/simulated/floor/carpet/retro_red + name = "red retro carpet" + icon_state = "retrocarpet_red" + initial_flooring = /decl/flooring/carpet/retro_red + /turf/simulated/floor/bluegrid name = "mainframe floor" icon = 'icons/turf/flooring/circuit.dmi' @@ -79,24 +94,67 @@ initial_flooring = /decl/flooring/wood /turf/simulated/floor/wood/broken - icon_state = "wood_broken0" // This gets changed when spawned. + icon_state = "wood-broken0" // This gets changed when spawned. -/turf/simulated/floor/wood/broken/Initialize() +/turf/simulated/floor/wood/broken/LateInitialize() + . = ..() break_tile() - return ..() /turf/simulated/floor/wood/sif name = "alien wooden floor" - icon = 'icons/turf/flooring/wood.dmi' icon_state = "sifwood" initial_flooring = /decl/flooring/wood/sif /turf/simulated/floor/wood/sif/broken - icon_state = "sifwood_broken0" // This gets changed when spawned. + icon_state = "sifwood-broken0" // This gets changed when spawned. -/turf/simulated/floor/wood/sif/broken/Initialize() +/turf/simulated/floor/wood/sif/broken/LateInitialize() + . = ..() + break_tile() + +/turf/simulated/floor/wood/alt + icon = 'icons/turf/flooring/wood.dmi' + initial_flooring = /decl/flooring/wood/alt + +/turf/simulated/floor/wood/alt/broken + icon_state = "wood-broken0" // This gets changed when spawned. + +/turf/simulated/floor/wood/alt/broken/LateInitialize() + . = ..() + break_tile() + +/turf/simulated/floor/wood/alt/tile + icon_state = "wood_tile" + initial_flooring = /decl/flooring/wood/alt/tile + +/turf/simulated/floor/wood/alt/tile/broken + icon_state = "wood_tile-broken0" // This gets changed when spawned. + +/turf/simulated/floor/wood/alt/tile/broken/LateInitialize() + . = ..() + break_tile() + +/turf/simulated/floor/wood/alt/panel + icon_state = "wood_panel" + initial_flooring = /decl/flooring/wood/alt/panel + +/turf/simulated/floor/wood/alt/panel/broken + icon_state = "wood_panel-broken0" // This gets changed when spawned. + +/turf/simulated/floor/wood/alt/panel/broken/LateInitialize() + . = ..() + break_tile() + +/turf/simulated/floor/wood/alt/parquet + icon_state = "wood_parquet" + initial_flooring = /decl/flooring/wood/alt/parquet + +/turf/simulated/floor/wood/alt/parquet/broken + icon_state = "wood_parquet-broken0" // This gets changed when spawned. + +/turf/simulated/floor/wood/alt/parquet/broken/LateInitialize() + . = ..() break_tile() - return ..() /turf/simulated/floor/grass name = "grass patch" diff --git a/code/game/turfs/simulated/floor_icon.dm b/code/game/turfs/simulated/floor_icon.dm index 420d8628b5..82153557a8 100644 --- a/code/game/turfs/simulated/floor_icon.dm +++ b/code/game/turfs/simulated/floor_icon.dm @@ -75,7 +75,10 @@ var/image/no_ceiling_image = null icon_state = "dmg[rand(1,4)]" else if(flooring) if(!isnull(broken) && (flooring.flags & TURF_CAN_BREAK)) - add_overlay(flooring.get_flooring_overlay("[flooring.icon_base]-broken-[broken]","broken[broken]")) + if(istype(src, /turf/simulated/floor/wood)) + add_overlay(flooring.get_flooring_overlay("[flooring.icon_base]-broken-[broken]","[flooring.icon_base]-broken[broken]")) + else + add_overlay(flooring.get_flooring_overlay("[flooring.icon_base]-broken-[broken]","broken[broken]")) if(!isnull(burnt) && (flooring.flags & TURF_CAN_BURN)) add_overlay(flooring.get_flooring_overlay("[flooring.icon_base]-burned-[burnt]","burned[burnt]")) diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 08a3a99880..bb88d9d7fb 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -218,7 +218,6 @@ var/global/floorIsLava = 0 /datum/player_info/var/content // text content of the information /datum/player_info/var/timestamp // Because this is bloody annoying -#define PLAYER_NOTES_ENTRIES_PER_PAGE 50 /datum/admins/proc/PlayerNotes() set category = "Admin" set name = "Player Notes" @@ -235,56 +234,20 @@ var/global/floorIsLava = 0 if (!istype(src,/datum/admins)) to_chat(usr, "Error: you are not an admin!") return - var/filter = input(usr, "Filter string (case-insensitive regex)", "Player notes filter") as text|null + var/filter = tgui_input_text(usr, "Filter string (case-insensitive regex)", "Player notes filter") PlayerNotesPage(1, filter) /datum/admins/proc/PlayerNotesPage(page, filter) - var/dat = "Player notes - Apply Filter
" var/savefile/S=new("data/player_notes.sav") var/list/note_keys S >> note_keys - if(!note_keys) - dat += "No notes found." - else - dat += "" + + if(note_keys) note_keys = sortList(note_keys) - if(filter) - var/list/results = list() - var/regex/needle = regex(filter, "i") - for(var/haystack in note_keys) - if(needle.Find(haystack)) - results += haystack - note_keys = results - - // Display the notes on the current page - var/number_pages = note_keys.len / PLAYER_NOTES_ENTRIES_PER_PAGE - // Emulate CEILING(why does BYOND not have ceil, 1) - if(number_pages != round(number_pages)) - number_pages = round(number_pages) + 1 - var/page_index = page - 1 - - if(page_index < 0 || page_index >= number_pages) - dat += "" - else - var/lower_bound = page_index * PLAYER_NOTES_ENTRIES_PER_PAGE + 1 - var/upper_bound = (page_index + 1) * PLAYER_NOTES_ENTRIES_PER_PAGE - upper_bound = min(upper_bound, note_keys.len) - for(var/index = lower_bound, index <= upper_bound, index++) - var/t = note_keys[index] - dat += "" - - dat += "
No keys found.
[t]

" - - // Display a footer to select different pages - for(var/index = 1, index <= number_pages, index++) - if(index == page) - dat += "" - dat += "[index] " - if(index == page) - dat += "" - - usr << browse(dat, "window=player_notes;size=400x400") + var/datum/tgui_module/player_notes/A = new(src) + A.ckeys = note_keys + A.tgui_interact(usr) /datum/admins/proc/player_has_info(var/key as text) @@ -303,44 +266,10 @@ var/global/floorIsLava = 0 if (!istype(src,/datum/admins)) to_chat(usr, "Error: you are not an admin!") return - var/dat = "Info on [key]" - dat += "" - - var/p_age = "unknown" - for(var/client/C in GLOB.clients) - if(C.ckey == key) - p_age = C.player_age - break - dat +="Player age: [p_age]
" - - var/savefile/info = new("data/player_saves/[copytext(key, 1, 2)]/[key]/info.sav") - var/list/infos - info >> infos - if(!infos) - dat += "No information found on the given key.
" - else - var/update_file = 0 - var/i = 0 - for(var/datum/player_info/I in infos) - i += 1 - if(!I.timestamp) - I.timestamp = "Pre-4/3/2012" - update_file = 1 - if(!I.rank) - I.rank = "N/A" - update_file = 1 - dat += "[I.content] by [I.author] ([I.rank]) on [I.timestamp] " - if(I.author == usr.key || I.author == "Adminbot" || ishost(usr)) - dat += "Remove" - dat += "

" - if(update_file) info << infos - - dat += "
" - dat += "Add Comment
" - - dat += "" - usr << browse(dat, "window=adminplayerinfo;size=480x480") + var/datum/tgui_module/player_notes_info/A = new(src) + A.key = key + A.tgui_interact(usr) /datum/admins/proc/access_news_network() //MARKER @@ -688,7 +617,7 @@ var/global/floorIsLava = 0 set desc="Announce your desires to the world" if(!check_rights(0)) return - var/message = tgui_input_message(usr, "Global message to send:", "Admin Announce") + var/message = tgui_input_text(usr, "Global message to send:", "Admin Announce", multiline = TRUE) if(message) if(!check_rights(R_SERVER,0)) message = sanitize(message, 500, extra = 0) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index c5f0c77e12..5cc05e5360 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -35,11 +35,24 @@ set category = "Admin" set name = "Aghost" if(!holder) return + + var/build_mode + if(src.buildmode) + build_mode = tgui_alert(src, "You appear to be currently in buildmode. Do you want to re-enter buildmode after aghosting?", "Buildmode", list("Yes", "No")) + if(build_mode != "Yes") + to_chat(src, "Will not re-enter buildmode after switch.") + if(istype(mob,/mob/observer/dead)) //re-enter var/mob/observer/dead/ghost = mob if(ghost.can_reenter_corpse) - ghost.reenter_corpse() + if(build_mode) + togglebuildmode(mob) + ghost.reenter_corpse() + if(build_mode == "Yes") + togglebuildmode(mob) + else + ghost.reenter_corpse() else to_chat(ghost, "Error: Aghost: Can't reenter corpse.") return @@ -51,9 +64,18 @@ else //ghostize var/mob/body = mob - var/mob/observer/dead/ghost = body.ghostize(1) - ghost.admin_ghosted = 1 - log_and_message_admins("[key_name(src)] admin-ghosted.") // CHOMPEdit - Add logging. + var/mob/observer/dead/ghost + if(build_mode) + togglebuildmode(body) + ghost = body.ghostize(1) + ghost.admin_ghosted = 1 + log_and_message_admins("[key_name(src)] admin-ghosted.") // CHOMPEdit - Add logging. + if(build_mode == "Yes") + togglebuildmode(ghost) + else + ghost = body.ghostize(1) + ghost.admin_ghosted = 1 + log_and_message_admins("[key_name(src)] admin-ghosted.") // CHOMPEdit - Add logging. if(body) body.teleop = ghost if(!body.key) @@ -538,4 +560,4 @@ T.spell_list += new S feedback_add_details("admin_verb","GS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! log_admin("[key_name(usr)] gave [key_name(T)] the spell [S].") - message_admins("[key_name_admin(usr)] gave [key_name(T)] the spell [S].", 1) \ No newline at end of file + message_admins("[key_name_admin(usr)] gave [key_name(T)] the spell [S].", 1) diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 37b8e67c57..e4c9ff198c 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -151,11 +151,7 @@ switch(new_rank) if(null,"") return if("*New Rank*") -<<<<<<< HEAD - new_rank = input(usr, "Please input a new rank", "New custom rank", null, null) as null|text -======= new_rank = tgui_input_text(usr, "Please input a new rank", "New custom rank") ->>>>>>> b875945450... Merge pull request #13112 from ItsSelis/tgui-input-conversions if(config.admin_legacy_system) new_rank = ckeyEx(new_rank) if(!new_rank) @@ -2023,21 +2019,6 @@ // player info stuff - if(href_list["add_player_info"]) - var/key = href_list["add_player_info"] - var/add = sanitize(input(usr, "Add Player Info") as null|text) - if(!add) return - - notes_add(key,add,usr) - show_player_info(key) - - if(href_list["remove_player_info"]) - var/key = href_list["remove_player_info"] - var/index = text2num(href_list["remove_index"]) - - notes_del(key, index) - show_player_info(key) - if(href_list["notes"]) var/ckey = href_list["ckey"] if(!ckey) @@ -2047,7 +2028,9 @@ switch(href_list["notes"]) if("show") - show_player_info(ckey) + var/datum/tgui_module/player_notes_info/A = new(src) + A.key = ckey + A.tgui_interact(usr) if("list") var/filter if(href_list["filter"] && href_list["filter"] != "0") diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index 01c6e023f4..1bf1a03bd2 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -140,7 +140,7 @@ return TOPIC_REFRESH else if(href_list["metadata"]) - var/new_metadata = sanitize(tgui_input_message(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata)), extra = 0) //VOREStation Edit + var/new_metadata = sanitize(tgui_input_text(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata), multiline = TRUE), extra = 0) //VOREStation Edit if(new_metadata && CanUseTopic(user)) pref.metadata = new_metadata return TOPIC_REFRESH diff --git a/code/modules/client/preference_setup/global/01_ui.dm b/code/modules/client/preference_setup/global/01_ui.dm index 56ea635dbd..fbe54b00cd 100644 --- a/code/modules/client/preference_setup/global/01_ui.dm +++ b/code/modules/client/preference_setup/global/01_ui.dm @@ -3,30 +3,34 @@ sort_order = 1 /datum/category_item/player_setup_item/player_global/ui/load_preferences(var/savefile/S) - S["UI_style"] >> pref.UI_style - S["UI_style_color"] >> pref.UI_style_color - S["UI_style_alpha"] >> pref.UI_style_alpha - S["ooccolor"] >> pref.ooccolor - S["tooltipstyle"] >> pref.tooltipstyle - S["client_fps"] >> pref.client_fps - S["ambience_freq"] >> pref.ambience_freq - S["ambience_chance"] >> pref.ambience_chance - S["tgui_fancy"] >> pref.tgui_fancy - S["tgui_lock"] >> pref.tgui_lock - S["tgui_input_mode"] >> pref.tgui_input_mode + S["UI_style"] >> pref.UI_style + S["UI_style_color"] >> pref.UI_style_color + S["UI_style_alpha"] >> pref.UI_style_alpha + S["ooccolor"] >> pref.ooccolor + S["tooltipstyle"] >> pref.tooltipstyle + S["client_fps"] >> pref.client_fps + S["ambience_freq"] >> pref.ambience_freq + S["ambience_chance"] >> pref.ambience_chance + S["tgui_fancy"] >> pref.tgui_fancy + S["tgui_lock"] >> pref.tgui_lock + S["tgui_input_mode"] >> pref.tgui_input_mode + S["tgui_large_buttons"] >> pref.tgui_large_buttons + S["tgui_swapped_buttons"] >> pref.tgui_swapped_buttons /datum/category_item/player_setup_item/player_global/ui/save_preferences(var/savefile/S) - S["UI_style"] << pref.UI_style - S["UI_style_color"] << pref.UI_style_color - S["UI_style_alpha"] << pref.UI_style_alpha - S["ooccolor"] << pref.ooccolor - S["tooltipstyle"] << pref.tooltipstyle - S["client_fps"] << pref.client_fps - S["ambience_freq"] << pref.ambience_freq - S["ambience_chance"] << pref.ambience_chance - S["tgui_fancy"] << pref.tgui_fancy - S["tgui_lock"] << pref.tgui_lock - S["tgui_input_mode"] << pref.tgui_input_mode + S["UI_style"] << pref.UI_style + S["UI_style_color"] << pref.UI_style_color + S["UI_style_alpha"] << pref.UI_style_alpha + S["ooccolor"] << pref.ooccolor + S["tooltipstyle"] << pref.tooltipstyle + S["client_fps"] << pref.client_fps + S["ambience_freq"] << pref.ambience_freq + S["ambience_chance"] << pref.ambience_chance + S["tgui_fancy"] << pref.tgui_fancy + S["tgui_lock"] << pref.tgui_lock + S["tgui_input_mode"] << pref.tgui_input_mode + S["tgui_large_buttons"] << pref.tgui_large_buttons + S["tgui_swapped_buttons"] << pref.tgui_swapped_buttons /datum/category_item/player_setup_item/player_global/ui/sanitize_preferences() pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style)) @@ -40,6 +44,8 @@ pref.tgui_fancy = sanitize_integer(pref.tgui_fancy, 0, 1, initial(pref.tgui_fancy)) pref.tgui_lock = sanitize_integer(pref.tgui_lock, 0, 1, initial(pref.tgui_lock)) pref.tgui_input_mode = sanitize_integer(pref.tgui_input_mode, 0, 1, initial(pref.tgui_input_mode)) + pref.tgui_large_buttons = sanitize_integer(pref.tgui_large_buttons, 0, 1, initial(pref.tgui_large_buttons)) + pref.tgui_swapped_buttons = sanitize_integer(pref.tgui_swapped_buttons, 0, 1, initial(pref.tgui_swapped_buttons)) /datum/category_item/player_setup_item/player_global/ui/content(var/mob/user) . = "UI Style: [pref.UI_style]
" @@ -52,7 +58,9 @@ . += "Ambience Chance: [pref.ambience_chance]
" . += "tgui Window Mode: [(pref.tgui_fancy) ? "Fancy (default)" : "Compatible (slower)"]
" . += "tgui Window Placement: [(pref.tgui_lock) ? "Primary Monitor" : "Free (default)"]
" - . += "Input Mode (Say, Me, Whisper, Subtle): [(pref.tgui_input_mode) ? "TGUI" : "BYOND (default)"]
" + . += "TGUI Input Framework: [(pref.tgui_input_mode) ? "Enabled" : "Disabled (default)"]
" + . += "TGUI Large Buttons: [(pref.tgui_large_buttons) ? "Enabled (default)" : "Disabled"]
" + . += "TGUI Swapped Buttons: [(pref.tgui_swapped_buttons) ? "Enabled" : "Disabled (default)"]
" if(can_select_ooc_color(user)) . += "OOC Color:" if(pref.ooccolor == initial(pref.ooccolor)) @@ -126,6 +134,14 @@ pref.tgui_input_mode = !pref.tgui_input_mode return TOPIC_REFRESH + else if(href_list["tgui_large_buttons"]) + pref.tgui_large_buttons = !pref.tgui_large_buttons + return TOPIC_REFRESH + + else if(href_list["tgui_swapped_buttons"]) + pref.tgui_swapped_buttons = !pref.tgui_swapped_buttons + return TOPIC_REFRESH + else if(href_list["reset"]) switch(href_list["reset"]) if("ui") diff --git a/code/modules/client/preference_setup/vore/02_size.dm b/code/modules/client/preference_setup/vore/02_size.dm index 05d24a861b..37a71587b1 100644 --- a/code/modules/client/preference_setup/vore/02_size.dm +++ b/code/modules/client/preference_setup/vore/02_size.dm @@ -87,7 +87,7 @@ return TOPIC_REFRESH else if(href_list["weight_gain"]) - var/weight_gain_rate = tgui_input_num(user, "Choose your character's rate of weight gain between 100% \ + var/weight_gain_rate = tgui_input_number(user, "Choose your character's rate of weight gain between 100% \ (full realism body fat gain) and 0% (no body fat gain).\n\ (If you want to disable weight gain, set this to 0.01 to round it to 0%.)\ ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_gain) @@ -96,7 +96,7 @@ return TOPIC_REFRESH else if(href_list["weight_loss"]) - var/weight_loss_rate = tgui_input_num(user, "Choose your character's rate of weight loss between 100% \ + var/weight_loss_rate = tgui_input_number(user, "Choose your character's rate of weight loss between 100% \ (full realism body fat loss) and 0% (no body fat loss).\n\ (If you want to disable weight loss, set this to 0.01 round it to 0%.)\ ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_loss) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index d401436c3d..c06d0ab35d 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -28,7 +28,9 @@ var/list/preferences_datums = list() var/tgui_fancy = TRUE var/tgui_lock = FALSE - var/tgui_input_mode = FALSE // Say, Me, Whisper, Subtle Input Mode; Disabled by default; FALSE = BYOND, TRUE = TGUI + var/tgui_input_mode = FALSE // All the Input Boxes (Text,Number,List,Alert) + var/tgui_large_buttons = TRUE + var/tgui_swapped_buttons = FALSE //character preferences var/num_languages = 0 //CHOMPEdit diff --git a/code/modules/emotes/definitions/audible.dm b/code/modules/emotes/definitions/audible.dm index 4df078aa11..d702a32d3a 100644 --- a/code/modules/emotes/definitions/audible.dm +++ b/code/modules/emotes/definitions/audible.dm @@ -205,6 +205,14 @@ emote_message_3p = "purrs." emote_sound = 'sound/voice/cat_purr_long.ogg' +/decl/emote/audible/fennecscream + key = "fennecscream" + emote_message_3p = "screeches!" + +/decl/emote/audible/zoom + key = "zoom" + emote_message_3p = "zooms." + /decl/emote/audible/teshsqueak key = "surprised" emote_message_1p = "You chirp in surprise!" diff --git a/code/modules/food/food/condiment.dm b/code/modules/food/food/condiment.dm index 5b137957db..2373db3bc2 100644 --- a/code/modules/food/food/condiment.dm +++ b/code/modules/food/food/condiment.dm @@ -135,6 +135,11 @@ desc = "Barbecue sauce, it's labeled 'sweet and spicy'." icon_state = "barbecue" center_of_mass = list("x"=16, "y"=6) + if("sprinkles") + name = "sprinkles" + desc = "Bottle of sprinkles, colourful!" + icon_state= "sprinkles" + center_of_mass = list("x"=16, "y"=6) else name = "Misc Condiment Bottle" if (reagents.reagent_list.len==1) @@ -208,6 +213,13 @@ . = ..() reagents.add_reagent("yeast", 50) +/obj/item/weapon/reagent_containers/food/condiment/sprinkles + name = "Sprinkles" + +/obj/item/weapon/reagent_containers/food/condiment/sprinkles/Initialize() + . = ..() + reagents.add_reagent("sprinkles", 50) + /obj/item/weapon/reagent_containers/food/condiment/small possible_transfer_amounts = list(1,20) amount_per_transfer_from_this = 1 @@ -448,22 +460,58 @@ //End of MRE stuff. -/obj/item/weapon/reagent_containers/food/condiment/flour - name = "flour sack" - desc = "A big bag of flour. Good for baking!" +/obj/item/weapon/reagent_containers/food/condiment/carton/flour + name = "flour carton" + desc = "A big carton of flour. Good for baking!" icon = 'icons/obj/food.dmi' icon_state = "flour" volume = 220 center_of_mass = list("x"=16, "y"=8) -/obj/item/weapon/reagent_containers/food/condiment/flour/on_reagent_change() +/obj/item/weapon/reagent_containers/food/condiment/carton/flour/on_reagent_change() + update_icon() return -/obj/item/weapon/reagent_containers/food/condiment/flour/Initialize() +/obj/item/weapon/reagent_containers/food/condiment/carton/flour/Initialize() . = ..() reagents.add_reagent("flour", 200) randpixel_xy() +/obj/item/weapon/reagent_containers/food/condiment/carton/update_icon() + overlays.Cut() + + if(reagents.total_volume) + var/image/filling = image('icons/obj/food.dmi', src, "[icon_state]10") + + filling.icon_state = "[icon_state]-[clamp(round(100 * reagents.total_volume / volume, 25), 0, 100)]" + + overlays += filling + +/obj/item/weapon/reagent_containers/food/condiment/carton/flour/rustic + name = "flour sack" + desc = "An artisanal sack of flour. Classy!" + icon_state = "flour_bag" + +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar + name = "sugar carton" + desc = "A big carton of sugar. Sweet!" + icon_state = "sugar" + volume = 120 + center_of_mass = list("x"=16, "y"=8) + +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar/on_reagent_change() + update_icon() + return + +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar/Initialize() + . = ..() + reagents.add_reagent("sugar", 100) + +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar/rustic + name = "sugar sack" + desc = "An artisanal sack of sugar. Classy!" + icon_state = "sugar_bag" + /obj/item/weapon/reagent_containers/food/condiment/spacespice name = "space spices" desc = "An exotic blend of spices for cooking. Definitely not worms." @@ -477,4 +525,4 @@ /obj/item/weapon/reagent_containers/food/condiment/spacespice/Initialize() . = ..() - reagents.add_reagent("spacespice", 40) \ No newline at end of file + reagents.add_reagent("spacespice", 40) diff --git a/code/modules/food/kitchen/cooking_machines/container.dm b/code/modules/food/kitchen/cooking_machines/container.dm index 981a809fd4..bd4a9d8e00 100644 --- a/code/modules/food/kitchen/cooking_machines/container.dm +++ b/code/modules/food/kitchen/cooking_machines/container.dm @@ -7,6 +7,7 @@ var/shortname var/max_space = 20//Maximum sum of w-classes of foods in this container at once var/max_reagents = 80//Maximum units of reagents + var/food_items = 0 // Used for icon updates flags = OPENCONTAINER | NOREACT var/list/insertable = list( /obj/item/weapon/reagent_containers/food/snacks, @@ -59,6 +60,8 @@ return I.forceMove(src) to_chat(user, "You put the [I] into the [src].") + food_items += 1 + update_icon() return /obj/item/weapon/reagent_containers/cooking_container/verb/empty() @@ -102,6 +105,8 @@ /obj/item/weapon/reagent_containers/cooking_container/AltClick(var/mob/user) do_empty(user) + food_items = 0 + update_icon() //Deletes contents of container. //Used when food is burned, before replacing it with a burned mess @@ -158,6 +163,22 @@ if (weights[I]) holder.trans_to(I, weights[I] / total) +/obj/item/weapon/reagent_containers/cooking_container/update_icon() + overlays.Cut() + + if(food_items) + var/image/filling = image('icons/obj/cooking_machines.dmi', src, "[icon_state]10") + + var/percent = round((food_items / max_space) * 100) + switch(percent) + if(0 to 2) filling.icon_state = "[icon_state]" + if(3 to 24) filling.icon_state = "[icon_state]1" + if(25 to 49) filling.icon_state = "[icon_state]2" + if(50 to 74) filling.icon_state = "[icon_state]3" + if(75 to 79) filling.icon_state = "[icon_state]4" + if(80 to INFINITY) filling.icon_state = "[icon_state]5" + + overlays += filling /obj/item/weapon/reagent_containers/cooking_container/oven name = "oven dish" @@ -186,4 +207,4 @@ name = "grill rack" shortname = "rack" desc = "Put ingredients 'in'/on this; designed for use with a grill. Warranty void if used incorrectly. Alt click to remove contents." - icon_state = "grillrack" \ No newline at end of file + icon_state = "grillrack" diff --git a/code/modules/mob/holder.dm b/code/modules/mob/holder.dm index d9d6b79663..b2a61b9c12 100644 --- a/code/modules/mob/holder.dm +++ b/code/modules/mob/holder.dm @@ -173,6 +173,12 @@ var/list/holder_mob_icon_cache = list() item_state = "cat" /obj/item/weapon/holder/cat/runtime + +/obj/item/holder/fennec + origin_tech = list(TECH_BIO = 2) + +/obj/item/holder/cat/runtime + origin_tech = list(TECH_BIO = 2, TECH_DATA = 4) /obj/item/weapon/holder/cat/cak diff --git a/code/modules/mob/living/carbon/human/emote.dm b/code/modules/mob/living/carbon/human/emote.dm index 5739f94399..bac825919a 100644 --- a/code/modules/mob/living/carbon/human/emote.dm +++ b/code/modules/mob/living/carbon/human/emote.dm @@ -143,7 +143,9 @@ var/list/_human_default_emotes = list( /decl/emote/audible/coyawoo2, /decl/emote/audible/coyawoo3, /decl/emote/audible/coyawoo4, - /decl/emote/audible/coyawoo5 + /decl/emote/audible/coyawoo5, + /decl/emote/audible/fennecscream, + /decl/emote/audible/zoom //VOREStation Add End ) @@ -273,7 +275,10 @@ var/list/_simple_mob_default_emotes = list( /decl/emote/visible/blep, /decl/emote/audible/prbt, /decl/emote/audible/gyoh, - /decl/emote/audible/rumble + /decl/emote/audible/rumble, + /decl/emote/audible/fennecscream, + /decl/emote/audible/zoom + ) //VOREStation Add End diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index 14e2f452ac..65560c651f 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -76,22 +76,12 @@ BP_L_LEG = skip_body & EXAMINE_SKIPLEGS, BP_R_LEG = skip_body & EXAMINE_SKIPLEGS) - var/datum/gender/T = gender_datums[get_visible_gender()] + var/gender_hidden = (skip_gear & EXAMINE_SKIPJUMPSUIT) && (skip_body & EXAMINE_SKIPFACE) + var/gender_key = get_visible_gender(user, gender_hidden) - if((skip_gear & EXAMINE_SKIPJUMPSUIT) && (skip_body & EXAMINE_SKIPFACE)) //big suits/masks/helmets make it hard to tell their gender - T = gender_datums[PLURAL] - - else if(species && species.ambiguous_genders) - if(ishuman(user)) - var/mob/living/carbon/human/H = user - if(H.species && !istype(species, H.species)) - T = gender_datums[PLURAL]// Species with ambiguous_genders will not show their true gender upon examine if the examiner is not also the same species. - if(!(issilicon(user) || isobserver(user))) // Ghosts and borgs are all knowing - T = gender_datums[PLURAL] - - if(!T) - // Just in case someone VVs the gender to something strange. It'll runtime anyway when it hits usages, better to CRASH() now with a helpful message. - CRASH("Gender datum was null; key was '[((skip_gear & EXAMINE_SKIPJUMPSUIT) && (skip_body & EXAMINE_SKIPFACE)) ? PLURAL : gender]'") + var/datum/gender/T = gender_datums[gender_key] + if (!T) + CRASH({"Null gender datum on examine: mob="[src]",hidden="[gender_hidden]",key="[gender_key]",bio="[gender]",id="[identifying_gender]""}) var/name_ender = "" if(!((skip_gear & EXAMINE_SKIPJUMPSUIT) && (skip_body & EXAMINE_SKIPFACE))) @@ -144,7 +134,7 @@ LAZYADD(pocket_msg, r_store_message) if(LAZYLEN(pocket_msg)) tie_msg += " Near the waist it has [english_list(pocket_msg)]." - + if(w_uniform.blood_DNA) msg += "[T.He] [T.is] wearing [bicon(w_uniform)] [w_uniform.gender==PLURAL?"some":"a"] [(w_uniform.blood_color != SYNTH_BLOOD_COLOUR) ? "blood" : "oil"]-stained [w_uniform.name]![tie_msg]" else diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index d17964c60b..3a1a77826c 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -855,13 +855,25 @@ remoteview_target = null reset_view(0) -/mob/living/carbon/human/get_visible_gender() - if(wear_suit && wear_suit.flags_inv & HIDEJUMPSUIT && ((head && head.flags_inv & HIDEMASK) || wear_mask)) - return PLURAL //plural is the gender-neutral default - if(species) - if(species.ambiguous_genders) - return PLURAL // regardless of what you're wearing, your gender can't be figured out - return get_gender() +/mob/living/carbon/human/get_visible_gender(mob/user, force) + switch(force) + if(VISIBLE_GENDER_FORCE_PLURAL) + return PLURAL + if(VISIBLE_GENDER_FORCE_IDENTIFYING) + return get_gender() + if(VISIBLE_GENDER_FORCE_BIOLOGICAL) + return gender + else + if((wear_mask || (head?.flags_inv & HIDEMASK)) && (wear_suit?.flags_inv & HIDEJUMPSUIT)) + return PLURAL + if(species?.ambiguous_genders && user) + if(ishuman(user)) + var/mob/living/carbon/human/human = user + if(!istype(human.species, species)) + return PLURAL + else if(!isobserver(user) && !issilicon(user)) + return PLURAL + return get_gender() /mob/living/carbon/human/proc/increase_germ_level(n) if(gloves) diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm index df2994530d..1a9686e7e8 100644 --- a/code/modules/mob/living/carbon/human/human_movement.dm +++ b/code/modules/mob/living/carbon/human/human_movement.dm @@ -291,3 +291,8 @@ playsound(T, S, volume, FALSE) return + +/mob/living/carbon/human/set_dir(var/new_dir) + . = ..() + if(. && (species.tail || tail_style)) + update_tail_showing() \ No newline at end of file diff --git a/code/modules/mob/living/carbon/human/species/station/teshari.dm b/code/modules/mob/living/carbon/human/species/station/teshari.dm index 753dd211ce..17fe6606da 100644 --- a/code/modules/mob/living/carbon/human/species/station/teshari.dm +++ b/code/modules/mob/living/carbon/human/species/station/teshari.dm @@ -64,7 +64,7 @@ ambiguous_genders = TRUE - spawn_flags = SPECIES_CAN_JOIN | SPECIES_IS_WHITELISTED | SPECIES_NO_POSIBRAIN //CHOMPedit: This is overriden by teshari_vr.dm. Noting here for future reference. + spawn_flags = SPECIES_CAN_JOIN | SPECIES_IS_WHITELISTED appearance_flags = HAS_HAIR_COLOR | HAS_SKIN_COLOR | HAS_EYE_COLOR bump_flag = MONKEY swap_flags = MONKEY|SLIME|SIMPLE_ANIMAL diff --git a/code/modules/mob/living/carbon/human/update_icons.dm b/code/modules/mob/living/carbon/human/update_icons.dm index aff59d4010..868fd0e467 100644 --- a/code/modules/mob/living/carbon/human/update_icons.dm +++ b/code/modules/mob/living/carbon/human/update_icons.dm @@ -66,37 +66,40 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() #define MOB_DAM_LAYER 4 //Injury overlay sprites like open wounds #define SURGERY_LAYER 5 //Overlays for open surgical sites #define UNDERWEAR_LAYER 6 //Underwear/bras/etc -#define SHOES_LAYER_ALT 7 //Shoe-slot item (when set to be under uniform via verb) -#define UNIFORM_LAYER 8 //Uniform-slot item -#define ID_LAYER 9 //ID-slot item -#define SHOES_LAYER 10 //Shoe-slot item -#define GLOVES_LAYER 11 //Glove-slot item -#define BELT_LAYER 12 //Belt-slot item -#define SUIT_LAYER 13 //Suit-slot item -#define TAIL_LAYER 14 //Some species have tails to render -#define GLASSES_LAYER 15 //Eye-slot item -#define BELT_LAYER_ALT 16 //Belt-slot item (when set to be above suit via verb) -#define SUIT_STORE_LAYER 17 //Suit storage-slot item -#define BACK_LAYER 18 //Back-slot item -#define HAIR_LAYER 19 //The human's hair -#define HAIR_ACCESSORY_LAYER 20 //VOREStation edit. Simply move this up a number if things are added. -#define EARS_LAYER 21 //Both ear-slot items (combined image) -#define EYES_LAYER 22 //Mob's eyes (used for glowing eyes) -#define FACEMASK_LAYER 23 //Mask-slot item -#define HEAD_LAYER 24 //Head-slot item -#define HANDCUFF_LAYER 25 //Handcuffs, if the human is handcuffed, in a secret inv slot -#define LEGCUFF_LAYER 26 //Same as handcuffs, for legcuffs -#define L_HAND_LAYER 27 //Left-hand item -#define R_HAND_LAYER 28 //Right-hand item -#define WING_LAYER 29 //Wings or protrusions over the suit. -#define TAIL_LAYER_ALT 30 //Modified tail-sprite layer. Tend to be larger. -#define MODIFIER_EFFECTS_LAYER 31 //Effects drawn by modifiers -#define FIRE_LAYER 32 //'Mob on fire' overlay layer -#define MOB_WATER_LAYER 33 //'Mob submerged' overlay layer -#define TARGETED_LAYER 34 //'Aimed at' overlay layer -#define TOTAL_LAYERS 34 //VOREStation edit. <---- KEEP THIS UPDATED, should always equal the highest number here, used to initialize a list. +#define TAIL_SOUTH_LAYER 7 //Tail as viewed from the south +#define SHOES_LAYER_ALT 8 //Shoe-slot item (when set to be under uniform via verb) +#define UNIFORM_LAYER 9 //Uniform-slot item +#define ID_LAYER 10 //ID-slot item +#define SHOES_LAYER 11 //Shoe-slot item +#define GLOVES_LAYER 12 //Glove-slot item +#define BELT_LAYER 13 //Belt-slot item +#define SUIT_LAYER 14 //Suit-slot item +#define TAIL_NORTH_LAYER 15 //Some species have tails to render (As viewed from the N, E, or W) +#define GLASSES_LAYER 16 //Eye-slot item +#define BELT_LAYER_ALT 17 //Belt-slot item (when set to be above suit via verb) +#define SUIT_STORE_LAYER 18 //Suit storage-slot item +#define BACK_LAYER 19 //Back-slot item +#define HAIR_LAYER 20 //The human's hair +#define HAIR_ACCESSORY_LAYER 21 //VOREStation edit. Simply move this up a number if things are added. +#define EARS_LAYER 22 //Both ear-slot items (combined image) +#define EYES_LAYER 23 //Mob's eyes (used for glowing eyes) +#define FACEMASK_LAYER 24 //Mask-slot item +#define HEAD_LAYER 25 //Head-slot item +#define HANDCUFF_LAYER 26 //Handcuffs, if the human is handcuffed, in a secret inv slot +#define LEGCUFF_LAYER 27 //Same as handcuffs, for legcuffs +#define L_HAND_LAYER 28 //Left-hand item +#define R_HAND_LAYER 29 //Right-hand item +#define WING_LAYER 30 //Wings or protrusions over the suit. +#define TAIL_NORTH_LAYER_ALT 31 //Modified tail-sprite layer. Tend to be larger. +#define MODIFIER_EFFECTS_LAYER 32 //Effects drawn by modifiers +#define FIRE_LAYER 33 //'Mob on fire' overlay layer +#define MOB_WATER_LAYER 34 //'Mob submerged' overlay layer +#define TARGETED_LAYER 35 //'Aimed at' overlay layer +#define TOTAL_LAYERS 35 //VOREStation edit. <---- KEEP THIS UPDATED, should always equal the highest number here, used to initialize a list. ////////////////////////////////// +#define GET_TAIL_LAYER (dir == SOUTH ? TAIL_SOUTH_LAYER : TAIL_NORTH_LAYER) + /mob/living/carbon/human var/list/overlays_standing[TOTAL_LAYERS] var/previous_damage_appearance // store what the body last looked like, so we only have to update it if something changed @@ -376,6 +379,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() update_tail_showing() update_wing_showing() + /mob/living/carbon/human/proc/update_skin() if(QDESTROYING(src)) return @@ -838,7 +842,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() suit_sprite = INV_SUIT_DEF_ICON var/icon/c_mask = null - var/tail_is_rendered = (overlays_standing[TAIL_LAYER] || overlays_standing[TAIL_LAYER_ALT]) + var/tail_is_rendered = (overlays_standing[TAIL_NORTH_LAYER] || overlays_standing[TAIL_NORTH_LAYER_ALT] || overlays_standing[TAIL_SOUTH_LAYER]) var/valid_clip_mask = tail_style?.clip_mask if(tail_is_rendered && valid_clip_mask && !(istype(suit) && suit.taurized)) //Clip the lower half of the suit off using the tail's clip mask for taurs since taur bodies aren't hidden. @@ -955,16 +959,19 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() if(QDESTROYING(src)) return - remove_layer(TAIL_LAYER) - remove_layer(TAIL_LAYER_ALT) // Alt Tail Layer + remove_layer(TAIL_NORTH_LAYER) + remove_layer(TAIL_NORTH_LAYER_ALT) + remove_layer(TAIL_SOUTH_LAYER) - var/used_tail_layer = tail_alt ? TAIL_LAYER_ALT : TAIL_LAYER + var/tail_layer = GET_TAIL_LAYER + if(tail_alt && tail_layer == TAIL_NORTH_LAYER) + tail_layer = TAIL_NORTH_LAYER_ALT var/image/tail_image = get_tail_image() if(tail_image) - tail_image.layer = BODY_LAYER+used_tail_layer - overlays_standing[used_tail_layer] = tail_image - apply_layer(used_tail_layer) + tail_image.layer = BODY_LAYER+tail_layer + overlays_standing[tail_layer] = tail_image + apply_layer(tail_layer) return var/species_tail = species.get_tail(src) // Species tail icon_state prefix. @@ -972,7 +979,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() //This one is actually not that bad I guess. if(species_tail && !(wear_suit && wear_suit.flags_inv & HIDETAIL)) var/icon/tail_s = get_tail_icon() - overlays_standing[used_tail_layer] = image(icon = tail_s, icon_state = "[species_tail]_s", layer = BODY_LAYER+used_tail_layer) // Alt Tail Layer + overlays_standing[tail_layer] = image(icon = tail_s, icon_state = "[species_tail]_s", layer = BODY_LAYER+tail_layer) animate_tail_reset() //TODO: Is this the appropriate place for this, and not on species...? @@ -997,19 +1004,22 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() return tail_icon /mob/living/carbon/human/proc/set_tail_state(var/t_state) - var/used_tail_layer = tail_alt ? TAIL_LAYER_ALT : TAIL_LAYER // Alt Tail Layer - var/image/tail_overlay = overlays_standing[used_tail_layer] + var/tail_layer = GET_TAIL_LAYER + if(tail_alt && tail_layer == TAIL_NORTH_LAYER) + tail_layer = TAIL_NORTH_LAYER_ALT + var/image/tail_overlay = overlays_standing[tail_layer] - remove_layer(TAIL_LAYER) - remove_layer(TAIL_LAYER_ALT) + remove_layer(TAIL_NORTH_LAYER) + remove_layer(TAIL_NORTH_LAYER_ALT) + remove_layer(TAIL_SOUTH_LAYER) if(tail_overlay) - overlays_standing[used_tail_layer] = tail_overlay + overlays_standing[tail_layer] = tail_overlay if(species.get_tail_animation(src)) tail_overlay.icon_state = t_state . = tail_overlay - apply_layer(used_tail_layer) + apply_layer(tail_layer) //Not really once, since BYOND can't do that. //Update this if the ability to flick() images or make looping animation start at the first frame is ever added. @@ -1019,9 +1029,9 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() return var/t_state = "[species.get_tail(src)]_once" - var/used_tail_layer = tail_alt ? TAIL_LAYER_ALT : TAIL_LAYER // Alt Tail Layer + var/tail_layer = GET_TAIL_LAYER - var/image/tail_overlay = overlays_standing[used_tail_layer] // Alt Tail Layer + var/image/tail_overlay = overlays_standing[tail_layer] if(tail_overlay && tail_overlay.icon_state == t_state) return //let the existing animation finish @@ -1029,7 +1039,7 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() if(tail_overlay) spawn(20) //check that the animation hasn't changed in the meantime - if(overlays_standing[used_tail_layer] == tail_overlay && tail_overlay.icon_state == t_state) // Alt Tail Layer + if(overlays_standing[tail_layer] == tail_overlay && tail_overlay.icon_state == t_state) animate_tail_stop() /mob/living/carbon/human/proc/animate_tail_start() @@ -1279,7 +1289,9 @@ var/global/list/damage_icon_parts = list() //see UpdateDamageIcon() #undef GLOVES_LAYER #undef BELT_LAYER #undef SUIT_LAYER -#undef TAIL_LAYER +#undef TAIL_NORTH_LAYER +#undef TAIL_SOUTH_LAYER +#undef GET_TAIL_LAYER #undef GLASSES_LAYER #undef BELT_LAYER_ALT #undef SUIT_STORE_LAYER diff --git a/code/modules/mob/living/simple_mob/simple_mob_vr.dm b/code/modules/mob/living/simple_mob/simple_mob_vr.dm index ae30050e95..e7866ec375 100644 --- a/code/modules/mob/living/simple_mob/simple_mob_vr.dm +++ b/code/modules/mob/living/simple_mob/simple_mob_vr.dm @@ -215,6 +215,7 @@ // Since they have bellies, add verbs to toggle settings on them. verbs |= /mob/living/simple_mob/proc/toggle_digestion verbs |= /mob/living/simple_mob/proc/toggle_fancygurgle + verbs |= /mob/living/proc/vertical_nom //A much more detailed version of the default /living implementation var/obj/belly/B = new /obj/belly(src) diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/pets/fennec.dm b/code/modules/mob/living/simple_mob/subtypes/animal/pets/fennec.dm new file mode 100644 index 0000000000..0ea451246f --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/pets/fennec.dm @@ -0,0 +1,29 @@ +/mob/living/simple_mob/animal/passive/fennec + name = "fennec" + desc = "A fox preferring arid climates, also known as a dingler, or a goob." + tt_desc = "Vulpes Zerda" + icon_state = "fennec" + item_state = "fennec" + + movement_cooldown = 0.5 SECONDS + + see_in_dark = 6 + response_help = "pets" + response_disarm = "gently pushes aside" + response_harm = "kicks" + + holder_type = /obj/item/holder/fennec + mob_size = MOB_SMALL + + has_langs = list("Cat, Dog") //they're similar, why not. + +/mob/living/simple_mob/animal/passive/fennec/faux + name = "faux" + desc = "Domesticated fennec. Seems to like screaming just as much though." + +/mob/living/simple_mob/animal/passive/fennec/Initialize() + icon_living = "[initial(icon_state)]" + icon_dead = "[initial(icon_state)]_dead" + icon_rest = "[initial(icon_state)]_rest" + update_icon() + return ..() diff --git a/code/modules/mob/new_player/preferences_setup.dm b/code/modules/mob/new_player/preferences_setup.dm index 66d712bc44..e564bba7b0 100644 --- a/code/modules/mob/new_player/preferences_setup.dm +++ b/code/modules/mob/new_player/preferences_setup.dm @@ -259,6 +259,7 @@ mannequin.update_transform() //VOREStation Edit to update size/shape stuff. mannequin.toggle_tail(setting = TRUE) mannequin.toggle_wing(setting = TRUE) + mannequin.update_tail_showing() COMPILE_OVERLAYS(mannequin) update_character_previews(new /mutable_appearance(mannequin)) diff --git a/code/modules/mob/new_player/sprite_accessories_ear_vr.dm b/code/modules/mob/new_player/sprite_accessories_ear_vr.dm index d9be7399c4..62f0bfeda3 100644 --- a/code/modules/mob/new_player/sprite_accessories_ear_vr.dm +++ b/code/modules/mob/new_player/sprite_accessories_ear_vr.dm @@ -119,6 +119,15 @@ do_colouration = 1 color_blend_mode = ICON_MULTIPLY +/datum/sprite_accessory/ears/antennae_eye + name = "antennae eye, colorable" + desc = "" + icon_state = "antennae" + extra_overlay = "antennae_eye_1" + extra_overlay2 = "antennae_eye_2" + do_colouration = 1 + color_blend_mode = ICON_MULTIPLY + /datum/sprite_accessory/ears/curly_bug name = "curly antennae, colorable" desc = "" diff --git a/code/modules/mob/new_player/sprite_accessories_taur.dm b/code/modules/mob/new_player/sprite_accessories_taur.dm index 4637ac008e..4288f9bbc6 100644 --- a/code/modules/mob/new_player/sprite_accessories_taur.dm +++ b/code/modules/mob/new_player/sprite_accessories_taur.dm @@ -285,6 +285,11 @@ suit_sprites = 'icons/mob/taursuits_slug.dmi' icon_sprite_tag = "slug" +/datum/sprite_accessory/tail/taur/slug/snail + name = "Snail (Taur)" + icon_state = "slug_s" + extra_overlay = "snail_shell_marking" + /datum/sprite_accessory/tail/taur/frog name = "Frog (Taur)" icon_state = "frog_s" diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm index 7ab6de6fe5..08547e3146 100644 --- a/code/modules/mob/typing_indicator.dm +++ b/code/modules/mob/typing_indicator.dm @@ -40,11 +40,8 @@ set hidden = 1 set_typing_indicator(TRUE) - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_text(usr, "Type your message:", "Say") - else - message = input(usr, "Type your message:", "Say") as text + var/message = tgui_input_text(usr, "Type your message:", "Say") + message = readd_quotes(message) set_typing_indicator(FALSE) if(message) @@ -55,11 +52,8 @@ set hidden = 1 set_typing_indicator(TRUE) - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_message(usr, "Type your message:", "Emote") - else - message = input(usr, "Type your message:", "Emote") as message + var/message = tgui_input_text(usr, "Type your message:", "Emote", multiline = TRUE) + message = readd_quotes(message) set_typing_indicator(FALSE) if(message) @@ -70,11 +64,8 @@ set name = ".Whisper" set hidden = 1 - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_text(usr, "Type your message:", "Whisper") - else - message = input(usr, "Type your message:", "Whisper") as text + var/message = tgui_input_text(usr, "Type your message:", "Whisper") + message = readd_quotes(message) if(message) whisper(message) @@ -83,11 +74,8 @@ set name = ".Subtle" set hidden = 1 - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_message(usr, "Type your message:", "Subtle") - else - message = input(usr, "Type your message:", "Subtle") as message + var/message = tgui_input_text(usr, "Type your message:", "Subtle", multiline = TRUE) + message = readd_quotes(message) if(message) - me_verb_subtle(message) + me_verb_subtle(message) \ No newline at end of file diff --git a/code/modules/paperwork/adminpaper.dm b/code/modules/paperwork/adminpaper.dm index 426cbcb6e4..91d3fe241c 100644 --- a/code/modules/paperwork/adminpaper.dm +++ b/code/modules/paperwork/adminpaper.dm @@ -87,7 +87,7 @@ to_chat(usr, "There isn't enough space left on \the [src] to write anything.") return - var/raw_t = tgui_input_message(usr, "Enter what you want to write:", "Write") + var/raw_t = tgui_input_text(usr, "Enter what you want to write:", "Write", multiline = TRUE) if(!raw_t) return var/t = sanitize(raw_t, free_space, extra = 0) diff --git a/code/modules/projectiles/guns/energy/stun_vr.dm b/code/modules/projectiles/guns/energy/stun_vr.dm index b0bfd53e46..3ddc43986c 100644 --- a/code/modules/projectiles/guns/energy/stun_vr.dm +++ b/code/modules/projectiles/guns/energy/stun_vr.dm @@ -3,4 +3,7 @@ fire_delay = 4 /obj/item/weapon/gun/energy/stunrevolver - charge_cost = 400 \ No newline at end of file + charge_cost = 400 + +/obj/item/weapon/gun/energy/taser/mounted/cyborg + charge_cost = 160 \ No newline at end of file diff --git a/code/modules/tgui/modules/rcon.dm b/code/modules/tgui/modules/rcon.dm index 624b353c4b..0a29153d76 100644 --- a/code/modules/tgui/modules/rcon.dm +++ b/code/modules/tgui/modules/rcon.dm @@ -40,7 +40,7 @@ smes_data["RCON_tag"] = SMES.RCon_tag smeslist.Add(list(smes_data)) - data["pages"] = number_pages + data["pages"] = number_pages + 1 data["current_page"] = current_page data["smes_info"] = sortByKey(smeslist, "RCON_tag") diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index c34ccf63d7..42acb1eac0 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -32,8 +32,12 @@ var/closing = FALSE /// The status/visibility of the UI. var/status = STATUS_INTERACTIVE + /// Timed refreshing state + var/refreshing = FALSE /// Topic state used to determine status/interactability. var/datum/tgui_state/state = null + /// Rate limit client refreshes to prevent DoS. + COOLDOWN_DECLARE(refresh_cooldown) /// The map z-level to display. var/map_z_level = 1 /// The Parent UI @@ -176,11 +180,17 @@ /datum/tgui/proc/send_full_update(custom_data, force) if(!user.client || !initialized || closing) return + if(!COOLDOWN_FINISHED(src, refresh_cooldown)) + refreshing = TRUE + addtimer(CALLBACK(src, .proc/send_full_update), TGUI_REFRESH_FULL_UPDATE_COOLDOWN, TIMER_UNIQUE) + return + refreshing = FALSE var/should_update_data = force || status >= STATUS_UPDATE window.send_message("update", get_payload( custom_data, with_data = should_update_data, with_static_data = TRUE)) + COOLDOWN_START(src, refresh_cooldown, TGUI_REFRESH_FULL_UPDATE_COOLDOWN) /** * public @@ -211,6 +221,7 @@ "title" = title, "status" = status, "interface" = interface, + "refreshing" = refreshing, "map" = (using_map && using_map.path) ? using_map.path : "Unknown", "mapZLevel" = map_z_level, "window" = list( @@ -312,6 +323,9 @@ return FALSE switch(type) if("ready") + // Send a full update when the user manually refreshes the UI + if(initialized) + send_full_update() initialized = TRUE if("pingReply") initialized = TRUE diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui/tgui_input_text.dm deleted file mode 100644 index e2247a32ce..0000000000 --- a/code/modules/tgui/tgui_input_text.dm +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Creates a TGUI input text window and returns the user's response. - * - * This proc should be used to create alerts that the caller will wait for a response from. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_text(mob/user, message, title, default, timeout = 0) - if (istext(user)) - stack_trace("tgui_input_text() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "text" - input.tgui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) - -/** - * Creates a TGUI input message window and returns the user's response. - * - * This proc should be used to create alerts that the caller will wait for a response from. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_message(mob/user, message, title, default, timeout = 0) - if (istext(user)) - stack_trace("tgui_input_message() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "message" - input.tgui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) - -/** - * Creates a TGUI input num window and returns the user's response. - * - * This proc should be used to create alerts that the caller will wait for a response from. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_num(mob/user, message, title, default, timeout = 0) - if (istext(user)) - stack_trace("tgui_input_num() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "num" - input.tgui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) - -/** - * Creates an asynchronous TGUI input text window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_input_text_async() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout) - input.input_type = "text" - input.tgui_interact(user) - -/** - * Creates an asynchronous TGUI input message window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_message_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_input_message_async() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout) - input.input_type = "message" - input.tgui_interact(user) - -/** - * Creates an asynchronous TGUI input num window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_num_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_input_num_async() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout) - input.input_type = "num" - input.tgui_interact(user) - -/** - * # tgui_input_dialog - * - * Datum used for instantiating and using a TGUI-controlled input that prompts the user with - * a message and a box for accepting text/message/num input. - */ -/datum/tgui_input_dialog - /// The title of the TGUI window - var/title - /// The textual body of the TGUI window - var/message - /// The default value to initially populate the input box. - var/initial - /// The value that the user input into the input box, null if cancelled. - var/choice - /// The time at which the tgui_text_input was created, for displaying timeout progress. - var/start_time - /// The lifespan of the tgui_text_input, after which the window will close and delete itself. - var/timeout - /// Boolean field describing if the tgui_text_input was closed by the user. - var/closed - /// Indicates the data type we want to collect ("text", "message", "num") - var/input_type = "text" - -/datum/tgui_input_dialog/New(mob/user, message, title, default, timeout) - src.title = title - src.message = message - // TODO - Do we need to sanitize the initial value for illegal characters? - src.initial = default - if (timeout) - src.timeout = timeout - start_time = world.time - QDEL_IN(src, timeout) - -/datum/tgui_input_dialog/Destroy(force, ...) - SStgui.close_uis(src) - . = ..() - -/** - * Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if - * the window was closed by the user. - */ -/datum/tgui_input_dialog/proc/wait() - while (!choice && !closed) - stoplag(1) - -/datum/tgui_input_dialog/tgui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "InputModal") - ui.open() - -/datum/tgui_input_dialog/tgui_close(mob/user) - . = ..() - closed = TRUE - -/datum/tgui_input_dialog/tgui_state(mob/user) - return GLOB.tgui_always_state - -/datum/tgui_input_dialog/tgui_static_data(mob/user) - . = list( - "title" = title, - "message" = message, - "initial" = initial, - "input_type" = input_type - ) - -/datum/tgui_input_dialog/tgui_data(mob/user) - . = list() - if(timeout) - .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1) - -/datum/tgui_input_dialog/tgui_act(action, list/params) - . = ..() - if (.) - return - switch(action) - if("choose") - set_choice(params["choice"]) - if(isnull(src.choice)) - return - SStgui.close_uis(src) - return TRUE - if("cancel") - SStgui.close_uis(src) - closed = TRUE - return TRUE - -/datum/tgui_input_dialog/proc/set_choice(choice) - if(input_type == "num") - src.choice = text2num(choice) - return - src.choice = choice - -/** - * # async tgui_text_input - * - * An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses. - */ -/datum/tgui_input_dialog/async - /// The callback to be invoked by the tgui_text_input upon having a choice made. - var/datum/callback/callback - -/datum/tgui_input_dialog/async/New(mob/user, message, title, default, callback, timeout) - ..(user, title, message, default, timeout) - src.callback = callback - -/datum/tgui_input_dialog/async/Destroy(force, ...) - QDEL_NULL(callback) - . = ..() - -/datum/tgui_input_dialog/async/tgui_close(mob/user) - . = ..() - qdel(src) - -/datum/tgui_input_dialog/async/set_choice(choice) - . = ..() - if(!isnull(src.choice)) - callback?.InvokeAsync(src.choice) - -/datum/tgui_input_dialog/async/wait() - return diff --git a/code/modules/tgui/tgui_alert.dm b/code/modules/tgui_input/alert.dm similarity index 74% rename from code/modules/tgui/tgui_alert.dm rename to code/modules/tgui_input/alert.dm index 4ae15da886..a53089e4b0 100644 --- a/code/modules/tgui/tgui_alert.dm +++ b/code/modules/tgui_input/alert.dm @@ -8,8 +8,9 @@ * * title - The of the alert modal, shown on the top of the TGUI window. * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout. + * * autofocus - The bool that controls if this alert should grab window focus. */ -/proc/tgui_alert(mob/user, message = null, title = null, list/buttons = list("Ok"), timeout = 0) +/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE) if (istext(buttons)) stack_trace("tgui_alert() received text for buttons instead of list") return @@ -24,7 +25,19 @@ user = client.mob else return - var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout) + // A gentle nudge - you should not be using TGUI alert for anything other than a simple message. + if(length(buttons) > 3) + log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert") + return tgui_input_list(user, message, title, buttons, timeout, autofocus) + + // Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + if(length(buttons) == 2) + return alert(user, message, title, buttons[1], buttons[2]) + if(length(buttons) == 3) + return alert(user, message, title, buttons[1], buttons[2], buttons[3]) + + var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus) alert.tgui_interact(user) alert.wait() if (alert) @@ -32,37 +45,7 @@ qdel(alert) /** - * Creates an asynchronous TGUI alert window with an associated callback. - * - * This proc should be used to create alerts that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the alert to. - * * message - The content of the alert, shown in the body of the TGUI window. - * * title - The of the alert modal, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise. - */ -/proc/tgui_alert_async(mob/user, message = null, title = null, list/buttons = list("Ok"), datum/callback/callback, timeout = 0) - if (istext(buttons)) - stack_trace("tgui_alert() received text for buttons instead of list") - return - if (istext(user)) - stack_trace("tgui_alert() received text for user instead of list") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_alert/async/alert = new(user, message, title, buttons, callback, timeout) - alert.tgui_interact(user) - -/** - * # tgui_modal + * # tgui_alert * * Datum used for instantiating and using a TGUI-controlled modal that prompts the user with * a message and has buttons for responses. @@ -80,13 +63,16 @@ var/start_time /// The lifespan of the tgui_modal, after which the window will close and delete itself. var/timeout + /// The bool that controls if this modal should grab window focus + var/autofocus /// Boolean field describing if the tgui_modal was closed by the user. var/closed -/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout) - src.title = title - src.message = message +/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus) + src.autofocus = autofocus src.buttons = buttons.Copy() + src.message = message + src.title = title if (timeout) src.timeout = timeout start_time = world.time @@ -118,15 +104,21 @@ /datum/tgui_alert/tgui_state(mob/user) return GLOB.tgui_always_state -/datum/tgui_alert/tgui_data(mob/user) - . = list( - "title" = title, - "message" = message, - "buttons" = buttons - ) +/datum/tgui_alert/tgui_static_data(mob/user) + var/list/data = list() + data["autofocus"] = autofocus + data["buttons"] = buttons + data["message"] = message + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data +/datum/tgui_alert/tgui_data(mob/user) + var/list/data = list() if(timeout) .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + return data /datum/tgui_alert/tgui_act(action, list/params) . = ..() @@ -139,10 +131,44 @@ set_choice(params["choice"]) SStgui.close_uis(src) return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE /datum/tgui_alert/proc/set_choice(choice) src.choice = choice +/** + * Creates an asynchronous TGUI alert window with an associated callback. + * + * This proc should be used to create alerts that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the alert to. + * * message - The content of the alert, shown in the body of the TGUI window. + * * title - The of the alert modal, shown on the top of the TGUI window. + * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise. + */ +/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = TRUE) + if (istext(buttons)) + stack_trace("tgui_alert() received text for buttons instead of list") + return + if (istext(user)) + stack_trace("tgui_alert() received text for user instead of list") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_alert/async/alert = new(user, message, title, buttons, callback, timeout, autofocus) + alert.tgui_interact(user) + /** * # async tgui_modal * @@ -152,8 +178,8 @@ /// The callback to be invoked by the tgui_modal upon having a choice made. var/datum/callback/callback -/datum/tgui_alert/async/New(mob/user, message, title, list/buttons, callback, timeout) - ..(user, message, title, buttons, timeout) +/datum/tgui_alert/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus) + ..(user, message, title, buttons, timeout, autofocus) src.callback = callback /datum/tgui_alert/async/Destroy(force, ...) diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui_input/list.dm similarity index 71% rename from code/modules/tgui/tgui_input_list.dm rename to code/modules/tgui_input/list.dm index 8b1af90196..76ebe411e4 100644 --- a/code/modules/tgui/tgui_input_list.dm +++ b/code/modules/tgui_input/list.dm @@ -6,17 +6,17 @@ * * user - The user to show the input box to. * * message - The content of the input box, shown in the body of the TGUI window. * * title - The title of the input box, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. * * default - The option with this value will be selected on first paint of the TGUI window. * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_list(mob/user, message, title, list/buttons, default, timeout = 0) +/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0) if (istext(user)) stack_trace("tgui_alert() received text for user instead of mob") return if (!user) user = usr - if(!length(buttons)) + if(!length(items)) return if (!istype(user)) if (istype(user, /client)) @@ -24,43 +24,16 @@ user = client.mob else return - var/datum/tgui_list_input/input = new(user, message, title, buttons, default, timeout) + /// Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + return input(user, message, title, default) as null|anything in items + var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout) input.tgui_interact(user) input.wait() if (input) . = input.choice qdel(input) -/** - * Creates an asynchronous TGUI input list window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. - * * default - The option with this value will be selected on first paint of the TGUI window. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_list_async(mob/user, message, title, list/buttons, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_alert() received text for user instead of mob") - return - if (!user) - user = usr - if(!length(buttons)) - return - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_list_input/async/input = new(user, message, title, buttons, default, callback, timeout) - input.tgui_interact(user) - /** * # tgui_list_input * @@ -72,14 +45,14 @@ var/title /// The textual body of the TGUI window var/message - /// The list of buttons (responses) provided on the TGUI window - var/list/buttons - /// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb) - var/list/buttons_map - /// Value of the button that should be pre-selected on first paint. - var/initial + /// The list of items (responses) provided on the TGUI window + var/list/items + /// Items (strings specifically) mapped to the actual value (e.g. a mob or a verb) + var/list/items_map /// The button that the user has pressed, null if no selection has been made var/choice + /// The default item to be selected + var/default /// The time at which the tgui_list_input was created, for displaying timeout progress. var/start_time /// The lifespan of the tgui_list_input, after which the window will close and delete itself. @@ -87,29 +60,29 @@ /// Boolean field describing if the tgui_list_input was closed by the user. var/closed -/datum/tgui_list_input/New(mob/user, message, title, list/buttons, default, timeout) +/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout) src.title = title src.message = message - src.buttons = list() - src.buttons_map = list() - src.initial = default - var/list/repeat_buttons = list() + src.items = list() + src.items_map = list() + src.default = default + var/list/repeat_items = list() // Gets rid of illegal characters var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"}) - for(var/i in buttons) + for(var/i in items) if(isnull(i)) - stack_trace("Null in a tgui_input_list() buttons") + stack_trace("Null in a tgui_input_list() items") continue var/string_key = whitelistedWords.Replace("[i]", "") //avoids duplicated keys E.g: when areas have the same name - string_key = avoid_assoc_duplicate_keys(string_key, repeat_buttons) + string_key = avoid_assoc_duplicate_keys(string_key, repeat_items) - src.buttons += string_key - src.buttons_map[string_key] = i + src.items += string_key + src.items_map[string_key] = i if (timeout) @@ -119,7 +92,7 @@ /datum/tgui_list_input/Destroy(force, ...) SStgui.close_uis(src) - QDEL_NULL(buttons) + QDEL_NULL(items) . = ..() /** @@ -133,7 +106,7 @@ /datum/tgui_list_input/tgui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, "ListInput") + ui = new(user, src, "ListInputModal") ui.open() /datum/tgui_list_input/tgui_close(mob/user) @@ -144,27 +117,31 @@ return GLOB.tgui_always_state /datum/tgui_list_input/tgui_static_data(mob/user) - . = list( - "title" = title, - "message" = message, - "buttons" = buttons, - "initial" = initial - ) + var/list/data = list() + data["init_value"] = default || items[1] + data["items"] = items + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["message"] = message + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data /datum/tgui_list_input/tgui_data(mob/user) - . = list() + var/list/data = list() if(timeout) .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1) + return data /datum/tgui_list_input/tgui_act(action, list/params) . = ..() if (.) return switch(action) - if("choose") - if (!(params["choice"] in buttons)) + if("submit") + if (!(params["entry"] in items)) return - set_choice(buttons_map[params["choice"]]) + set_choice(items_map[params["entry"]]) + closed = TRUE SStgui.close_uis(src) return TRUE if("cancel") @@ -175,6 +152,36 @@ /datum/tgui_list_input/proc/set_choice(choice) src.choice = choice +/** + * Creates an asynchronous TGUI input list window with an associated callback. + * + * This proc should be used to create inputs that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the input box to. + * * message - The content of the input box, shown in the body of the TGUI window. + * * title - The title of the input box, shown on the top of the TGUI window. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - The option with this value will be selected on first paint of the TGUI window. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_list_async(mob/user, message, title, list/items, default, datum/callback/callback, timeout = 60 SECONDS) + if (istext(user)) + stack_trace("tgui_alert() received text for user instead of mob") + return + if (!user) + user = usr + if(!length(items)) + return + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout) + input.tgui_interact(user) + /** * # async tgui_list_input * @@ -184,8 +191,8 @@ /// The callback to be invoked by the tgui_list_input upon having a choice made. var/datum/callback/callback -/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, default, callback, timeout) - ..(user, title, message, buttons, default, timeout) +/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout) + ..(user, title, message, items, default, timeout) src.callback = callback /datum/tgui_list_input/async/Destroy(force, ...) diff --git a/code/modules/tgui_input/number.dm b/code/modules/tgui_input/number.dm new file mode 100644 index 0000000000..3f7ffccd96 --- /dev/null +++ b/code/modules/tgui_input/number.dm @@ -0,0 +1,209 @@ +/** + * Creates a TGUI window with a number input. Returns the user's response as num | null. + * + * This proc should be used to create windows for number entry that the caller will wait for a response from. + * If tgui fancy chat is turned off: Will return a normal input. If a max or min value is specified, will + * validate the input inside the UI and ui_act. + * + * Arguments: + * * user - The user to show the number input to. + * * message - The content of the number input, shown in the body of the TGUI window. + * * title - The title of the number input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this. + * * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000. + * * min_value - Specifies a minimum value. Often 0. + * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. + * * round_value - whether the inputted number is rounded down into an integer. + */ +/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE) + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + var/input_number = input(user, message, title, default) as null|num + return clamp(round_value ? round(input_number) : input_number, min_value, max_value) + var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value) + number_input.tgui_interact(user) + number_input.wait() + if (number_input) + . = number_input.entry + qdel(number_input) + +/** + * # tgui_input_number + * + * Datum used for instantiating and using a TGUI-controlled number input that prompts the user with + * a message and has an input for number entry. + */ +/datum/tgui_input_number + /// Boolean field describing if the tgui_input_number was closed by the user. + var/closed + /// The default (or current) value, shown as a default. Users can press reset with this. + var/default + /// The entry that the user has return_typed in. + var/entry + /// The maximum value that can be entered. + var/max_value + /// The prompt's body, if any, of the TGUI window. + var/message + /// The minimum value that can be entered. + var/min_value + /// Whether the submitted number is rounded down into an integer. + var/round_value + /// The time at which the number input was created, for displaying timeout progress. + var/start_time + /// The lifespan of the number input, after which the window will close and delete itself. + var/timeout + /// The title of the TGUI window + var/title + +/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value) + src.default = default + src.max_value = max_value + src.message = message + src.min_value = min_value + src.title = title + src.round_value = round_value + if (timeout) + src.timeout = timeout + start_time = world.time + QDEL_IN(src, timeout) + /// Checks for empty numbers - bank accounts, etc. + if(max_value == 0) + src.min_value = 0 + if(default) + src.default = 0 + /// Sanity check + if(default < min_value) + src.default = min_value + if(default > max_value) + CRASH("Default value is greater than max value.") + +/datum/tgui_input_number/Destroy(force, ...) + SStgui.close_uis(src) + return ..() + +/** + * Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_input_number/proc/wait() + while (!entry && !closed && !QDELETED(src)) + stoplag(1) + +/datum/tgui_input_number/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "NumberInputModal") + ui.open() + +/datum/tgui_input_number/tgui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_number/tgui_state(mob/user) + return GLOB.tgui_always_state + +/datum/tgui_input_number/tgui_static_data(mob/user) + var/list/data = list() + data["init_value"] = default // Default is a reserved keyword + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["max_value"] = max_value + data["message"] = message + data["min_value"] = min_value + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data + +/datum/tgui_input_number/tgui_data(mob/user) + var/list/data = list() + if(timeout) + data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + return data + +/datum/tgui_input_number/tgui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if(!isnum(params["entry"])) + CRASH("A non number was input into tgui input number by [usr]") + var/choice = round_value ? round(params["entry"]) : params["entry"] + if(choice > max_value) + CRASH("A number greater than the max value was input into tgui input number by [usr]") + if(choice < min_value) + CRASH("A number less than the min value was input into tgui input number by [usr]") + set_entry(choice) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE + +/datum/tgui_input_number/proc/set_entry(entry) + src.entry = entry + +/** + * Creates an asynchronous TGUI input num window with an associated callback. + * + * This proc should be used to create inputs that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the input box to. + * * message - The content of the input box, shown in the body of the TGUI window. + * * title - The title of the input box, shown on the top of the TGUI window. + * * default - The default value pre-populated in the input box. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_number_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) + if (istext(user)) + stack_trace("tgui_input_num_async() received text for user instead of mob") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_input_number/async/input = new(user, message, title, default, callback, timeout) + input.tgui_interact(user) + +/** + * # async tgui_text_input + * + * An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_number/async + /// The callback to be invoked by the tgui_text_input upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_number/async/New(mob/user, message, title, default, callback, timeout) + ..(user, title, message, default, timeout) + src.callback = callback + +/datum/tgui_input_number/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_input_number/async/tgui_close(mob/user) + . = ..() + qdel(src) + +/datum/tgui_input_number/async/set_entry(entry) + . = ..() + if(!isnull(src.entry)) + callback?.InvokeAsync(src.entry) + +/datum/tgui_input_number/async/wait() + return \ No newline at end of file diff --git a/code/modules/tgui_input/text.dm b/code/modules/tgui_input/text.dm new file mode 100644 index 0000000000..474f8e7c3e --- /dev/null +++ b/code/modules/tgui_input/text.dm @@ -0,0 +1,209 @@ +/** + * Creates a TGUI window with a text input. Returns the user's response. + * + * This proc should be used to create windows for text entry that the caller will wait for a response from. + * If tgui fancy chat is turned off: Will return a normal input. If max_length is specified, will return + * stripped_multiline_input. + * + * Arguments: + * * user - The user to show the text input to. + * * message - The content of the text input, shown in the body of the TGUI window. + * * title - The title of the text input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. + * * max_length - Specifies a max length for input. MAX_MESSAGE_LEN is default (1024) + * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc. + * * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input. + * * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0) + if (istext(user)) + stack_trace("tgui_input_text() received text for user instead of mob") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + if(encode) + if(multiline) + return stripped_multiline_input(user, message, title, default, max_length) + else + return stripped_input(user, message, title, default, max_length) + else + if(multiline) + return input(user, message, title, default) as message|null + else + return input(user, message, title, default) as text|null + var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout) + text_input.tgui_interact(user) + text_input.wait() + if (text_input) + . = text_input.entry + qdel(text_input) + +/** + * tgui_input_text + * + * Datum used for instantiating and using a TGUI-controlled text input that prompts the user with + * a message and has an input for text entry. + */ +/datum/tgui_input_text + /// Boolean field describing if the tgui_input_text was closed by the user. + var/closed + /// The default (or current) value, shown as a default. + var/default + /// Whether the input should be stripped using html_encode + var/encode + /// The entry that the user has return_typed in. + var/entry + /// The maximum length for text entry + var/max_length + /// The prompt's body, if any, of the TGUI window. + var/message + /// Multiline input for larger input boxes. + var/multiline + /// The time at which the text input was created, for displaying timeout progress. + var/start_time + /// The lifespan of the text input, after which the window will close and delete itself. + var/timeout + /// The title of the TGUI window + var/title + +/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout) + src.default = default + src.encode = encode + src.max_length = max_length + src.message = message + src.multiline = multiline + src.title = title + if (timeout) + src.timeout = timeout + start_time = world.time + QDEL_IN(src, timeout) + +/datum/tgui_input_text/Destroy(force, ...) + SStgui.close_uis(src) + . = ..() + +/** + * Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_input_text/proc/wait() + while (!entry && !closed) + stoplag(1) + +/datum/tgui_input_text/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TextInputModal") + ui.open() + +/datum/tgui_input_text/tgui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_text/tgui_state(mob/user) + return GLOB.tgui_always_state + +/datum/tgui_input_text/tgui_static_data(mob/user) + var/list/data = list() + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["max_length"] = max_length + data["message"] = message + data["multiline"] = multiline + data["placeholder"] = default // Default is a reserved keyword + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data + +/datum/tgui_input_text/tgui_data(mob/user) + var/list/data = list() + if(timeout) + .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1) + return data + +/datum/tgui_input_text/tgui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if(length(params["entry"]) > max_length) + return + if(encode && (length(html_encode(params["entry"])) > max_length)) + to_chat(usr, span_notice("Your message was clipped due to special character usage.")) + set_entry(params["entry"]) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + SStgui.close_uis(src) + closed = TRUE + return TRUE + +/datum/tgui_input_text/proc/set_entry(entry) + if(!isnull(entry)) + var/converted_entry = encode ? html_encode(entry) : entry + src.entry = trim(converted_entry, max_length) + +/** + * Creates an asynchronous TGUI input text window with an associated callback. + * + * This proc should be used to create inputs that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the input box to. + * * message - The content of the input box, shown in the body of the TGUI window. + * * title - The title of the input box, shown on the top of the TGUI window. + * * default - The default value pre-populated in the input box. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, max_length, multiline, encode, timeout = 60 SECONDS) + if (istext(user)) + stack_trace("tgui_input_text_async() received text for user instead of mob") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_input_text/async/input = new(user, message, title, default, callback, max_length, multiline, encode, timeout) + input.tgui_interact(user) + +/** + * # async tgui_text_input + * + * An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_text/async + /// The callback to be invoked by the tgui_text_input upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_text/async/New(mob/user, message, title, default, callback, max_length, multiline, encode, timeout) + ..(user, title, message, default, max_length, multiline, encode, timeout) + src.callback = callback + +/datum/tgui_input_text/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_input_text/async/tgui_close(mob/user) + . = ..() + qdel(src) + +/datum/tgui_input_text/async/set_entry(entry) + . = ..() + if(!isnull(src.entry)) + callback?.InvokeAsync(src.entry) + +/datum/tgui_input_text/async/wait() + return diff --git a/code/modules/vore/eating/bellymodes_vr.dm b/code/modules/vore/eating/bellymodes_vr.dm index be63fdffdc..b3cdbabaf7 100644 --- a/code/modules/vore/eating/bellymodes_vr.dm +++ b/code/modules/vore/eating/bellymodes_vr.dm @@ -157,7 +157,8 @@ formatted_message = replacetext(formatted_message, "%pred", owner) formatted_message = replacetext(formatted_message, "%prey", M) formatted_message = replacetext(formatted_message, "%countprey", absorbed_count) - to_chat(M, "[formatted_message]") + if(formatted_message) + to_chat(M, "[formatted_message]") else if(digest_mode == DM_DIGEST && !M.digestable) EL = emote_lists[DM_HOLD] // Use Hold's emote list if we're indigestible @@ -169,7 +170,8 @@ formatted_message = replacetext(formatted_message, "%prey", M) formatted_message = replacetext(formatted_message, "%countprey", living_count) formatted_message = replacetext(formatted_message, "%count", contents.len) - to_chat(M, "[formatted_message]") + if(formatted_message) + to_chat(M, "[formatted_message]") if(to_update) updateVRPanels() diff --git a/icons/inventory/head/mob_vox.dmi b/icons/inventory/head/mob_vox.dmi index 346ea9b7d1..f1570089c8 100644 Binary files a/icons/inventory/head/mob_vox.dmi and b/icons/inventory/head/mob_vox.dmi differ diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi index def737dcc3..2cb35cf667 100644 Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ diff --git a/icons/mob/head.dmi b/icons/mob/head.dmi new file mode 100644 index 0000000000..3e8d60a579 Binary files /dev/null and b/icons/mob/head.dmi differ diff --git a/icons/mob/species/teshari/head.dmi b/icons/mob/species/teshari/head.dmi index 0bb0637c4e..6cf6b2ac7d 100644 Binary files a/icons/mob/species/teshari/head.dmi and b/icons/mob/species/teshari/head.dmi differ diff --git a/icons/mob/vore/ears_vr.dmi b/icons/mob/vore/ears_vr.dmi index 032ba8e4fa..4bde02b8bf 100644 Binary files a/icons/mob/vore/ears_vr.dmi and b/icons/mob/vore/ears_vr.dmi differ diff --git a/icons/mob/vore/taurs_vr.dmi b/icons/mob/vore/taurs_vr.dmi index 4466b9a466..20ddaee2a4 100644 Binary files a/icons/mob/vore/taurs_vr.dmi and b/icons/mob/vore/taurs_vr.dmi differ diff --git a/icons/obj/cooking_machines.dmi b/icons/obj/cooking_machines.dmi index 4009cb6224..b8c3ea6f5d 100644 Binary files a/icons/obj/cooking_machines.dmi and b/icons/obj/cooking_machines.dmi differ diff --git a/icons/obj/food.dmi b/icons/obj/food.dmi index 915c170551..f1637aca4b 100644 Binary files a/icons/obj/food.dmi and b/icons/obj/food.dmi differ diff --git a/icons/obj/stacks.dmi b/icons/obj/stacks.dmi index 6a70a2982d..67a370b219 100644 Binary files a/icons/obj/stacks.dmi and b/icons/obj/stacks.dmi differ diff --git a/icons/turf/flooring/carpet.dmi b/icons/turf/flooring/carpet.dmi index f668ae8943..a66db7af48 100644 Binary files a/icons/turf/flooring/carpet.dmi and b/icons/turf/flooring/carpet.dmi differ diff --git a/icons/turf/flooring/wood.dmi b/icons/turf/flooring/wood.dmi index eb91333673..afec20c851 100644 Binary files a/icons/turf/flooring/wood.dmi and b/icons/turf/flooring/wood.dmi differ diff --git a/icons/turf/flooring/wood_vr.dmi b/icons/turf/flooring/wood_vr.dmi index d6024f3de0..f072969e54 100644 Binary files a/icons/turf/flooring/wood_vr.dmi and b/icons/turf/flooring/wood_vr.dmi differ diff --git a/maps/expedition_vr/beach/beach.dmm b/maps/expedition_vr/beach/beach.dmm index c8eda05079..1e6f8a8769 100644 --- a/maps/expedition_vr/beach/beach.dmm +++ b/maps/expedition_vr/beach/beach.dmm @@ -863,6 +863,10 @@ "nD" = ( /turf/simulated/mineral/ignore_mapgen, /area/tether_away/beach/water) +"nP" = ( +/mob/living/simple_mob/animal/passive/fennec/faux, +/turf/simulated/floor/wood, +/area/tether_away/beach/resort/kitchen) "of" = ( /obj/structure/table/woodentable, /obj/random/drinkbottle, @@ -1316,16 +1320,16 @@ /turf/simulated/floor/tiled/asteroid_steel, /area/tether_away/beach/resort/kitchen) "Bk" = ( -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, @@ -1334,10 +1338,10 @@ /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/material/knife/butch, /obj/item/weapon/material/minihoe, /obj/item/weapon/material/knife/machete/hatchet, @@ -11025,7 +11029,7 @@ ad ad ad ad -ad +nP ad ad ad diff --git a/maps/gateway_vr/eggnogtown.dmm b/maps/gateway_vr/eggnogtown.dmm index 89e98be639..2b394135f0 100644 --- a/maps/gateway_vr/eggnogtown.dmm +++ b/maps/gateway_vr/eggnogtown.dmm @@ -1000,16 +1000,16 @@ /turf/simulated/floor/tiled, /area/gateway/eggnogtown/loniabode) "gk" = ( -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, @@ -1018,10 +1018,10 @@ /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/structure/closet, /obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, /obj/item/weapon/reagent_containers/food/condiment/small/peppermill, @@ -3649,7 +3649,7 @@ "Rd" = ( /obj/structure/table/marble, /obj/item/weapon/material/kitchen/rollingpin, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/reagent_containers/food/condiment/yeast, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/yeast, diff --git a/maps/gateway_vr/variable/arynthilake_a.dmm b/maps/gateway_vr/variable/arynthilake_a.dmm index 7b0f1d4cc6..8f08ec212b 100644 --- a/maps/gateway_vr/variable/arynthilake_a.dmm +++ b/maps/gateway_vr/variable/arynthilake_a.dmm @@ -1143,16 +1143,16 @@ /obj/random/meat, /obj/random/meat, /obj/random/meat, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, diff --git a/maps/gateway_vr/variable/arynthilake_b.dmm b/maps/gateway_vr/variable/arynthilake_b.dmm index 712b51a588..5538a63096 100644 --- a/maps/gateway_vr/variable/arynthilake_b.dmm +++ b/maps/gateway_vr/variable/arynthilake_b.dmm @@ -157,16 +157,16 @@ /obj/random/meat, /obj/random/meat, /obj/random/meat, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, diff --git a/maps/gateway_vr/wildwest.dmm b/maps/gateway_vr/wildwest.dmm index 2a42bcc14f..12a79c33ec 100644 --- a/maps/gateway_vr/wildwest.dmm +++ b/maps/gateway_vr/wildwest.dmm @@ -676,9 +676,9 @@ /area/awaymission/wwmines) "fb" = ( /obj/structure/closet/secure_closet/freezer/kitchen, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /turf/simulated/floor/tiled/eris/cafe, /area/awaymission/wwmines) "fc" = ( diff --git a/maps/groundbase/gb-z2.dmm b/maps/groundbase/gb-z2.dmm index 0efdbb2a7a..40f2faadc9 100644 --- a/maps/groundbase/gb-z2.dmm +++ b/maps/groundbase/gb-z2.dmm @@ -471,7 +471,7 @@ /turf/simulated/floor/lino, /area/groundbase/civilian/chapel/office) "bl" = ( -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "bn" = ( /obj/machinery/door/airlock{ @@ -2359,7 +2359,7 @@ /obj/machinery/light{ dir = 4 }, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "gZ" = ( /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, @@ -2455,7 +2455,7 @@ /obj/structure/bed/chair/sofa/black, /obj/machinery/alarm, /obj/effect/landmark/start/chef, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "hp" = ( /obj/structure/bed/chair/sofa/pew/left{ @@ -4202,7 +4202,7 @@ /obj/machinery/light{ dir = 4 }, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "mo" = ( /obj/structure/table/woodentable, @@ -4457,7 +4457,7 @@ /area/groundbase/civilian/janitor) "nf" = ( /obj/structure/table/wooden_reinforced, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "ng" = ( /obj/machinery/camera/network/research{ @@ -6988,18 +6988,18 @@ /turf/simulated/floor/outdoors/sidewalk/slab/virgo3c, /area/groundbase/level2/northspur) "uU" = ( -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, /obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, /obj/item/weapon/reagent_containers/food/condiment/small/peppermill, @@ -10333,7 +10333,7 @@ /area/groundbase/medical/psych) "EW" = ( /obj/structure/bed/chair/sofa/right/black, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "EX" = ( /obj/structure/closet/secure_closet/quartermaster, @@ -13486,7 +13486,7 @@ /obj/structure/bed/chair/sofa/black{ dir = 8 }, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "NU" = ( /obj/structure/sign/painting/library_secure{ @@ -13583,7 +13583,7 @@ /obj/structure/bed/chair/sofa/left/black{ dir = 8 }, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "Oi" = ( /obj/structure/disposalpipe/tagger{ @@ -13605,7 +13605,7 @@ layer = 3.3; pixel_y = -27 }, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "Ok" = ( /obj/machinery/power/apc{ @@ -17276,7 +17276,7 @@ /area/groundbase/cargo/office) "Zi" = ( /obj/machinery/firealarm, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/groundbase/civilian/kitchen/backroom) "Zk" = ( /obj/item/weapon/bedsheet/browndouble, diff --git a/maps/groundbase/southwilds/villagepois/square7.dmm b/maps/groundbase/southwilds/villagepois/square7.dmm index 5cc3728a75..e92257e2a1 100644 --- a/maps/groundbase/southwilds/villagepois/square7.dmm +++ b/maps/groundbase/southwilds/villagepois/square7.dmm @@ -119,18 +119,18 @@ /turf/simulated/floor/tiled/eris/cafe, /area/submap/groundbase/poi/wildvillage/square/square7) "G" = ( -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, /obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, /obj/item/weapon/reagent_containers/food/condiment/small/peppermill, diff --git a/maps/offmap_vr/om_ships/itglight.dmm b/maps/offmap_vr/om_ships/itglight.dmm index d4cd850274..2893bca848 100644 --- a/maps/offmap_vr/om_ships/itglight.dmm +++ b/maps/offmap_vr/om_ships/itglight.dmm @@ -2014,16 +2014,16 @@ /turf/simulated/floor, /area/itglight/portengi) "lO" = ( -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/sugar, @@ -2032,10 +2032,10 @@ /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/material/knife/butch, /obj/item/weapon/material/minihoe, /obj/item/weapon/material/knife/machete/hatchet, diff --git a/maps/stellardelight/overmap.dmm b/maps/stellardelight/overmap.dmm index 2890f0159b..8edb64c502 100644 --- a/maps/stellardelight/overmap.dmm +++ b/maps/stellardelight/overmap.dmm @@ -9,7 +9,7 @@ /turf/unsimulated/map, /area/overmap) "I" = ( -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/space) "J" = ( /obj/effect/overmap/bluespace_rift, diff --git a/maps/stellardelight/stellar_delight2.dmm b/maps/stellardelight/stellar_delight2.dmm index 24a6192ea5..73422b6915 100644 --- a/maps/stellardelight/stellar_delight2.dmm +++ b/maps/stellardelight/stellar_delight2.dmm @@ -7772,18 +7772,18 @@ pixel_x = -1; pixel_y = -42 }, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /obj/item/weapon/reagent_containers/food/condiment/spacespice, /obj/item/weapon/reagent_containers/food/condiment/spacespice, -/obj/item/weapon/reagent_containers/food/condiment/sugar, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, /obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, /obj/item/weapon/reagent_containers/food/condiment/small/peppermill, diff --git a/maps/stellardelight/stellar_delight3.dmm b/maps/stellardelight/stellar_delight3.dmm index 3dff8303ce..e0a350f536 100644 --- a/maps/stellardelight/stellar_delight3.dmm +++ b/maps/stellardelight/stellar_delight3.dmm @@ -8094,7 +8094,7 @@ /obj/machinery/atmospherics/pipe/manifold/hidden/supply{ dir = 1 }, -/turf/simulated/floor/carpet/deco, +/turf/simulated/floor/carpet/geo, /area/ai) "Dy" = ( /obj/machinery/atmospherics/pipe/simple/hidden/black, diff --git a/maps/submaps/surface_submaps/plains/Diner.dmm b/maps/submaps/surface_submaps/plains/Diner.dmm index b1a74d4f0b..f52213586e 100644 --- a/maps/submaps/surface_submaps/plains/Diner.dmm +++ b/maps/submaps/surface_submaps/plains/Diner.dmm @@ -43,7 +43,7 @@ /turf/simulated/floor/tiled/white, /area/submap/diner) "ak" = ( -/obj/machinery/vending/cola, +/obj/machinery/vending/hotfood, /turf/simulated/floor/tiled/white, /area/submap/diner) "al" = ( @@ -130,7 +130,7 @@ /obj/item/weapon/reagent_containers/food/drinks/bottle/cream, /obj/item/weapon/reagent_containers/food/drinks/bottle/cream, /obj/item/weapon/reagent_containers/food/drinks/bottle/cream, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /turf/simulated/floor/tiled/white, /area/submap/diner) "az" = ( @@ -176,13 +176,13 @@ /area/submap/diner) "aI" = ( /obj/structure/closet/crate/freezer, -/obj/item/weapon/reagent_containers/food/snacks/meat, -/obj/item/weapon/reagent_containers/food/snacks/meat, -/obj/item/weapon/reagent_containers/food/snacks/meat, -/obj/item/weapon/reagent_containers/food/snacks/meat, -/obj/item/weapon/reagent_containers/food/snacks/meat, -/obj/item/weapon/reagent_containers/food/snacks/meat, -/obj/item/weapon/reagent_containers/food/snacks/meat, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, /turf/simulated/floor/tiled/freezer, /area/submap/diner) "aJ" = ( @@ -245,13 +245,13 @@ /area/submap/diner) "aQ" = ( /obj/structure/closet/crate/freezer, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /turf/simulated/floor/tiled/freezer, /area/submap/diner) "aR" = ( @@ -375,6 +375,7 @@ /area/submap/diner) "bo" = ( /obj/structure/table/woodentable, +/obj/random/mug, /turf/simulated/floor/lino, /area/submap/diner) "bp" = ( @@ -407,6 +408,7 @@ "bu" = ( /obj/structure/sink{ dir = 8; + icon_state = "sink"; pixel_x = -12; pixel_y = 2 }, @@ -872,7 +874,7 @@ aa (16,1,1) = {" aa ad -ah +bj ap ap ap diff --git a/maps/submaps/surface_submaps/plains/Diner_vr.dmm b/maps/submaps/surface_submaps/plains/Diner_vr.dmm index 67e2a712c0..6c3cc6070d 100644 --- a/maps/submaps/surface_submaps/plains/Diner_vr.dmm +++ b/maps/submaps/surface_submaps/plains/Diner_vr.dmm @@ -130,7 +130,7 @@ /obj/item/weapon/reagent_containers/food/drinks/bottle/cream, /obj/item/weapon/reagent_containers/food/drinks/bottle/cream, /obj/item/weapon/reagent_containers/food/drinks/bottle/cream, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /turf/simulated/floor/tiled/white, /area/submap/Diner) "az" = ( @@ -245,13 +245,13 @@ /area/submap/Diner) "aQ" = ( /obj/structure/closet/crate/freezer, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, -/obj/item/weapon/reagent_containers/food/condiment/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, +/obj/item/weapon/reagent_containers/food/condiment/carton/flour, /turf/simulated/floor/tiled/freezer, /area/submap/Diner) "aR" = ( diff --git a/maps/submaps/surface_submaps/plains/lonehome.dmm b/maps/submaps/surface_submaps/plains/lonehome.dmm index 229116b3b9..3331c0dc1b 100644 --- a/maps/submaps/surface_submaps/plains/lonehome.dmm +++ b/maps/submaps/surface_submaps/plains/lonehome.dmm @@ -555,7 +555,7 @@ /obj/structure/table/rack/shelf/steel, /obj/item/weapon/storage/box/donkpockets, /obj/item/weapon/storage/box/condimentbottles, -/obj/item/weapon/reagent_containers/food/condiment/sugar, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, /obj/structure/curtain/open/bed, /obj/structure/window/reinforced/polarized{ dir = 4; @@ -1054,544 +1054,3 @@ }, /turf/simulated/floor/outdoors/grass/heavy, /area/submap/lonehome) - -(1,1,1) = {" -NL -NL -NL -NL -NL -NL -yh -NL -Xz -NL -Xz -NL -NL -NL -NL -uR -NL -NL -MM -NL -NL -NL -NL -MM -NL -"} -(2,1,1) = {" -NL -NL -NL -NL -NL -Gm -NL -gn -Sp -Ek -Ek -uc -Gm -Gm -Gm -Gm -Gm -NL -NL -MM -MM -NL -MM -MM -NL -"} -(3,1,1) = {" -NL -NL -NL -yh -Gm -Gm -ND -yn -rg -rg -WQ -Gm -Gm -DL -cn -ox -po -Gm -Gm -qa -Ji -qa -jt -NL -NL -"} -(4,1,1) = {" -NL -NL -NL -Gm -Gm -Fh -wa -nD -ve -ve -yF -Gm -ru -dv -Ys -na -Ll -Gm -Kr -MM -MM -MM -VI -NL -NL -"} -(5,1,1) = {" -NL -yh -yh -yh -JW -Lv -qz -eA -Fu -Fu -oZ -Gm -gI -Tw -KM -xA -iw -Or -Fy -MM -MM -MM -pb -NL -NL -"} -(6,1,1) = {" -yh -yh -NL -Gm -Gm -Ll -iw -Lv -Dr -Dr -wa -Gm -um -SN -NK -Fu -wa -Gr -Kr -rE -MM -MM -uh -NL -NL -"} -(7,1,1) = {" -NL -yh -NL -NL -Gm -uc -wa -iw -Eo -ZL -Zg -uc -Zg -Aq -Fu -Fu -wj -hq -uI -MM -MM -MM -ZR -NL -NL -"} -(8,1,1) = {" -yh -NL -NL -NL -NL -Gm -lJ -QX -wa -Nx -Xc -Gm -aJ -iw -ks -Jq -qo -Gm -Fy -MM -MM -MM -ZR -NL -NL -"} -(9,1,1) = {" -NL -NL -aw -NL -NL -Gm -Gm -uc -JW -Gm -tp -Gm -JW -Gm -uc -Gm -Gm -Gm -Gm -uK -MM -MM -pb -NL -NL -"} -(10,1,1) = {" -NL -NL -NL -NL -fl -Gm -Od -JW -Zg -Zg -jy -bK -yv -ck -qz -qz -qz -Lv -JW -MM -Kb -MM -UA -NL -NL -"} -(11,1,1) = {" -NL -NL -NL -NL -Gm -Gm -Gm -Gm -JW -Gm -aI -uc -Gm -Gm -Gm -pX -Gm -Gm -Gm -DG -Gm -MM -VI -NL -aw -"} -(12,1,1) = {" -NL -NL -NL -NL -Gm -Kw -BS -iQ -wa -Zi -wa -ip -uC -Gm -UB -yv -Ll -aK -ld -fi -Gm -MM -pb -NL -NL -"} -(13,1,1) = {" -NL -NL -NL -NL -uc -nL -BS -kg -Zg -nT -Zg -jH -uw -uc -Im -xr -Ea -Gm -Hu -fi -Gm -MM -VI -NL -NL -"} -(14,1,1) = {" -NL -NL -NL -NL -uc -qk -TX -hH -ii -Lv -Vn -wa -Uj -Gm -kT -ro -Wf -Gm -GS -nz -uc -MM -Wn -NL -NL -"} -(15,1,1) = {" -NL -NL -NL -NL -Gm -qi -Ta -Ta -Tg -Lv -Pc -hh -lz -Gm -Ki -yr -oS -Gm -kU -ew -Gm -MM -VI -NL -NL -"} -(16,1,1) = {" -NL -NL -NL -NL -Gm -Gm -DE -US -jK -LS -GA -Mb -Gm -Gm -Gm -Gm -Gm -Gm -Gm -gC -Gm -MM -VI -NL -NL -"} -(17,1,1) = {" -NL -NL -NL -NL -NL -Gm -Dd -Mn -kn -OL -lB -hK -Gm -Hc -xO -lS -mn -QU -qE -au -uc -MM -xM -NL -NL -"} -(18,1,1) = {" -NL -aw -NL -NL -NL -Gm -yh -AQ -Qx -RI -Xz -Oe -Gm -Gm -Wj -EZ -cE -ur -YK -Gm -Gm -qa -Sd -MM -NL -"} -(19,1,1) = {" -NL -NL -NL -NL -NL -yh -NL -NL -uo -NL -NL -yh -uR -Gm -Gm -Gm -Gm -Gm -Gm -Gm -NL -NL -NL -NL -NL -"} -(20,1,1) = {" -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -NL -"} diff --git a/maps/submaps/surface_submaps/plains/lonehome_vr.dmm b/maps/submaps/surface_submaps/plains/lonehome_vr.dmm new file mode 100644 index 0000000000..6cee63decc --- /dev/null +++ b/maps/submaps/surface_submaps/plains/lonehome_vr.dmm @@ -0,0 +1,1595 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"aa" = ( +/turf/template_noop, +/area/submap/lonehome) +"ab" = ( +/turf/simulated/floor/outdoors/dirt/virgo3b, +/area/submap/lonehome) +"ac" = ( +/obj/structure/flora/tree/sif, +/turf/template_noop, +/area/submap/lonehome) +"ad" = ( +/turf/simulated/wall/wood, +/area/submap/lonehome) +"ae" = ( +/obj/structure/simple_door/wood, +/turf/simulated/floor/wood, +/area/submap/lonehome) +"af" = ( +/obj/structure/loot_pile/maint/trash, +/turf/template_noop, +/area/submap/lonehome) +"ag" = ( +/obj/structure/girder, +/turf/simulated/floor, +/area/submap/lonehome) +"ah" = ( +/obj/machinery/button/windowtint{ + id = "h_living" + }, +/turf/simulated/wall/wood, +/area/submap/lonehome) +"ai" = ( +/obj/structure/coatrack, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aj" = ( +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/broken, +/area/submap/lonehome) +"ak" = ( +/obj/machinery/space_heater, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"al" = ( +/obj/structure/closet/secure_closet/freezer/fridge, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, +/obj/random/meat, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"am" = ( +/obj/machinery/appliance/cooker/oven, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"an" = ( +/obj/structure/table/marble, +/obj/machinery/light{ + dir = 1 + }, +/obj/machinery/microwave, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"ao" = ( +/obj/structure/table/marble, +/obj/machinery/chemical_dispenser/bar_alc/full, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"ap" = ( +/obj/structure/table/bench/marble, +/obj/structure/window/reinforced/polarized{ + dir = 8; + id = "h_living" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aq" = ( +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"ar" = ( +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"as" = ( +/turf/simulated/floor/wood/broken, +/area/submap/lonehome) +"au" = ( +/obj/item/device/tape, +/obj/structure/table/wooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"av" = ( +/obj/item/weapon/extinguisher{ + pixel_x = 8; + pixel_y = 1 + }, +/obj/random/maintenance/medical, +/obj/random/maintenance/medical, +/obj/random/maintenance/medical, +/obj/random/medical/lite, +/obj/random/medical/lite, +/obj/structure/mopbucket{ + pixel_x = -8; + pixel_y = -4 + }, +/obj/item/weapon/mop, +/obj/item/device/multitool, +/obj/item/device/flashlight{ + pixel_x = 2; + pixel_y = 2 + }, +/obj/random/unidentified_medicine, +/obj/random/unidentified_medicine, +/obj/random/unidentified_medicine, +/obj/structure/table/steel, +/turf/simulated/floor/tiled, +/area/submap/lonehome) +"aw" = ( +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"ax" = ( +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"ay" = ( +/obj/item/clothing/suit/storage/apron/white, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"az" = ( +/obj/structure/window/reinforced/tinted/frosted{ + dir = 8 + }, +/obj/item/weapon/storage/box/glasses/square{ + pixel_y = -2 + }, +/obj/item/weapon/storage/box/glass_extras/sticks{ + pixel_y = 4 + }, +/obj/item/weapon/storage/box/glass_extras/straws{ + pixel_y = 7 + }, +/obj/item/weapon/packageWrap, +/obj/structure/curtain/open/bed, +/obj/item/weapon/material/kitchen/utensil/fork, +/obj/item/weapon/material/kitchen/utensil/fork, +/obj/item/weapon/material/kitchen/utensil/spoon{ + pixel_x = 2 + }, +/obj/item/weapon/material/kitchen/utensil/spoon{ + pixel_x = 2 + }, +/obj/structure/table/rack, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aA" = ( +/obj/machinery/button/windowtint{ + id = "h_kitchen" + }, +/obj/item/weapon/storage/box/donkpockets, +/obj/item/weapon/storage/box/condimentbottles, +/obj/item/weapon/reagent_containers/food/condiment/carton/sugar, +/obj/structure/curtain/open/bed, +/obj/structure/table/rack, +/obj/structure/window/reinforced/polarized{ + dir = 4; + id = "h_kitchen" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aB" = ( +/obj/structure/window/reinforced{ + dir = 4; + health = 1e+006 + }, +/turf/template_noop, +/area/submap/lonehome) +"aC" = ( +/obj/structure/window/reinforced/polarized{ + dir = 8; + id = "h_living" + }, +/obj/structure/bed/chair/sofa/corner/black{ + dir = 1 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aD" = ( +/obj/structure/bed/chair/sofa/black, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aE" = ( +/obj/structure/bed/chair/sofa/left/black, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aF" = ( +/obj/random/trash, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aG" = ( +/obj/structure/table/marble, +/obj/item/weapon/material/knife/butch, +/obj/item/weapon/material/kitchen/rollingpin, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"aH" = ( +/obj/structure/table/marble, +/obj/item/weapon/material/kitchen/utensil/spoon, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"aI" = ( +/obj/structure/table/marble, +/obj/item/weapon/material/sharpeningkit, +/obj/item/weapon/reagent_containers/dropper, +/turf/simulated/floor/tiled/white/virgo3b, +/area/submap/lonehome) +"aJ" = ( +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aK" = ( +/obj/machinery/button/windowtint{ + id = "h_kitchen" + }, +/obj/item/weapon/reagent_containers/food/condiment/enzyme, +/obj/structure/window/reinforced/polarized{ + dir = 4; + id = "h_kitchen" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aL" = ( +/obj/structure/window/reinforced{ + dir = 8; + health = 1e+006 + }, +/obj/item/weapon/material/shard, +/turf/simulated/floor/outdoors/dirt/virgo3b, +/area/submap/lonehome) +"aM" = ( +/obj/item/weapon/material/shard, +/turf/template_noop, +/area/submap/lonehome) +"aN" = ( +/obj/item/weapon/material/shard, +/obj/item/weapon/material/shard{ + pixel_x = 5; + pixel_y = 3 + }, +/turf/simulated/floor/outdoors/dirt/virgo3b, +/area/submap/lonehome) +"aO" = ( +/obj/structure/window/reinforced/polarized{ + dir = 8; + id = "h_living" + }, +/obj/structure/bed/chair/sofa/black{ + dir = 4 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aP" = ( +/obj/structure/table/bench/glass, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"aQ" = ( +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"aR" = ( +/obj/random/junk, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"aS" = ( +/obj/random/junk, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aT" = ( +/obj/item/weapon/material/kitchen/utensil/fork, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aU" = ( +/obj/structure/bed/chair/wood, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aV" = ( +/obj/structure/bed/chair/wood, +/obj/machinery/button/windowtint{ + id = "h_kitchen" + }, +/obj/structure/window/reinforced/polarized{ + dir = 4; + id = "h_kitchen" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"aW" = ( +/obj/item/weapon/material/shard{ + pixel_x = 6 + }, +/obj/item/weapon/material/shard, +/turf/simulated/floor/outdoors/dirt/virgo3b, +/area/submap/lonehome) +"aX" = ( +/obj/item/weapon/material/shard, +/turf/simulated/floor/outdoors/dirt/virgo3b, +/area/submap/lonehome) +"aY" = ( +/obj/structure/window/reinforced{ + dir = 4; + health = 1e+006 + }, +/turf/simulated/floor/outdoors/dirt/virgo3b, +/area/submap/lonehome) +"aZ" = ( +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"ba" = ( +/obj/machinery/light{ + dir = 4 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bb" = ( +/obj/machinery/light{ + dir = 8 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bc" = ( +/obj/item/stack/cable_coil, +/turf/simulated/floor/wood/broken, +/area/submap/lonehome) +"bd" = ( +/obj/item/trash/plate, +/obj/item/weapon/reagent_containers/food/condiment/small/saltshaker, +/obj/structure/table/sifwooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"be" = ( +/obj/machinery/button/windowtint{ + id = "h_kitchen" + }, +/obj/item/weapon/material/kitchen/utensil/spoon{ + pixel_x = 2 + }, +/obj/structure/table/sifwooden_reinforced, +/obj/structure/window/reinforced/polarized{ + dir = 4; + id = "h_kitchen" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bf" = ( +/obj/structure/window/reinforced{ + dir = 8; + health = 1e+006 + }, +/turf/simulated/floor/outdoors/dirt/virgo3b, +/area/submap/lonehome) +"bg" = ( +/obj/structure/window/reinforced/polarized{ + dir = 8; + id = "h_living" + }, +/obj/structure/bed/chair/sofa/corner/black{ + dir = 4 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bh" = ( +/obj/structure/bed/chair/sofa/black{ + dir = 1 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bi" = ( +/obj/structure/bed/chair/sofa/right/black{ + dir = 1 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bj" = ( +/obj/item/weapon/module/power_control, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bk" = ( +/obj/structure/flora/pottedplant/bamboo{ + pixel_y = 12 + }, +/obj/structure/table/bench/wooden, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bl" = ( +/obj/item/weapon/material/shard, +/obj/item/weapon/material/shard{ + pixel_x = 5; + pixel_y = 10 + }, +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bm" = ( +/obj/structure/window/reinforced{ + dir = 8; + health = 1e+006 + }, +/obj/structure/flora/pottedplant/fern{ + pixel_y = 12 + }, +/obj/structure/table/bench/wooden, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bn" = ( +/obj/item/weapon/reagent_containers/food/condiment/small/sugar, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/broken, +/area/submap/lonehome) +"bo" = ( +/obj/item/weapon/material/kitchen/utensil/fork, +/obj/random/trash, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bp" = ( +/obj/item/weapon/reagent_containers/food/snacks/ghostmuffin/poison, +/obj/structure/table/sifwooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bq" = ( +/obj/machinery/button/windowtint{ + id = "h_kitchen" + }, +/obj/item/trash/plate, +/obj/structure/table/sifwooden_reinforced, +/obj/structure/window/reinforced/polarized{ + dir = 4; + id = "h_kitchen" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"br" = ( +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/broken, +/area/submap/lonehome) +"bs" = ( +/obj/item/weapon/pen, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bt" = ( +/obj/item/weapon/material/kitchen/utensil/spoon{ + pixel_x = 2 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bu" = ( +/obj/item/weapon/reagent_containers/food/condiment/small/peppermill, +/obj/item/weapon/pen, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bv" = ( +/obj/structure/bed/chair/wood{ + dir = 1 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bw" = ( +/obj/structure/bed/chair/wood{ + dir = 1 + }, +/obj/structure/window/reinforced/polarized{ + dir = 4; + id = "h_kitchen" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bx" = ( +/obj/structure/window/reinforced{ + dir = 8; + health = 1e+006 + }, +/turf/template_noop, +/area/submap/lonehome) +"by" = ( +/obj/structure/table/rack, +/obj/item/weapon/makeover, +/obj/structure/curtain/open/bed, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bz" = ( +/obj/structure/table/rack, +/obj/item/weapon/material/knife/ritual, +/obj/structure/curtain/open/bed, +/obj/item/clothing/under/suit_jacket/really_black, +/obj/item/clothing/under/suit_jacket/navy, +/obj/item/clothing/head/soft/solgov/veteranhat, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bA" = ( +/obj/structure/window/reinforced/tinted/frosted{ + dir = 8 + }, +/obj/structure/table/wooden_reinforced, +/obj/item/weapon/book/tome, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bB" = ( +/obj/item/weapon/paper{ + info = "Seems to be filled with odd runes drawn with blood"; + name = "Blood stained paper" + }, +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/broken, +/area/submap/lonehome) +"bC" = ( +/obj/effect/decal/cleanable/dirt, +/obj/item/inflatable/door, +/turf/simulated/floor/wood/broken, +/area/submap/lonehome) +"bD" = ( +/obj/item/seeds/random, +/obj/item/seeds/random, +/obj/machinery/space_heater, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bE" = ( +/obj/item/seeds/random, +/obj/item/weapon/reagent_containers/spray/cleaner, +/obj/structure/table/wooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bF" = ( +/obj/machinery/light/small, +/obj/item/weapon/ore/diamond, +/obj/structure/sign/goldenplaque{ + desc = "Done No Harm."; + name = "Best Doctor 2552"; + pixel_y = -32 + }, +/obj/structure/table/wooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bG" = ( +/obj/item/weapon/reagent_containers/glass/rag, +/obj/item/clothing/gloves/ring/engagement, +/obj/structure/table/wooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bH" = ( +/obj/machinery/button/windowtint{ + id = "h_kitchen" + }, +/turf/simulated/wall/wood, +/area/submap/lonehome) +"bI" = ( +/obj/structure/closet/cabinet, +/obj/item/weapon/storage/wallet/random, +/obj/item/weapon/towel/random, +/obj/item/weapon/melee/umbrella/random, +/obj/random/carp_plushie, +/obj/random/curseditem, +/obj/random/junk, +/obj/random/maintenance/medical, +/obj/random/maintenance/medical, +/obj/random/maintenance/medical, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bJ" = ( +/obj/item/weapon/paper{ + info = "Seems to be filled with odd runes drawn with blood"; + name = "Blood stained paper" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bK" = ( +/obj/item/weapon/cell/high/empty, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"bL" = ( +/obj/structure/bed/chair/wood/wings{ + dir = 1 + }, +/obj/effect/gibspawner/human, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"bM" = ( +/obj/item/weapon/paper{ + info = "F%$K this detective, I swear I can see them peeking from behind the window, these tinted glasses help but I swear I can still see them snooping around. His days are counted... I am so close to figuring this out, just need a few more days. Then Shepiffany will be part of the family once again, then our two kids and I can go back on having a normal life... a normal life..."; + name = "Stained sheet of paper" + }, +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"bN" = ( +/obj/machinery/light/small{ + dir = 8 + }, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bO" = ( +/obj/effect/decal/cleanable/blood/gibs, +/obj/structure/kitchenspike, +/obj/effect/rune, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"bP" = ( +/obj/machinery/light{ + dir = 8 + }, +/obj/item/weapon/paper/courtroom, +/obj/item/weapon/paper/courtroom, +/obj/item/weapon/paper_bin, +/obj/structure/table/wooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bQ" = ( +/obj/item/weapon/pen/fountain, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bR" = ( +/obj/item/weapon/paper{ + info = "Seems to be filled with odd runes drawn with blood"; + name = "Blood stained paper" + }, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"bS" = ( +/obj/random/trash, +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"bT" = ( +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/carpet/sblucarpet/virgo3b, +/area/submap/lonehome) +"bU" = ( +/obj/item/weapon/photo, +/obj/item/weapon/photo{ + pixel_x = 4 + }, +/obj/structure/table/bench/wooden, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bV" = ( +/obj/item/weapon/storage/box/characters, +/obj/structure/curtain/open/bed, +/obj/structure/table/rack, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bW" = ( +/obj/structure/window/reinforced/tinted/frosted{ + dir = 8 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bX" = ( +/obj/machinery/light/small{ + dir = 1 + }, +/obj/structure/bed/padded, +/obj/item/weapon/book/custom_library/fiction/truelovehathmyheart, +/obj/item/weapon/bedsheet/clown, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bY" = ( +/obj/structure/closet/cabinet, +/obj/random/multiple, +/obj/random/multiple, +/obj/random/multiple, +/obj/random/multiple, +/obj/random/multiple, +/obj/random/toy, +/obj/random/toy, +/obj/item/weapon/storage/mre/random, +/obj/item/weapon/storage/mre/random, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"bZ" = ( +/obj/item/clothing/suit/straight_jacket, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"ca" = ( +/obj/machinery/gibber/autogibber{ + emagged = 1 + }, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cb" = ( +/obj/structure/closet/cabinet, +/obj/item/weapon/storage/wallet/random, +/obj/item/weapon/towel/random, +/obj/item/weapon/melee/umbrella/random, +/obj/random/ammo, +/obj/random/cigarettes, +/obj/random/contraband, +/obj/random/junk, +/obj/random/maintenance/security, +/obj/random/maintenance/medical, +/obj/random/maintenance/medical, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cc" = ( +/obj/effect/decal/cleanable/dirt, +/mob/living/simple_mob/animal/passive/cat/bones{ + desc = "A very odd behaved cat, their scratched name tag reads 'Felix'. They look very malnourished."; + name = "Felix" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cd" = ( +/obj/structure/bed/double/padded, +/obj/machinery/light/small{ + dir = 4 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"ce" = ( +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/carpet/bcarpet, +/area/submap/lonehome) +"cf" = ( +/obj/item/weapon/pack/cardemon, +/turf/simulated/floor/carpet/bcarpet, +/area/submap/lonehome) +"cg" = ( +/obj/item/weapon/reagent_containers/blood, +/obj/item/weapon/pack/cardemon, +/obj/item/weapon/pack/cardemon, +/obj/item/weapon/deck/tarot, +/obj/structure/table/wooden_reinforced, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"ch" = ( +/obj/item/organ/internal/liver, +/obj/random/trash, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"ci" = ( +/obj/effect/decal/cleanable/blood/gibs, +/obj/structure/table/marble, +/obj/item/organ/internal/intestine/unathi, +/obj/item/weapon/surgical/bonesetter, +/obj/item/weapon/bone/ribs, +/obj/item/weapon/bone/skull{ + pixel_x = 4; + pixel_y = 6 + }, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cj" = ( +/obj/structure/sign/periodic, +/turf/simulated/wall/wood, +/area/submap/lonehome) +"ck" = ( +/obj/item/weapon/bedsheet/rddouble, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cl" = ( +/obj/item/weapon/phone, +/obj/structure/table/bench/wooden, +/obj/item/weapon/paper{ + info = "Seems to be filled with odd runes drawn with blood"; + name = "Blood stained paper" + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cm" = ( +/obj/item/weapon/pack/cardemon, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cn" = ( +/obj/structure/bed/padded, +/obj/random/trash, +/obj/random/trash, +/obj/random/trash, +/obj/random/trash, +/obj/random/trash, +/obj/random/junk, +/obj/random/junk, +/obj/random/junk, +/obj/random/junk, +/obj/item/weapon/bedsheet/ian, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"co" = ( +/obj/structure/closet/cabinet, +/obj/random/tech_supply/component, +/obj/random/tech_supply/component, +/obj/random/tech_supply/component, +/obj/random/tech_supply/component, +/obj/random/tech_supply/component, +/obj/random/toolbox, +/obj/random/toolbox, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cp" = ( +/obj/item/clothing/mask/muzzle, +/obj/random/trash, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cq" = ( +/obj/structure/table/marble, +/obj/item/organ/internal/brain/grey, +/obj/item/weapon/material/knife/plastic, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cr" = ( +/obj/structure/table/bench/marble, +/obj/structure/window/reinforced/polarized{ + id = "h_master" + }, +/obj/structure/flora/pottedplant/overgrown{ + pixel_y = 7 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cs" = ( +/obj/structure/table/bench/marble, +/obj/structure/window/reinforced/polarized{ + id = "h_master" + }, +/obj/structure/flora/pottedplant/bamboo{ + pixel_y = 7 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"ct" = ( +/obj/structure/table/bench/marble, +/obj/structure/window/reinforced/polarized{ + id = "h_master" + }, +/obj/structure/flora/pottedplant/dead{ + pixel_y = 7 + }, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) +"cu" = ( +/obj/machinery/button/windowtint{ + id = "h_master" + }, +/turf/simulated/wall/wood, +/area/submap/lonehome) +"cv" = ( +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cw" = ( +/obj/effect/decal/cleanable/blood/gibs, +/obj/item/clothing/gloves/yellow, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cx" = ( +/obj/structure/table/marble, +/obj/item/weapon/material/knife/hook, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cy" = ( +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cz" = ( +/obj/machinery/portable_atmospherics/hydroponics/soil, +/obj/item/weapon/material/shard, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cA" = ( +/obj/structure/window/reinforced{ + dir = 1 + }, +/obj/machinery/portable_atmospherics/hydroponics/soil, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cB" = ( +/obj/machinery/portable_atmospherics/hydroponics/soil, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cC" = ( +/obj/machinery/power/port_gen/pacman, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cD" = ( +/obj/item/frame/apc, +/obj/machinery/light_switch{ + pixel_x = 11; + pixel_y = 22 + }, +/obj/random/junk, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cE" = ( +/obj/structure/table/steel, +/obj/item/stack/material/phoron{ + amount = 5 + }, +/obj/item/stack/material/phoron{ + amount = 5 + }, +/obj/fiftyspawner/wood, +/obj/fiftyspawner/steel, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cF" = ( +/obj/item/weapon/chainsaw, +/obj/item/weapon/storage/box/lights/mixed, +/obj/item/weapon/extinguisher, +/obj/item/weapon/storage/toolbox/mechanical, +/obj/structure/table/rack, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cG" = ( +/obj/effect/decal/cleanable/blood/gibs, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cH" = ( +/obj/structure/table/marble, +/obj/item/organ/internal/intestine/unathi{ + pixel_x = -1; + pixel_y = 8 + }, +/obj/item/organ/internal/lungs, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cI" = ( +/obj/structure/fence, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cJ" = ( +/obj/item/weapon/material/shard, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cK" = ( +/obj/item/weapon/material/minihoe, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cL" = ( +/obj/structure/simple_door/wood, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cM" = ( +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cN" = ( +/obj/item/device/flashlight{ + pixel_x = 2; + pixel_y = 2 + }, +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cO" = ( +/obj/machinery/light/small{ + dir = 4 + }, +/obj/effect/decal/cleanable/dirt, +/obj/effect/decal/cleanable/dirt, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cP" = ( +/obj/item/organ/internal/lungs/vox, +/obj/item/weapon/beartrap/hunting{ + anchored = 1; + deployed = 1 + }, +/obj/structure/curtain/black, +/obj/random/junk, +/obj/random/junk, +/obj/random/junk, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"cQ" = ( +/obj/structure/fence/cut/medium, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cR" = ( +/obj/structure/loot_pile/maint/trash, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cS" = ( +/obj/structure/fence/corner{ + dir = 8 + }, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cT" = ( +/obj/structure/fence{ + dir = 4 + }, +/turf/template_noop, +/area/submap/lonehome) +"cU" = ( +/obj/structure/fence/cut/large{ + dir = 8 + }, +/turf/template_noop, +/area/submap/lonehome) +"cV" = ( +/obj/structure/fence/cut/medium{ + dir = 4 + }, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cW" = ( +/obj/structure/fence{ + dir = 4 + }, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"cX" = ( +/obj/structure/fence/door/opened, +/turf/template_noop, +/area/submap/lonehome) +"cY" = ( +/obj/structure/fence/cut/medium{ + dir = 4 + }, +/turf/template_noop, +/area/submap/lonehome) +"cZ" = ( +/obj/structure/fence/cut/large{ + dir = 8 + }, +/turf/simulated/floor/outdoors/grass/sif/forest/virgo3b, +/area/submap/lonehome) +"da" = ( +/obj/structure/fence/corner, +/turf/template_noop, +/area/submap/lonehome) +"mx" = ( +/obj/structure/girder, +/turf/simulated/floor/plating/external/virgo3b, +/area/submap/lonehome) +"ES" = ( +/obj/structure/simple_door/wood, +/turf/simulated/floor/wood/virgo3b, +/area/submap/lonehome) + +(1,1,1) = {" +aa +aa +aa +aa +aa +aa +ab +aa +aM +aa +aM +aa +aa +aa +aa +aa +aa +aa +cy +aa +aa +aa +aa +cy +aa +"} +(2,1,1) = {" +aa +aa +aa +aa +aa +ad +aa +aB +aN +aY +aY +mx +ad +ad +ad +ad +ad +af +aa +cy +cy +aa +cy +cy +aa +"} +(3,1,1) = {" +aa +aa +aa +ab +ad +ah +ap +aC +aO +aO +bg +ad +ad +bI +bP +cb +cj +ad +ad +cI +cQ +cI +cS +aa +aa +"} +(4,1,1) = {" +aa +aa +aa +ad +ad +ai +aq +aD +aP +aP +bh +ad +by +bJ +bQ +cc +ak +ad +cz +cy +cy +cy +cT +aa +aa +"} +(5,1,1) = {" +aa +ab +ab +ab +ae +aj +ar +aE +aQ +aQ +bi +ad +bz +bK +bR +bS +as +cr +cA +cy +cy +cy +cU +aa +aa +"} +(6,1,1) = {" +ab +ab +aa +ad +ad +ak +as +aj +aQ +aZ +aJ +ad +bA +bL +bS +aQ +aJ +cs +cz +cJ +cy +cy +cV +aa +aa +"} +(7,1,1) = {" +aa +ab +aa +aa +ad +mx +aJ +as +aR +aZ +ar +ad +aq +bM +bT +aZ +ck +ct +cB +cy +cy +cy +cW +aa +aa +"} +(8,1,1) = {" +ab +aa +aa +aa +af +ad +au +aF +aJ +ba +bj +ad +bB +br +bU +cd +cl +cu +cA +cy +cy +cy +cW +aa +aa +"} +(9,1,1) = {" +aa +aa +ac +aa +aa +ad +ad +mx +ES +ad +bk +ad +ES +ad +ad +ad +ad +ad +ad +cK +cy +cy +cU +aa +aa +"} +(10,1,1) = {" +aa +aa +aa +aa +af +ad +av +ES +ar +ar +bl +br +bC +bN +ar +ar +aq +as +ES +cy +cR +cy +cX +aa +aa +"} +(11,1,1) = {" +aa +aa +aa +aa +ad +ad +ad +ad +ES +ad +bm +mx +ad +ad +ad +ES +ad +ad +ad +cL +ad +cy +cT +aa +ac +"} +(12,1,1) = {" +aa +aa +aa +aa +ad +al +aw +aG +aJ +bb +aq +bs +bD +ad +bV +aj +ak +cv +cC +cM +ad +cy +cU +aa +aa +"} +(13,1,1) = {" +aa +aa +aa +aa +ag +am +ax +aH +aq +bc +aq +bt +bE +mx +bW +ce +cm +ad +cD +cM +ad +cy +cT +aa +aa +"} +(14,1,1) = {" +aa +aa +aa +aa +ag +an +ay +aI +aS +as +bn +aJ +bF +ad +bX +cf +cn +ad +cE +cN +mx +cy +cY +aa +aa +"} +(15,1,1) = {" +aa +aa +aa +aa +ad +ao +ax +aw +aT +as +bo +bu +bG +ad +bY +cg +co +ad +cF +cO +ad +cy +cT +aa +aa +"} +(16,1,1) = {" +aa +aa +aa +aa +ad +ad +az +aJ +aU +bd +bp +bv +bH +ad +ad +ad +ad +ad +ad +mx +ad +cy +cT +aa +aa +"} +(17,1,1) = {" +aa +aa +aa +aa +aa +ad +aA +aK +aV +be +bq +bw +ad +bO +bZ +ch +cp +cw +cG +cP +mx +cy +cZ +aa +aa +"} +(18,1,1) = {" +aa +ac +aa +aa +aa +ad +ab +aL +aW +bf +aM +bx +ad +ad +ca +ci +cq +cx +cH +ad +ad +cI +da +cy +aa +"} +(19,1,1) = {" +aa +aa +aa +aa +aa +ab +aa +aa +aX +aa +aa +ab +aa +ad +ad +ad +ad +ad +ad +ad +aa +aa +aa +aa +aa +"} +(20,1,1) = {" +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +aa +"} diff --git a/maps/tether/tether_turfs.dm b/maps/tether/tether_turfs.dm index 6bdd1f42fe..9b73406590 100644 --- a/maps/tether/tether_turfs.dm +++ b/maps/tether/tether_turfs.dm @@ -276,6 +276,9 @@ VIRGO3B_TURF_CREATE(/turf/simulated/mineral/floor) for(var/obj/effect/step_trigger/teleporter/planetary_fall/virgo3b/F in src) qdel(F) +/turf/space/v3b_midpoint/CanZPass(atom, direction) + return 0 // We're not Space + // Tram transit floor /turf/simulated/floor/tiled/techfloor/grid/transit icon = 'icons/turf/transit_vr.dmi' diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index 5337f19f6f..7290ff0a13 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -670,7 +670,7 @@ rules: # react/sort-prop-types: error ## Enforce the state initialization style to be either in a constructor or ## with a class property - react/state-in-constructor: error + # react/state-in-constructor: error ## Enforces where React component static properties should be positioned. # react/static-property-placement: error ## Enforce style prop value being an object diff --git a/tgui/.gitignore b/tgui/.gitignore index 56cb38b290..bcdbd1c91a 100644 --- a/tgui/.gitignore +++ b/tgui/.gitignore @@ -14,6 +14,7 @@ package-lock.json ## Build artifacts /public/.tmp/**/* +/public/*.map ## Previously ignored locations that are kept to avoid confusing git ## while transitioning to a new project structure. diff --git a/tgui/babel.config.js b/tgui/babel.config.js index e2f3248066..d8ddb75721 100644 --- a/tgui/babel.config.js +++ b/tgui/babel.config.js @@ -5,7 +5,7 @@ */ const createBabelConfig = options => { - const { mode, presets = [], plugins = [] } = options; + const { presets = [], plugins = [], removeConsole } = options; return { presets: [ [require.resolve('@babel/preset-typescript'), { @@ -20,17 +20,17 @@ const createBabelConfig = options => { targets: [], }], ...presets, - ], + ].filter(Boolean), plugins: [ [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true, }], require.resolve('@babel/plugin-transform-jscript'), require.resolve('babel-plugin-inferno'), - require.resolve('babel-plugin-transform-remove-console'), + removeConsole && require.resolve('babel-plugin-transform-remove-console'), require.resolve('common/string.babel-plugin.cjs'), ...plugins, - ], + ].filter(Boolean), }; }; diff --git a/tgui/bin/tgui b/tgui/bin/tgui index dc1d05578b..256f0e579e 100644 --- a/tgui/bin/tgui +++ b/tgui/bin/tgui @@ -92,7 +92,7 @@ task-clean() { rm -f .yarn/build-state.yml rm -f .yarn/install-state.gz rm -f .yarn/install-target - rm -f .pnp.js + rm -f .pnp.* ## NPM artifacts rm -rf **/node_modules rm -f **/package-lock.json diff --git a/tgui/bin/tgui-bench.bat b/tgui/bin/tgui-bench.bat new file mode 100644 index 0000000000..da22a7b2ae --- /dev/null +++ b/tgui/bin/tgui-bench.bat @@ -0,0 +1,9 @@ +@echo off +rem Copyright (c) 2020 Aleksej Komarov +rem SPDX-License-Identifier: MIT +call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\tgui_.ps1" --bench %* +rem Pause if launched in a separate shell unless initiated from powershell +echo %PSModulePath% | findstr %USERPROFILE% >NUL +if %errorlevel% equ 0 exit 0 +echo %cmdcmdline% | find /i "/c" +if %errorlevel% equ 0 pause diff --git a/tgui/bin/tgui_.ps1 b/tgui/bin/tgui_.ps1 index 54585c6252..a1909fe70e 100644 --- a/tgui/bin/tgui_.ps1 +++ b/tgui/bin/tgui_.ps1 @@ -50,6 +50,10 @@ function task-dev-server { yarn node --experimental-modules "packages/tgui-dev-server/index.js" @Args } +function task-bench { + yarn tgui:bench @Args +} + ## Run a linter through all packages function task-lint { yarn run tsc @@ -75,7 +79,7 @@ function task-clean { Remove-Quiet -Force ".yarn\build-state.yml" Remove-Quiet -Force ".yarn\install-state.gz" Remove-Quiet -Force ".yarn\install-target" - Remove-Quiet -Force ".pnp.js" + Remove-Quiet -Force ".pnp.*" ## NPM artifacts Get-ChildItem -Path "." -Include "node_modules" -Recurse -File:$false | Remove-Item -Recurse -Force Remove-Quiet -Force "package-lock.json" @@ -132,6 +136,13 @@ if ($Args.Length -gt 0) { task-webpack --mode=production --analyze exit 0 } + + if ($Args[0] -eq "--bench") { + $Rest = $Args | Select-Object -Skip 1 + task-install + task-bench --wait-on-error + exit 0 + } } ## Make a production webpack build diff --git a/tgui/global.d.ts b/tgui/global.d.ts index aa8495a249..0f69e37153 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -4,184 +4,180 @@ * @license MIT */ -declare global { - // Webpack asset modules. - // Should match extensions used in webpack config. - declare module '*.png' { - const content: string; - export default content; - } - - declare module '*.jpg' { - const content: string; - export default content; - } - - declare module '*.svg' { - const content: string; - export default content; - } - - type TguiMessage = { - type: string; - payload?: any; - [key: string]: any; - }; - - type ByondType = { - /** - * ID of the Byond window this script is running on. - * Can be used as a parameter to winget/winset. - */ - windowId: string; - - /** - * True if javascript is running in BYOND. - */ - IS_BYOND: boolean; - - /** - * Version of Trident engine of Internet Explorer. Null if N/A. - */ - TRIDENT: number | null; - - /** - * True if browser is IE8 or lower. - */ - IS_LTE_IE8: boolean; - - /** - * True if browser is IE9 or lower. - */ - IS_LTE_IE9: boolean; - - /** - * True if browser is IE10 or lower. - */ - IS_LTE_IE10: boolean; - - /** - * True if browser is IE11 or lower. - */ - IS_LTE_IE11: boolean; - - /** - * Makes a BYOND call. - * - * If path is empty, this will trigger a Topic call. - * You can reference a specific object by setting the "src" parameter. - * - * See: https://secure.byond.com/docs/ref/skinparams.html - */ - call(path: string, params: object): void; - - /** - * Makes an asynchronous BYOND call. Returns a promise. - */ - callAsync(path: string, params: object): Promise; - - /** - * Makes a Topic call. - * - * You can reference a specific object by setting the "src" parameter. - */ - topic(params: object): void; - - /** - * Runs a command or a verb. - */ - command(command: string): void; - - /** - * Retrieves all properties of the BYOND skin element. - * - * Returns a promise with a key-value object containing all properties. - */ - winget(id: string | null): Promise; - - /** - * Retrieves all properties of the BYOND skin element. - * - * Returns a promise with a key-value object containing all properties. - */ - winget(id: string | null, propName: '*'): Promise; - - /** - * Retrieves an exactly one property of the BYOND skin element, - * as defined in `propName`. - * - * Returns a promise with the value of that property. - */ - winget(id: string | null, propName: string): Promise; - - /** - * Retrieves multiple properties of the BYOND skin element, - * as defined in the `propNames` array. - * - * Returns a promise with a key-value object containing listed properties. - */ - winget(id: string | null, propNames: string[]): Promise; - - /** - * Assigns properties to BYOND skin elements in bulk. - */ - winset(props: object): void; - - /** - * Assigns properties to the BYOND skin element. - */ - winset(id: string | null, props: object): void; - - /** - * Sets a property on the BYOND skin element to a certain value. - */ - winset(id: string | null, propName: string, propValue: any): void; - - /** - * Parses BYOND JSON. - * - * Uses a special encoding to preserve `Infinity` and `NaN`. - */ - parseJson(text: string): any; - - /** - * Sends a message to `/datum/tgui_window` which hosts this window instance. - */ - sendMessage(type: string, payload?: any): void; - sendMessage(message: TguiMessage): void; - - /** - * Subscribe to incoming messages that were sent from `/datum/tgui_window`. - */ - subscribe(listener: (type: string, payload: any) => void): void; - - /** - * Subscribe to incoming messages *of some specific type* - * that were sent from `/datum/tgui_window`. - */ - subscribeTo(type: string, listener: (payload: any) => void): void; - - /** - * Loads a stylesheet into the document. - */ - loadCss(url: string): void; - - /** - * Loads a script into the document. - */ - loadJs(url: string): void; - }; - - /** - * Object that provides access to Byond Skin API and is available in - * any tgui application. - */ - const Byond: ByondType; - - interface Window { - Byond: ByondType; - } - +// Webpack asset modules. +// Should match extensions used in webpack config. +declare module '*.png' { + const content: string; + export default content; } -export {}; +declare module '*.jpg' { + const content: string; + export default content; +} + +declare module '*.svg' { + const content: string; + export default content; +} + +type TguiMessage = { + type: string; + payload?: any; + [key: string]: any; +}; + + +type ByondType = { + /** + * ID of the Byond window this script is running on. + * Can be used as a parameter to winget/winset. + */ + windowId: string; + + /** + * True if javascript is running in BYOND. + */ + IS_BYOND: boolean; + + /** + * Version of Trident engine of Internet Explorer. Null if N/A. + */ + TRIDENT: number | null; + + /** + * True if browser is IE8 or lower. + */ + IS_LTE_IE8: boolean; + + /** + * True if browser is IE9 or lower. + */ + IS_LTE_IE9: boolean; + + /** + * True if browser is IE10 or lower. + */ + IS_LTE_IE10: boolean; + + /** + * True if browser is IE11 or lower. + */ + IS_LTE_IE11: boolean; + + /** + * Makes a BYOND call. + * + * If path is empty, this will trigger a Topic call. + * You can reference a specific object by setting the "src" parameter. + * + * See: https://secure.byond.com/docs/ref/skinparams.html + */ + call(path: string, params: object): void; + + /** + * Makes an asynchronous BYOND call. Returns a promise. + */ + callAsync(path: string, params: object): Promise; + + /** + * Makes a Topic call. + * + * You can reference a specific object by setting the "src" parameter. + */ + topic(params: object): void; + + /** + * Runs a command or a verb. + */ + command(command: string): void; + + /** + * Retrieves all properties of the BYOND skin element. + * + * Returns a promise with a key-value object containing all properties. + */ + winget(id: string | null): Promise; + + /** + * Retrieves all properties of the BYOND skin element. + * + * Returns a promise with a key-value object containing all properties. + */ + winget(id: string | null, propName: '*'): Promise; + + /** + * Retrieves an exactly one property of the BYOND skin element, + * as defined in `propName`. + * + * Returns a promise with the value of that property. + */ + winget(id: string | null, propName: string): Promise; + + /** + * Retrieves multiple properties of the BYOND skin element, + * as defined in the `propNames` array. + * + * Returns a promise with a key-value object containing listed properties. + */ + winget(id: string | null, propNames: string[]): Promise; + + /** + * Assigns properties to BYOND skin elements in bulk. + */ + winset(props: object): void; + + /** + * Assigns properties to the BYOND skin element. + */ + winset(id: string | null, props: object): void; + + /** + * Sets a property on the BYOND skin element to a certain value. + */ + winset(id: string | null, propName: string, propValue: any): void; + + /** + * Parses BYOND JSON. + * + * Uses a special encoding to preserve `Infinity` and `NaN`. + */ + parseJson(text: string): any; + + /** + * Sends a message to `/datum/tgui_window` which hosts this window instance. + */ + sendMessage(type: string, payload?: any): void; + sendMessage(message: TguiMessage): void; + + /** + * Subscribe to incoming messages that were sent from `/datum/tgui_window`. + */ + subscribe(listener: (type: string, payload: any) => void): void; + + /** + * Subscribe to incoming messages *of some specific type* + * that were sent from `/datum/tgui_window`. + */ + subscribeTo(type: string, listener: (payload: any) => void): void; + + /** + * Loads a stylesheet into the document. + */ + loadCss(url: string): void; + + /** + * Loads a script into the document. + */ + loadJs(url: string): void; +}; + +/** + * Object that provides access to Byond Skin API and is available in + * any tgui application. + */ +const Byond: ByondType; + +interface Window { + Byond: ByondType; +} diff --git a/tgui/jest.config.js b/tgui/jest.config.js index 5802332817..e654f0089b 100644 --- a/tgui/jest.config.js +++ b/tgui/jest.config.js @@ -4,6 +4,9 @@ module.exports = { '/packages/**/__tests__/*.{js,ts,tsx}', '/packages/**/*.{spec,test}.{js,ts,tsx}', ], + testPathIgnorePatterns: [ + '/packages/tgui-bench', + ], testEnvironment: 'jsdom', testRunner: require.resolve('jest-circus/runner'), transform: { diff --git a/tgui/package.json b/tgui/package.json index cdaea68107..7fc6d67cd2 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -6,6 +6,18 @@ "workspaces": [ "packages/*" ], + "scripts": { + "tgui:build": "webpack", + "tgui:analyze": "webpack --analyze", + "tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js", + "tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx", + "tgui:sonar": "eslint packages --ext .js,.cjs,.ts,.tsx -c .eslintrc-harder.yml", + "tgui:tsc": "tsc", + "tgui:test": "jest --watch", + "tgui:test-simple": "CI=true jest --color", + "tgui:test-ci": "CI=true jest --color --collect-coverage", + "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js" + }, "dependencies": { "@babel/core": "^7.18.0", "@babel/eslint-parser": "^7.17.0", @@ -16,6 +28,8 @@ "@types/jest": "^27.5.1", "@types/jsdom": "^16.2.14", "@types/node": "^17.0.35", + "@types/webpack": "^5.28.0", + "@types/webpack-env": "^1.17.0", "@typescript-eslint/parser": "^5.25.0", "babel-jest": "^28.1.0", "babel-loader": "^8.2.5", diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 7abe49ff23..f0a8bfeba3 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -4,64 +4,6 @@ * @license MIT */ -/** - * Converts a given collection to an array. - * - * - Arrays are returned unmodified; - * - If object was provided, keys will be discarded; - * - Everything else will result in an empty array. - * - * @returns {any[]} - */ -export const toArray = collection => { - if (Array.isArray(collection)) { - return collection; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(collection[i]); - } - } - return result; - } - return []; -}; - -/** - * Converts a given object to an array, and appends a key to every - * object inside of that array. - * - * Example input (object): - * ``` - * { - * 'Foo': { info: 'Hello world!' }, - * 'Bar': { info: 'Hello world!' }, - * } - * ``` - * - * Example output (array): - * ``` - * [ - * { key: 'Foo', info: 'Hello world!' }, - * { key: 'Bar', info: 'Hello world!' }, - * ] - * ``` - * - * @template T - * @param {{ [key: string]: T }} obj Object, or in DM terms, an assoc array - * @param {string} keyProp Property, to which key will be assigned - * @returns {T[]} Array of keyed objects - */ -export const toKeyedArray = (obj, keyProp = 'key') => { - return map((item, key) => ({ - [keyProp]: key, - ...item, - }))(obj); -}; - /** * Iterates over elements of collection, returning an array of all elements * iteratee returns truthy for. The predicate is invoked with three @@ -72,21 +14,40 @@ export const toKeyedArray = (obj, keyProp = 'key') => { * * @returns {any[]} */ -export const filter = iterateeFn => collection => { - if (collection === null || collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - const item = collection[i]; - if (iterateeFn(item, i, collection)) { - result.push(item); +export const filter = (iterateeFn: ( + input: T, + index: number, + collection: T[], +) => boolean) => + (collection: T[]): T[] => { + if (collection === null || collection === undefined) { + return collection; } - } - return result; - } - throw new Error(`filter() can't iterate on type ${typeof collection}`); + if (Array.isArray(collection)) { + const result: T[] = []; + for (let i = 0; i < collection.length; i++) { + const item = collection[i]; + if (iterateeFn(item, i, collection)) { + result.push(item); + } + } + return result; + } + throw new Error(`filter() can't iterate on type ${typeof collection}`); + }; + +type MapFunction = { + (iterateeFn: ( + value: T, + index: number, + collection: T[], + ) => U): (collection: T[]) => U[]; + + (iterateeFn: ( + value: T, + index: K, + collection: Record, + ) => U): (collection: Record) => U[]; }; /** @@ -96,32 +57,25 @@ export const filter = iterateeFn => collection => { * * If collection is 'null' or 'undefined', it will be returned "as is" * without emitting any errors (which can be useful in some cases). - * - * @returns {any[]} */ -export const map = iterateeFn => collection => { - if (collection === null || collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result = []; - for (let i = 0; i < collection.length; i++) { - result.push(iterateeFn(collection[i], i, collection)); +export const map: MapFunction = (iterateeFn) => + (collection: T[]): U[] => { + if (collection === null || collection === undefined) { + return collection; } - return result; - } - if (typeof collection === 'object') { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const result = []; - for (let i in collection) { - if (hasOwnProperty.call(collection, i)) { - result.push(iterateeFn(collection[i], i, collection)); - } + + if (Array.isArray(collection)) { + return collection.map(iterateeFn); } - return result; - } - throw new Error(`map() can't iterate on type ${typeof collection}`); -}; + + if (typeof collection === 'object') { + return Object.entries(collection).map(([key, value]) => { + return iterateeFn(value, key, collection); + }); + } + + throw new Error(`map() can't iterate on type ${typeof collection}`); + }; const COMPARATOR = (objA, objB) => { const criteriaA = objA.criteria; @@ -148,28 +102,35 @@ const COMPARATOR = (objA, objB) => { * * @returns {any[]} */ -export const sortBy = (...iterateeFns) => array => { - if (!Array.isArray(array)) { - return array; - } - let length = array.length; - // Iterate over the array to collect criteria to sort it by - let mappedArray = []; - for (let i = 0; i < length; i++) { - const value = array[i]; - mappedArray.push({ - criteria: iterateeFns.map(fn => fn(value)), - value, - }); - } - // Sort criteria using the base comparator - mappedArray.sort(COMPARATOR); - // Unwrap values - while (length--) { - mappedArray[length] = mappedArray[length].value; - } - return mappedArray; -}; +export const sortBy = ( + ...iterateeFns: ((input: T) => unknown)[] +) => (array: T[]): T[] => { + if (!Array.isArray(array)) { + return array; + } + let length = array.length; + // Iterate over the array to collect criteria to sort it by + let mappedArray: { + criteria: unknown[], + value: T, + }[] = []; + for (let i = 0; i < length; i++) { + const value = array[i]; + mappedArray.push({ + criteria: iterateeFns.map(fn => fn(value)), + value, + }); + } + // Sort criteria using the base comparator + mappedArray.sort(COMPARATOR); + + // Unwrap values + const values: T[] = []; + while (length--) { + values[length] = mappedArray[length].value; + } + return values; + }; export const sort = sortBy(); @@ -212,40 +173,38 @@ export const reduce = (reducerFn, initialValue) => array => { * is determined by the order they occur in the array. The iteratee is * invoked with one argument: value. */ -/* eslint-disable indent */ export const uniqBy = ( iterateeFn?: (value: T) => unknown -) => (array: T[]) => { - const { length } = array; - const result = []; - const seen = iterateeFn ? [] : result; - let index = -1; - outer: - while (++index < length) { - let value: T | 0 = array[index]; - const computed = iterateeFn ? iterateeFn(value) : value; - value = value !== 0 ? value : 0; - if (computed === computed) { - let seenIndex = seen.length; - while (seenIndex--) { - if (seen[seenIndex] === computed) { - continue outer; +) => (array: T[]): T[] => { + const { length } = array; + const result: T[] = []; + const seen: unknown[] = iterateeFn ? [] : result; + let index = -1; + outer: + while (++index < length) { + let value: T | 0 = array[index]; + const computed = iterateeFn ? iterateeFn(value) : value; + if (computed === computed) { + let seenIndex = seen.length; + while (seenIndex--) { + if (seen[seenIndex] === computed) { + continue outer; + } } + if (iterateeFn) { + seen.push(computed); + } + result.push(value); } - if (iterateeFn) { - seen.push(computed); + else if (!seen.includes(computed)) { + if (seen !== result) { + seen.push(computed); + } + result.push(value); } - result.push(value); } - else if (!seen.includes(computed)) { - if (seen !== result) { - seen.push(computed); - } - result.push(value); - } - } - return result; -}; + return result; + }; /* eslint-enable indent */ export const uniq = uniqBy(); @@ -261,17 +220,19 @@ type Zip = { */ export const zip = (...arrays: T): Zip => { if (arrays.length === 0) { - return; + return []; } const numArrays = arrays.length; const numValues = arrays[0].length; - const result = []; + const result: Zip = []; for (let valueIndex = 0; valueIndex < numValues; valueIndex++) { - const entry = []; + const entry: unknown[] = []; for (let arrayIndex = 0; arrayIndex < numArrays; arrayIndex++) { entry.push(arrays[arrayIndex][valueIndex]); } - result.push(entry); + + // I tried everything to remove this any, and have no idea how to do it. + result.push(entry as any); } return result; }; @@ -280,9 +241,8 @@ export const zip = (...arrays: T): Zip => { * This method is like "zip" except that it accepts iteratee to * specify how grouped values should be combined. The iteratee is * invoked with the elements of each group. - * - * @returns {any[]} */ -export const zipWith = iterateeFn => (...arrays) => { - return map(values => iterateeFn(...values))(zip(...arrays)); -}; +export const zipWith = (iterateeFn: (...values: T[]) => U) => + (...arrays: T[][]): U[] => { + return map((values: T[]) => iterateeFn(...values))(zip(...arrays)); + }; diff --git a/tgui/packages/common/types.ts b/tgui/packages/common/types.ts new file mode 100644 index 0000000000..a92ac122d9 --- /dev/null +++ b/tgui/packages/common/types.ts @@ -0,0 +1,5 @@ +/** + * Returns the arguments of a function F as an array. + */ +export type ArgumentsOf + = F extends (...args: infer A) => unknown ? A : never; diff --git a/tgui/packages/tgfont/dist/tgfont.css b/tgui/packages/tgfont/dist/tgfont.css index 6295dcce8f..ad732b8fc0 100644 --- a/tgui/packages/tgfont/dist/tgfont.css +++ b/tgui/packages/tgfont/dist/tgfont.css @@ -1,7 +1,7 @@ @font-face { font-family: "tgfont"; - src: url("./tgfont.woff2?8fcc44d209cc0a286e2fedc5edea15e7") format("woff2"), -url("./tgfont.eot?8fcc44d209cc0a286e2fedc5edea15e7#iefix") format("embedded-opentype"); + src: url("./tgfont.woff2?45c3c7acc69dd413375d77898d24e41e") format("woff2"), +url("./tgfont.eot?45c3c7acc69dd413375d77898d24e41e#iefix") format("embedded-opentype"); } i[class^="tg-"]:before, i[class*=" tg-"]:before { @@ -21,21 +21,30 @@ i[class^="tg-"]:before, i[class*=" tg-"]:before { .tg-air-tank:before { content: "\f102"; } -.tg-image-minus:before { +.tg-bad-touch:before { content: "\f103"; } -.tg-image-plus:before { +.tg-image-minus:before { content: "\f104"; } -.tg-nanotrasen-logo:before { +.tg-image-plus:before { content: "\f105"; } -.tg-sound-minus:before { +.tg-nanotrasen-logo:before { content: "\f106"; } -.tg-sound-plus:before { +.tg-non-binary:before { content: "\f107"; } -.tg-syndicate-logo:before { +.tg-prosthetic-leg:before { content: "\f108"; } +.tg-sound-minus:before { + content: "\f109"; +} +.tg-sound-plus:before { + content: "\f10a"; +} +.tg-syndicate-logo:before { + content: "\f10b"; +} diff --git a/tgui/packages/tgfont/dist/tgfont.eot b/tgui/packages/tgfont/dist/tgfont.eot index a1ffb10df4..ba307e0672 100644 Binary files a/tgui/packages/tgfont/dist/tgfont.eot and b/tgui/packages/tgfont/dist/tgfont.eot differ diff --git a/tgui/packages/tgfont/dist/tgfont.woff2 b/tgui/packages/tgfont/dist/tgfont.woff2 index 4e2dae731a..3f54573899 100644 Binary files a/tgui/packages/tgfont/dist/tgfont.woff2 and b/tgui/packages/tgfont/dist/tgfont.woff2 differ diff --git a/tgui/packages/tgfont/icons/ATTRIBUTIONS.md b/tgui/packages/tgfont/icons/ATTRIBUTIONS.md new file mode 100644 index 0000000000..2f218388d3 --- /dev/null +++ b/tgui/packages/tgfont/icons/ATTRIBUTIONS.md @@ -0,0 +1,6 @@ +bad-touch.svg contains: +- hug by Phạm Thanh Lộc from the Noun Project +- Fight by Rudez Studio from the Noun Project + +prosthetic-leg.svg contains: +- prosthetic leg by Gan Khoon Lay from the Noun Project diff --git a/tgui/packages/tgfont/icons/bad-touch.svg b/tgui/packages/tgfont/icons/bad-touch.svg new file mode 100644 index 0000000000..a2566c1670 --- /dev/null +++ b/tgui/packages/tgfont/icons/bad-touch.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/tgui/packages/tgfont/icons/non-binary.svg b/tgui/packages/tgfont/icons/non-binary.svg new file mode 100644 index 0000000000..9aaec674bb --- /dev/null +++ b/tgui/packages/tgfont/icons/non-binary.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/tgui/packages/tgfont/icons/prosthetic-leg.svg b/tgui/packages/tgfont/icons/prosthetic-leg.svg new file mode 100644 index 0000000000..c1f6ceee3f --- /dev/null +++ b/tgui/packages/tgfont/icons/prosthetic-leg.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/tgui/packages/tgfont/package.json b/tgui/packages/tgfont/package.json index 90c38f82db..5e5a3f060c 100644 --- a/tgui/packages/tgfont/package.json +++ b/tgui/packages/tgfont/package.json @@ -2,10 +2,10 @@ "private": true, "name": "tgfont", "version": "1.0.0", - "dependencies": { - "fantasticon": "^1.2.3" - }, "scripts": { "build": "node mkdist.cjs && fantasticon --config config.cjs" + }, + "dependencies": { + "fantasticon": "^1.2.3" } } diff --git a/tgui/packages/tgui-bench/entrypoint.tsx b/tgui/packages/tgui-bench/entrypoint.tsx new file mode 100644 index 0000000000..d41d667894 --- /dev/null +++ b/tgui/packages/tgui-bench/entrypoint.tsx @@ -0,0 +1,73 @@ +/** + * @file + * @copyright 2021 Aleksej Komarov + * @license MIT + */ + +import { setupGlobalEvents } from 'tgui/events'; +import 'tgui/styles/main.scss'; +import Benchmark from './lib/benchmark'; + +const sendMessage = (obj: any) => { + const req = new XMLHttpRequest(); + req.open('POST', `/message`, false); + req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + // req.timeout = 250; + req.send(JSON.stringify(obj)); +}; + +const setupApp = async () => { + // Delay setup + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupApp); + return; + } + + setupGlobalEvents({ + ignoreWindowFocus: true, + }); + + const requireTest = require.context('./tests', false, /\.test\./); + + for (const file of requireTest.keys()) { + sendMessage({ type: 'suite-start', file }); + try { + const tests = requireTest(file); + await new Promise((resolve) => { + const suite = new Benchmark.Suite(file, { + onCycle(e) { + sendMessage({ + type: 'suite-cycle', + message: String(e.target), + }); + }, + onComplete() { + // This message is somewhat useless, but leaving it here in case + // someone has an idea how to show more useful data. + // sendMessage({ + // type: 'suite-complete', + // message: 'Fastest is ' + this.filter('fastest').map('name'), + // }); + resolve(); + }, + onError(e) { + sendMessage({ type: 'error', e }); + resolve(); + }, + }); + for (const [name, fn] of Object.entries(tests)) { + if (typeof fn === 'function') { + suite.add(name, fn); + } + } + suite.run(); + }); + } + catch (error) { + sendMessage({ type: 'error', error }); + } + } + sendMessage({ type: 'finished' }); +}; + +setupApp(); diff --git a/tgui/packages/tgui-bench/index.js b/tgui/packages/tgui-bench/index.js new file mode 100644 index 0000000000..e29ad9a5c2 --- /dev/null +++ b/tgui/packages/tgui-bench/index.js @@ -0,0 +1,93 @@ +/** + * @file + * @copyright 2021 Aleksej Komarov + * @license MIT + */ + +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { fastify } = require('fastify'); + +process.chdir(__dirname); + +const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time)); + +const IE_TIMEOUT_SECONDS = 60; + +const setup = async () => { + const server = fastify(); + + let hasResponded = false; + + let assets = ''; + assets += `\n`; + + const publicDir = path.resolve(__dirname, '../../public'); + const page = fs.readFileSync(path.join(publicDir, 'tgui.html'), 'utf-8') + .replace('\n', assets); + + server.register(require('@fastify/static'), { + root: publicDir, + }); + + server.get('/', async (req, res) => { + return res.type('text/html').send(page); + }); + + server.post('/message', async (req, res) => { + if (!hasResponded) { + process.stdout.write('\n'); + hasResponded = true; + } + const { type, ...rest } = req.body; + if (type === 'suite-start') { + console.log(`=> Test '${rest.file}'`); + return res.send(); + } + if (type === 'suite-cycle') { + console.log(rest.message); + return res.send(); + } + if (type === 'suite-complete') { + console.log(rest.message); + return res.send(); + } + if (type === 'finished') { + await res.send(); + process.exit(0); + } + // Unhandled message + console.log(req.body); + return res.send(); + }); + + try { + await server.listen(3002, '0.0.0.0'); + } + catch (err) { + console.error(err); + process.exit(1); + } + + if (process.platform === 'win32') { + exec(`start "" "iexplore" "http://127.0.0.1:3002"`); + } + + console.log('Waiting for Internet Explorer to respond.'); + for (let i = 0; i < IE_TIMEOUT_SECONDS; i++) { + await sleep(1000); + if (hasResponded) { + return; + } + process.stdout.write('.'); + } + process.stdout.write('\n'); + console.error('Did not receive a response, exiting.'); + process.exit(1); +}; + +setup(); diff --git a/tgui/packages/tgui-bench/lib/benchmark.d.ts b/tgui/packages/tgui-bench/lib/benchmark.d.ts new file mode 100644 index 0000000000..7f3310005f --- /dev/null +++ b/tgui/packages/tgui-bench/lib/benchmark.d.ts @@ -0,0 +1,219 @@ +/* eslint-disable */ +// Type definitions for Benchmark v2.1.4 +// Project: https://benchmarkjs.com +// Definitions by: Asana +// Charlie Fish +// Blair Zajac +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare class Benchmark { + static filter(arr: T[], callback: (value: T) => any, thisArg?: any): T[]; + static filter(arr: T[], filter: string, thisArg?: any): T[]; + static formatNumber(num: number): string; + static join(obj: Object, separator1?: string, separator2?: string): string; + static invoke( + benches: Benchmark[], + name: string | Object, + ...args: any[] + ): any[]; + static runInContext(context: Object): Function; + + static each(obj: Object | any[], callback: Function, thisArg?: any): void; + static forEach(arr: T[], callback: (value: T) => any, thisArg?: any): void; + static forOwn(obj: Object, callback: Function, thisArg?: any): void; + static has(obj: Object, path: any[] | string): boolean; + static indexOf(arr: T[], value: T, fromIndex?: number): number; + static map(arr: T[], callback: (value: T) => K, thisArg?: any): K[]; + static reduce( + arr: T[], + callback: (accumulator: K, value: T) => K, + thisArg?: any + ): K; + + static options: Benchmark.Options; + static platform: Benchmark.Platform; + static support: Benchmark.Support; + static version: string; + + constructor(fn: Function | string, options?: Benchmark.Options); + constructor(name: string, fn: Function | string, options?: Benchmark.Options); + constructor(name: string, options?: Benchmark.Options); + constructor(options: Benchmark.Options); + + id: number; + name: string; + count: number; + cycles: number; + hz: number; + compiled: Function | string; + error: Error; + fn: Function | string; + aborted: boolean; + running: boolean; + setup: Function | string; + teardown: Function | string; + + stats: Benchmark.Stats; + times: Benchmark.Times; + + abort(): Benchmark; + clone(options: Benchmark.Options): Benchmark; + compare(benchmark: Benchmark): number; + emit(type: string | Object): any; + listeners(type: string): Function[]; + off(type?: string, listener?: Function): Benchmark; + off(types: string[]): Benchmark; + on(type?: string, listener?: Function): Benchmark; + on(types: string[]): Benchmark; + reset(): Benchmark; + run(options?: Benchmark.Options): Benchmark; + toString(): string; +} + +declare namespace Benchmark { + export interface Options { + async?: boolean | undefined; + defer?: boolean | undefined; + delay?: number | undefined; + id?: string | undefined; + initCount?: number | undefined; + maxTime?: number | undefined; + minSamples?: number | undefined; + minTime?: number | undefined; + name?: string | undefined; + onAbort?: Function | undefined; + onComplete?: Function | undefined; + onCycle?: Function | undefined; + onError?: Function | undefined; + onReset?: Function | undefined; + onStart?: Function | undefined; + setup?: Function | string | undefined; + teardown?: Function | string | undefined; + fn?: Function | string | undefined; + queued?: boolean | undefined; + } + + export interface Platform { + description: string; + layout: string; + product: string; + name: string; + manufacturer: string; + os: string; + prerelease: string; + version: string; + toString(): string; + } + + export interface Support { + browser: boolean; + timeout: boolean; + decompilation: boolean; + } + + export interface Stats { + moe: number; + rme: number; + sem: number; + deviation: number; + mean: number; + sample: any[]; + variance: number; + } + + export interface Times { + cycle: number; + elapsed: number; + period: number; + timeStamp: number; + } + + export class Deferred { + constructor(clone: Benchmark); + + benchmark: Benchmark; + cycles: number; + elapsed: number; + timeStamp: number; + + resolve(): void; + } + + export interface Target { + options: Options; + async?: boolean | undefined; + defer?: boolean | undefined; + delay?: number | undefined; + initCount?: number | undefined; + maxTime?: number | undefined; + minSamples?: number | undefined; + minTime?: number | undefined; + name?: string | undefined; + fn?: Function | undefined; + id: number; + stats?: Stats | undefined; + times?: Times | undefined; + running: boolean; + count?: number | undefined; + compiled?: Function | undefined; + cycles?: number | undefined; + hz?: number | undefined; + } + + export class Event { + constructor(type: string | Object); + + aborted: boolean; + cancelled: boolean; + currentTarget: Object; + result: any; + target: Target; + timeStamp: number; + type: string; + } + + export class Suite { + static options: { name: string }; + + constructor(name?: string, options?: Options); + + length: number; + aborted: boolean; + running: boolean; + + abort(): Suite; + add(name: string, fn: Function | string, options?: Options): Suite; + add(fn: Function | string, options?: Options): Suite; + add(name: string, options?: Options): Suite; + add(options: Options): Suite; + clone(options: Options): Suite; + emit(type: string | Object): any; + filter(callback: Function | string): Suite; + join(separator?: string): string; + listeners(type: string): Function[]; + off(type?: string, callback?: Function): Suite; + off(types: string[]): Suite; + on(type?: string, callback?: Function): Suite; + on(types: string[]): Suite; + push(benchmark: Benchmark): number; + reset(): Suite; + run(options?: Options): Suite; + reverse(): any[]; + sort(compareFn: (a: any, b: any) => number): any[]; + splice(start: number, deleteCount?: number): any[]; + unshift(benchmark: Benchmark): number; + + each(callback: Function): Suite; + forEach(callback: Function): Suite; + indexOf(value: any): number; + map(callback: Function | string): any[]; + reduce(callback: Function, accumulator: T): T; + + pop(): Function; + shift(): Benchmark; + slice(start: number, end: number): any[]; + slice(start: number, deleteCount: number, ...values: any[]): any[]; + } +} + +export = Benchmark; diff --git a/tgui/packages/tgui-bench/lib/benchmark.js b/tgui/packages/tgui-bench/lib/benchmark.js new file mode 100644 index 0000000000..1e76cec708 --- /dev/null +++ b/tgui/packages/tgui-bench/lib/benchmark.js @@ -0,0 +1,2759 @@ +/* eslint-disable */ +/*! + * Benchmark.js + * Copyright 2010-2016 Mathias Bynens + * Based on JSLitmus.js, copyright Robert Kieffer + * Modified by John-David Dalton + * Manually stripped from useless junk by /tg/station13 maintainers. + * Available under MIT license + */ +module.exports = (function() { + 'use strict'; + + /** Used as a safe reference for `undefined` in pre ES5 environments. */ + var undefined; + + /** Used to determine if values are of the language type Object. */ + var objectTypes = { + 'function': true, + 'object': true + }; + + /** Used as a reference to the global object. */ + var root = (objectTypes[typeof window] && window) || this; + + /** Detect free variable `define`. */ + var freeDefine = false; + + /** Used to assign each benchmark an incremented id. */ + var counter = 0; + + /** Used to detect primitive types. */ + var rePrimitive = /^(?:boolean|number|string|undefined)$/; + + /** Used to make every compiled test unique. */ + var uidCounter = 0; + + /** Used to assign default `context` object properties. */ + var contextProps = [ + 'Array', 'Date', 'Function', 'Math', 'Object', 'RegExp', 'String', '_', + 'clearTimeout', 'chrome', 'chromium', 'document', 'navigator', 'phantom', + 'platform', 'process', 'runtime', 'setTimeout' + ]; + + /** Used to avoid hz of Infinity. */ + var divisors = { + '1': 4096, + '2': 512, + '3': 64, + '4': 8, + '5': 0 + }; + + /** + * T-Distribution two-tailed critical values for 95% confidence. + * For more info see http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm. + */ + var tTable = { + '1': 12.706, '2': 4.303, '3': 3.182, '4': 2.776, '5': 2.571, '6': 2.447, + '7': 2.365, '8': 2.306, '9': 2.262, '10': 2.228, '11': 2.201, '12': 2.179, + '13': 2.16, '14': 2.145, '15': 2.131, '16': 2.12, '17': 2.11, '18': 2.101, + '19': 2.093, '20': 2.086, '21': 2.08, '22': 2.074, '23': 2.069, '24': 2.064, + '25': 2.06, '26': 2.056, '27': 2.052, '28': 2.048, '29': 2.045, '30': 2.042, + 'infinity': 1.96 + }; + + /** + * Critical Mann-Whitney U-values for 95% confidence. + * For more info see http://www.saburchill.com/IBbiology/stats/003.html. + */ + var uTable = { + '5': [0, 1, 2], + '6': [1, 2, 3, 5], + '7': [1, 3, 5, 6, 8], + '8': [2, 4, 6, 8, 10, 13], + '9': [2, 4, 7, 10, 12, 15, 17], + '10': [3, 5, 8, 11, 14, 17, 20, 23], + '11': [3, 6, 9, 13, 16, 19, 23, 26, 30], + '12': [4, 7, 11, 14, 18, 22, 26, 29, 33, 37], + '13': [4, 8, 12, 16, 20, 24, 28, 33, 37, 41, 45], + '14': [5, 9, 13, 17, 22, 26, 31, 36, 40, 45, 50, 55], + '15': [5, 10, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59, 64], + '16': [6, 11, 15, 21, 26, 31, 37, 42, 47, 53, 59, 64, 70, 75], + '17': [6, 11, 17, 22, 28, 34, 39, 45, 51, 57, 63, 67, 75, 81, 87], + '18': [7, 12, 18, 24, 30, 36, 42, 48, 55, 61, 67, 74, 80, 86, 93, 99], + '19': [7, 13, 19, 25, 32, 38, 45, 52, 58, 65, 72, 78, 85, 92, 99, 106, 113], + '20': [8, 14, 20, 27, 34, 41, 48, 55, 62, 69, 76, 83, 90, 98, 105, 112, 119, 127], + '21': [8, 15, 22, 29, 36, 43, 50, 58, 65, 73, 80, 88, 96, 103, 111, 119, 126, 134, 142], + '22': [9, 16, 23, 30, 38, 45, 53, 61, 69, 77, 85, 93, 101, 109, 117, 125, 133, 141, 150, 158], + '23': [9, 17, 24, 32, 40, 48, 56, 64, 73, 81, 89, 98, 106, 115, 123, 132, 140, 149, 157, 166, 175], + '24': [10, 17, 25, 33, 42, 50, 59, 67, 76, 85, 94, 102, 111, 120, 129, 138, 147, 156, 165, 174, 183, 192], + '25': [10, 18, 27, 35, 44, 53, 62, 71, 80, 89, 98, 107, 117, 126, 135, 145, 154, 163, 173, 182, 192, 201, 211], + '26': [11, 19, 28, 37, 46, 55, 64, 74, 83, 93, 102, 112, 122, 132, 141, 151, 161, 171, 181, 191, 200, 210, 220, 230], + '27': [11, 20, 29, 38, 48, 57, 67, 77, 87, 97, 107, 118, 125, 138, 147, 158, 168, 178, 188, 199, 209, 219, 230, 240, 250], + '28': [12, 21, 30, 40, 50, 60, 70, 80, 90, 101, 111, 122, 132, 143, 154, 164, 175, 186, 196, 207, 218, 228, 239, 250, 261, 272], + '29': [13, 22, 32, 42, 52, 62, 73, 83, 94, 105, 116, 127, 138, 149, 160, 171, 182, 193, 204, 215, 226, 238, 249, 260, 271, 282, 294], + '30': [13, 23, 33, 43, 54, 65, 76, 87, 98, 109, 120, 131, 143, 154, 166, 177, 189, 200, 212, 223, 235, 247, 258, 270, 282, 293, 305, 317] + }; + + /*--------------------------------------------------------------------------*/ + + /** + * Create a new `Benchmark` function using the given `context` object. + * + * @static + * @memberOf Benchmark + * @param {Object} [context=root] The context object. + * @returns {Function} Returns a new `Benchmark` function. + */ + function runInContext(context) { + // Exit early if unable to acquire lodash. + var _ = context && context._ || require('lodash') || root._; + if (!_) { + Benchmark.runInContext = runInContext; + return Benchmark; + } + // Avoid issues with some ES3 environments that attempt to use values, named + // after built-in constructors like `Object`, for the creation of literals. + // ES5 clears this up by stating that literals must use built-in constructors. + // See http://es5.github.io/#x11.1.5. + context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root; + + /** Native constructor references. */ + var Array = context.Array, + Date = context.Date, + Function = context.Function, + Math = context.Math, + Object = context.Object, + RegExp = context.RegExp, + String = context.String; + + /** Used for `Array` and `Object` method references. */ + var arrayRef = [], + objectProto = Object.prototype; + + /** Native method shortcuts. */ + var abs = Math.abs, + clearTimeout = context.clearTimeout, + floor = Math.floor, + log = Math.log, + max = Math.max, + min = Math.min, + pow = Math.pow, + push = arrayRef.push, + setTimeout = context.setTimeout, + shift = arrayRef.shift, + slice = arrayRef.slice, + sqrt = Math.sqrt, + toString = objectProto.toString, + unshift = arrayRef.unshift; + + /** Detect DOM document object. */ + var doc = isHostType(context, 'document') && context.document; + + /** Used to access Node.js's high resolution timer. */ + var processObject = isHostType(context, 'process') && context.process; + + /** Used to prevent a `removeChild` memory leak in IE < 9. */ + var trash = doc && doc.createElement('div'); + + /** Used to integrity check compiled tests. */ + var uid = 'uid' + _.now(); + + /** Used to avoid infinite recursion when methods call each other. */ + var calledBy = {}; + + /** + * An object used to flag environments/features. + * + * @static + * @memberOf Benchmark + * @type Object + */ + var support = {}; + + (function() { + + /** + * Detect if running in a browser environment. + * + * @memberOf Benchmark.support + * @type boolean + */ + support.browser = doc && isHostType(context, 'navigator') && !isHostType(context, 'phantom'); + + /** + * Detect if the Timers API exists. + * + * @memberOf Benchmark.support + * @type boolean + */ + support.timeout = isHostType(context, 'setTimeout') && isHostType(context, 'clearTimeout'); + + /** + * Detect if function decompilation is support. + * + * @name decompilation + * @memberOf Benchmark.support + * @type boolean + */ + try { + // Safari 2.x removes commas in object literals from `Function#toString` results. + // See http://webk.it/11609 for more details. + // Firefox 3.6 and Opera 9.25 strip grouping parentheses from `Function#toString` results. + // See http://bugzil.la/559438 for more details. + support.decompilation = Function( + ('return (' + (function(x) { return { 'x': '' + (1 + x) + '', 'y': 0 }; }) + ')') + // Avoid issues with code added by Istanbul. + .replace(/__cov__[^;]+;/g, '') + )()(0).x === '1'; + } catch(e) { + support.decompilation = false; + } + }()); + + /** + * Timer object used by `clock()` and `Deferred#resolve`. + * + * @private + * @type Object + */ + var timer = { + + /** + * The timer namespace object or constructor. + * + * @private + * @memberOf timer + * @type {Function|Object} + */ + 'ns': Date, + + /** + * Starts the deferred timer. + * + * @private + * @memberOf timer + * @param {Object} deferred The deferred instance. + */ + 'start': null, // Lazy defined in `clock()`. + + /** + * Stops the deferred timer. + * + * @private + * @memberOf timer + * @param {Object} deferred The deferred instance. + */ + 'stop': null // Lazy defined in `clock()`. + }; + + /*------------------------------------------------------------------------*/ + + /** + * The Benchmark constructor. + * + * Note: The Benchmark constructor exposes a handful of lodash methods to + * make working with arrays, collections, and objects easier. The lodash + * methods are: + * [`each/forEach`](https://lodash.com/docs#forEach), [`forOwn`](https://lodash.com/docs#forOwn), + * [`has`](https://lodash.com/docs#has), [`indexOf`](https://lodash.com/docs#indexOf), + * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) + * + * @constructor + * @param {string} name A name to identify the benchmark. + * @param {Function|string} fn The test to benchmark. + * @param {Object} [options={}] Options object. + * @example + * + * // basic usage (the `new` operator is optional) + * var bench = new Benchmark(fn); + * + * // or using a name first + * var bench = new Benchmark('foo', fn); + * + * // or with options + * var bench = new Benchmark('foo', fn, { + * + * // displayed by `Benchmark#toString` if `name` is not available + * 'id': 'xyz', + * + * // called when the benchmark starts running + * 'onStart': onStart, + * + * // called after each run cycle + * 'onCycle': onCycle, + * + * // called when aborted + * 'onAbort': onAbort, + * + * // called when a test errors + * 'onError': onError, + * + * // called when reset + * 'onReset': onReset, + * + * // called when the benchmark completes running + * 'onComplete': onComplete, + * + * // compiled/called before the test loop + * 'setup': setup, + * + * // compiled/called after the test loop + * 'teardown': teardown + * }); + * + * // or name and options + * var bench = new Benchmark('foo', { + * + * // a flag to indicate the benchmark is deferred + * 'defer': true, + * + * // benchmark test function + * 'fn': function(deferred) { + * // call `Deferred#resolve` when the deferred test is finished + * deferred.resolve(); + * } + * }); + * + * // or options only + * var bench = new Benchmark({ + * + * // benchmark name + * 'name': 'foo', + * + * // benchmark test as a string + * 'fn': '[1,2,3,4].sort()' + * }); + * + * // a test's `this` binding is set to the benchmark instance + * var bench = new Benchmark('foo', function() { + * 'My name is '.concat(this.name); // "My name is foo" + * }); + */ + function Benchmark(name, fn, options) { + var bench = this; + + // Allow instance creation without the `new` operator. + if (!(bench instanceof Benchmark)) { + return new Benchmark(name, fn, options); + } + // Juggle arguments. + if (_.isPlainObject(name)) { + // 1 argument (options). + options = name; + } + else if (_.isFunction(name)) { + // 2 arguments (fn, options). + options = fn; + fn = name; + } + else if (_.isPlainObject(fn)) { + // 2 arguments (name, options). + options = fn; + fn = null; + bench.name = name; + } + else { + // 3 arguments (name, fn [, options]). + bench.name = name; + } + setOptions(bench, options); + + bench.id || (bench.id = ++counter); + bench.fn == null && (bench.fn = fn); + + bench.stats = cloneDeep(bench.stats); + bench.times = cloneDeep(bench.times); + } + + /** + * The Deferred constructor. + * + * @constructor + * @memberOf Benchmark + * @param {Object} clone The cloned benchmark instance. + */ + function Deferred(clone) { + var deferred = this; + if (!(deferred instanceof Deferred)) { + return new Deferred(clone); + } + deferred.benchmark = clone; + clock(deferred); + } + + /** + * The Event constructor. + * + * @constructor + * @memberOf Benchmark + * @param {Object|string} type The event type. + */ + function Event(type) { + var event = this; + if (type instanceof Event) { + return type; + } + return (event instanceof Event) + ? _.assign(event, { 'timeStamp': _.now() }, typeof type == 'string' ? { 'type': type } : type) + : new Event(type); + } + + /** + * The Suite constructor. + * + * Note: Each Suite instance has a handful of wrapped lodash methods to + * make working with Suites easier. The wrapped lodash methods are: + * [`each/forEach`](https://lodash.com/docs#forEach), [`indexOf`](https://lodash.com/docs#indexOf), + * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) + * + * @constructor + * @memberOf Benchmark + * @param {string} name A name to identify the suite. + * @param {Object} [options={}] Options object. + * @example + * + * // basic usage (the `new` operator is optional) + * var suite = new Benchmark.Suite; + * + * // or using a name first + * var suite = new Benchmark.Suite('foo'); + * + * // or with options + * var suite = new Benchmark.Suite('foo', { + * + * // called when the suite starts running + * 'onStart': onStart, + * + * // called between running benchmarks + * 'onCycle': onCycle, + * + * // called when aborted + * 'onAbort': onAbort, + * + * // called when a test errors + * 'onError': onError, + * + * // called when reset + * 'onReset': onReset, + * + * // called when the suite completes running + * 'onComplete': onComplete + * }); + */ + function Suite(name, options) { + var suite = this; + + // Allow instance creation without the `new` operator. + if (!(suite instanceof Suite)) { + return new Suite(name, options); + } + // Juggle arguments. + if (_.isPlainObject(name)) { + // 1 argument (options). + options = name; + } else { + // 2 arguments (name [, options]). + suite.name = name; + } + setOptions(suite, options); + } + + /*------------------------------------------------------------------------*/ + + /** + * A specialized version of `_.cloneDeep` which only clones arrays and plain + * objects assigning all other values by reference. + * + * @private + * @param {*} value The value to clone. + * @returns {*} The cloned value. + */ + var cloneDeep = _.partial(_.cloneDeepWith, _, function(value) { + // Only clone primitives, arrays, and plain objects. + return (_.isObject(value) && !_.isArray(value) && !_.isPlainObject(value)) + ? value + : undefined; + }); + + /** + * Creates a function from the given arguments string and body. + * + * @private + * @param {string} args The comma separated function arguments. + * @param {string} body The function body. + * @returns {Function} The new function. + */ + function createFunction() { + // Lazy define. + createFunction = function(args, body) { + var result, + anchor = freeDefine ? freeDefine.amd : Benchmark, + prop = uid + 'createFunction'; + + runScript((freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '=function(' + args + '){' + body + '}'); + result = anchor[prop]; + delete anchor[prop]; + return result; + }; + // Fix JaegerMonkey bug. + // For more information see http://bugzil.la/639720. + createFunction = support.browser && (createFunction('', 'return"' + uid + '"') || _.noop)() == uid ? createFunction : Function; + return createFunction.apply(null, arguments); + } + + /** + * Delay the execution of a function based on the benchmark's `delay` property. + * + * @private + * @param {Object} bench The benchmark instance. + * @param {Object} fn The function to execute. + */ + function delay(bench, fn) { + bench._timerId = _.delay(fn, bench.delay * 1e3); + } + + /** + * Destroys the given element. + * + * @private + * @param {Element} element The element to destroy. + */ + function destroyElement(element) { + trash.appendChild(element); + trash.innerHTML = ''; + } + + /** + * Gets the name of the first argument from a function's source. + * + * @private + * @param {Function} fn The function. + * @returns {string} The argument name. + */ + function getFirstArgument(fn) { + return (!_.has(fn, 'toString') && + (/^[\s(]*function[^(]*\(([^\s,)]+)/.exec(fn) || 0)[1]) || ''; + } + + /** + * Computes the arithmetic mean of a sample. + * + * @private + * @param {Array} sample The sample. + * @returns {number} The mean. + */ + function getMean(sample) { + return (_.reduce(sample, function(sum, x) { + return sum + x; + }) / sample.length) || 0; + } + + /** + * Gets the source code of a function. + * + * @private + * @param {Function} fn The function. + * @returns {string} The function's source code. + */ + function getSource(fn) { + var result = ''; + if (isStringable(fn)) { + result = String(fn); + } else if (support.decompilation) { + // Escape the `{` for Firefox 1. + result = _.result(/^[^{]+\{([\s\S]*)\}\s*$/.exec(fn), 1); + } + // Trim string. + result = (result || '').replace(/^\s+|\s+$/g, ''); + + // Detect strings containing only the "use strict" directive. + return /^(?:\/\*+[\w\W]*?\*\/|\/\/.*?[\n\r\u2028\u2029]|\s)*(["'])use strict\1;?$/.test(result) + ? '' + : result; + } + + /** + * Checks if an object is of the specified class. + * + * @private + * @param {*} value The value to check. + * @param {string} name The name of the class. + * @returns {boolean} Returns `true` if the value is of the specified class, else `false`. + */ + function isClassOf(value, name) { + return value != null && toString.call(value) == '[object ' + name + ']'; + } + + /** + * Host objects can return type values that are different from their actual + * data type. The objects we are concerned with usually return non-primitive + * types of "object", "function", or "unknown". + * + * @private + * @param {*} object The owner of the property. + * @param {string} property The property to check. + * @returns {boolean} Returns `true` if the property value is a non-primitive, else `false`. + */ + function isHostType(object, property) { + if (object == null) { + return false; + } + var type = typeof object[property]; + return !rePrimitive.test(type) && (type != 'object' || !!object[property]); + } + + /** + * Checks if a value can be safely coerced to a string. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the value can be coerced, else `false`. + */ + function isStringable(value) { + return _.isString(value) || (_.has(value, 'toString') && _.isFunction(value.toString)); + } + + /** + * Runs a snippet of JavaScript via script injection. + * + * @private + * @param {string} code The code to run. + */ + function runScript(code) { + var anchor = freeDefine ? define.amd : Benchmark, + script = doc.createElement('script'), + sibling = doc.getElementsByTagName('script')[0], + parent = sibling.parentNode, + prop = uid + 'runScript', + prefix = '(' + (freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '||function(){})();'; + + // Firefox 2.0.0.2 cannot use script injection as intended because it executes + // asynchronously, but that's OK because script injection is only used to avoid + // the previously commented JaegerMonkey bug. + try { + // Remove the inserted script *before* running the code to avoid differences + // in the expected script element count/order of the document. + script.appendChild(doc.createTextNode(prefix + code)); + anchor[prop] = function() { destroyElement(script); }; + } catch(e) { + parent = parent.cloneNode(false); + sibling = null; + script.text = code; + } + parent.insertBefore(script, sibling); + delete anchor[prop]; + } + + /** + * A helper function for setting options/event handlers. + * + * @private + * @param {Object} object The benchmark or suite instance. + * @param {Object} [options={}] Options object. + */ + function setOptions(object, options) { + options = object.options = _.assign({}, cloneDeep(object.constructor.options), cloneDeep(options)); + + _.forOwn(options, function(value, key) { + if (value != null) { + // Add event listeners. + if (/^on[A-Z]/.test(key)) { + _.each(key.split(' '), function(key) { + object.on(key.slice(2).toLowerCase(), value); + }); + } else if (!_.has(object, key)) { + object[key] = cloneDeep(value); + } + } + }); + } + + /*------------------------------------------------------------------------*/ + + /** + * Handles cycling/completing the deferred benchmark. + * + * @memberOf Benchmark.Deferred + */ + function resolve() { + var deferred = this, + clone = deferred.benchmark, + bench = clone._original; + + if (bench.aborted) { + // cycle() -> clone cycle/complete event -> compute()'s invoked bench.run() cycle/complete. + deferred.teardown(); + clone.running = false; + cycle(deferred); + } + else if (++deferred.cycles < clone.count) { + clone.compiled.call(deferred, context, timer); + } + else { + timer.stop(deferred); + deferred.teardown(); + delay(clone, function() { cycle(deferred); }); + } + } + + /*------------------------------------------------------------------------*/ + + /** + * A generic `Array#filter` like method. + * + * @static + * @memberOf Benchmark + * @param {Array} array The array to iterate over. + * @param {Function|string} callback The function/alias called per iteration. + * @returns {Array} A new array of values that passed callback filter. + * @example + * + * // get odd numbers + * Benchmark.filter([1, 2, 3, 4, 5], function(n) { + * return n % 2; + * }); // -> [1, 3, 5]; + * + * // get fastest benchmarks + * Benchmark.filter(benches, 'fastest'); + * + * // get slowest benchmarks + * Benchmark.filter(benches, 'slowest'); + * + * // get benchmarks that completed without erroring + * Benchmark.filter(benches, 'successful'); + */ + function filter(array, callback) { + if (callback === 'successful') { + // Callback to exclude those that are errored, unrun, or have hz of Infinity. + callback = function(bench) { + return bench.cycles && _.isFinite(bench.hz) && !bench.error; + }; + } + else if (callback === 'fastest' || callback === 'slowest') { + // Get successful, sort by period + margin of error, and filter fastest/slowest. + var result = filter(array, 'successful').sort(function(a, b) { + a = a.stats; b = b.stats; + return (a.mean + a.moe > b.mean + b.moe ? 1 : -1) * (callback === 'fastest' ? 1 : -1); + }); + + return _.filter(result, function(bench) { + return result[0].compare(bench) == 0; + }); + } + return _.filter(array, callback); + } + + /** + * Converts a number to a more readable comma-separated string representation. + * + * @static + * @memberOf Benchmark + * @param {number} number The number to convert. + * @returns {string} The more readable string representation. + */ + function formatNumber(number) { + number = String(number).split('.'); + return number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + + (number[1] ? '.' + number[1] : ''); + } + + /** + * Invokes a method on all items in an array. + * + * @static + * @memberOf Benchmark + * @param {Array} benches Array of benchmarks to iterate over. + * @param {Object|string} name The name of the method to invoke OR options object. + * @param {...*} [args] Arguments to invoke the method with. + * @returns {Array} A new array of values returned from each method invoked. + * @example + * + * // invoke `reset` on all benchmarks + * Benchmark.invoke(benches, 'reset'); + * + * // invoke `emit` with arguments + * Benchmark.invoke(benches, 'emit', 'complete', listener); + * + * // invoke `run(true)`, treat benchmarks as a queue, and register invoke callbacks + * Benchmark.invoke(benches, { + * + * // invoke the `run` method + * 'name': 'run', + * + * // pass a single argument + * 'args': true, + * + * // treat as queue, removing benchmarks from front of `benches` until empty + * 'queued': true, + * + * // called before any benchmarks have been invoked. + * 'onStart': onStart, + * + * // called between invoking benchmarks + * 'onCycle': onCycle, + * + * // called after all benchmarks have been invoked. + * 'onComplete': onComplete + * }); + */ + function invoke(benches, name) { + var args, + bench, + queued, + index = -1, + eventProps = { 'currentTarget': benches }, + options = { 'onStart': _.noop, 'onCycle': _.noop, 'onComplete': _.noop }, + result = _.toArray(benches); + + /** + * Invokes the method of the current object and if synchronous, fetches the next. + */ + function execute() { + var listeners, + async = isAsync(bench); + + if (async) { + // Use `getNext` as the first listener. + bench.on('complete', getNext); + listeners = bench.events.complete; + listeners.splice(0, 0, listeners.pop()); + } + // Execute method. + result[index] = _.isFunction(bench && bench[name]) ? bench[name].apply(bench, args) : undefined; + // If synchronous return `true` until finished. + return !async && getNext(); + } + + /** + * Fetches the next bench or executes `onComplete` callback. + */ + function getNext(event) { + var cycleEvent, + last = bench, + async = isAsync(last); + + if (async) { + last.off('complete', getNext); + last.emit('complete'); + } + // Emit "cycle" event. + eventProps.type = 'cycle'; + eventProps.target = last; + cycleEvent = Event(eventProps); + options.onCycle.call(benches, cycleEvent); + + // Choose next benchmark if not exiting early. + if (!cycleEvent.aborted && raiseIndex() !== false) { + bench = queued ? benches[0] : result[index]; + if (isAsync(bench)) { + delay(bench, execute); + } + else if (async) { + // Resume execution if previously asynchronous but now synchronous. + while (execute()) {} + } + else { + // Continue synchronous execution. + return true; + } + } else { + // Emit "complete" event. + eventProps.type = 'complete'; + options.onComplete.call(benches, Event(eventProps)); + } + // When used as a listener `event.aborted = true` will cancel the rest of + // the "complete" listeners because they were already called above and when + // used as part of `getNext` the `return false` will exit the execution while-loop. + if (event) { + event.aborted = true; + } else { + return false; + } + } + + /** + * Checks if invoking `Benchmark#run` with asynchronous cycles. + */ + function isAsync(object) { + // Avoid using `instanceof` here because of IE memory leak issues with host objects. + var async = args[0] && args[0].async; + return name == 'run' && (object instanceof Benchmark) && + ((async == null ? object.options.async : async) && support.timeout || object.defer); + } + + /** + * Raises `index` to the next defined index or returns `false`. + */ + function raiseIndex() { + index++; + + // If queued remove the previous bench. + if (queued && index > 0) { + shift.call(benches); + } + // If we reached the last index then return `false`. + return (queued ? benches.length : index < result.length) + ? index + : (index = false); + } + // Juggle arguments. + if (_.isString(name)) { + // 2 arguments (array, name). + args = slice.call(arguments, 2); + } else { + // 2 arguments (array, options). + options = _.assign(options, name); + name = options.name; + args = _.isArray(args = 'args' in options ? options.args : []) ? args : [args]; + queued = options.queued; + } + // Start iterating over the array. + if (raiseIndex() !== false) { + // Emit "start" event. + bench = result[index]; + eventProps.type = 'start'; + eventProps.target = bench; + options.onStart.call(benches, Event(eventProps)); + + // End early if the suite was aborted in an "onStart" listener. + if (name == 'run' && (benches instanceof Suite) && benches.aborted) { + // Emit "cycle" event. + eventProps.type = 'cycle'; + options.onCycle.call(benches, Event(eventProps)); + // Emit "complete" event. + eventProps.type = 'complete'; + options.onComplete.call(benches, Event(eventProps)); + } + // Start method execution. + else { + if (isAsync(bench)) { + delay(bench, execute); + } else { + while (execute()) {} + } + } + } + return result; + } + + /** + * Creates a string of joined array values or object key-value pairs. + * + * @static + * @memberOf Benchmark + * @param {Array|Object} object The object to operate on. + * @param {string} [separator1=','] The separator used between key-value pairs. + * @param {string} [separator2=': '] The separator used between keys and values. + * @returns {string} The joined result. + */ + function join(object, separator1, separator2) { + var result = [], + length = (object = Object(object)).length, + arrayLike = length === length >>> 0; + + separator2 || (separator2 = ': '); + _.each(object, function(value, key) { + result.push(arrayLike ? value : key + separator2 + value); + }); + return result.join(separator1 || ','); + } + + /*------------------------------------------------------------------------*/ + + /** + * Aborts all benchmarks in the suite. + * + * @name abort + * @memberOf Benchmark.Suite + * @returns {Object} The suite instance. + */ + function abortSuite() { + var event, + suite = this, + resetting = calledBy.resetSuite; + + if (suite.running) { + event = Event('abort'); + suite.emit(event); + if (!event.cancelled || resetting) { + // Avoid infinite recursion. + calledBy.abortSuite = true; + suite.reset(); + delete calledBy.abortSuite; + + if (!resetting) { + suite.aborted = true; + invoke(suite, 'abort'); + } + } + } + return suite; + } + + /** + * Adds a test to the benchmark suite. + * + * @memberOf Benchmark.Suite + * @param {string} name A name to identify the benchmark. + * @param {Function|string} fn The test to benchmark. + * @param {Object} [options={}] Options object. + * @returns {Object} The suite instance. + * @example + * + * // basic usage + * suite.add(fn); + * + * // or using a name first + * suite.add('foo', fn); + * + * // or with options + * suite.add('foo', fn, { + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + * + * // or name and options + * suite.add('foo', { + * 'fn': fn, + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + * + * // or options only + * suite.add({ + * 'name': 'foo', + * 'fn': fn, + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + */ + function add(name, fn, options) { + var suite = this, + bench = new Benchmark(name, fn, options), + event = Event({ 'type': 'add', 'target': bench }); + + if (suite.emit(event), !event.cancelled) { + suite.push(bench); + } + return suite; + } + + /** + * Creates a new suite with cloned benchmarks. + * + * @name clone + * @memberOf Benchmark.Suite + * @param {Object} options Options object to overwrite cloned options. + * @returns {Object} The new suite instance. + */ + function cloneSuite(options) { + var suite = this, + result = new suite.constructor(_.assign({}, suite.options, options)); + + // Copy own properties. + _.forOwn(suite, function(value, key) { + if (!_.has(result, key)) { + result[key] = value && _.isFunction(value.clone) + ? value.clone() + : cloneDeep(value); + } + }); + return result; + } + + /** + * An `Array#filter` like method. + * + * @name filter + * @memberOf Benchmark.Suite + * @param {Function|string} callback The function/alias called per iteration. + * @returns {Object} A new suite of benchmarks that passed callback filter. + */ + function filterSuite(callback) { + var suite = this, + result = new suite.constructor(suite.options); + + result.push.apply(result, filter(suite, callback)); + return result; + } + + /** + * Resets all benchmarks in the suite. + * + * @name reset + * @memberOf Benchmark.Suite + * @returns {Object} The suite instance. + */ + function resetSuite() { + var event, + suite = this, + aborting = calledBy.abortSuite; + + if (suite.running && !aborting) { + // No worries, `resetSuite()` is called within `abortSuite()`. + calledBy.resetSuite = true; + suite.abort(); + delete calledBy.resetSuite; + } + // Reset if the state has changed. + else if ((suite.aborted || suite.running) && + (suite.emit(event = Event('reset')), !event.cancelled)) { + suite.aborted = suite.running = false; + if (!aborting) { + invoke(suite, 'reset'); + } + } + return suite; + } + + /** + * Runs the suite. + * + * @name run + * @memberOf Benchmark.Suite + * @param {Object} [options={}] Options object. + * @returns {Object} The suite instance. + * @example + * + * // basic usage + * suite.run(); + * + * // or with options + * suite.run({ 'async': true, 'queued': true }); + */ + function runSuite(options) { + var suite = this; + + suite.reset(); + suite.running = true; + options || (options = {}); + + invoke(suite, { + 'name': 'run', + 'args': options, + 'queued': options.queued, + 'onStart': function(event) { + suite.emit(event); + }, + 'onCycle': function(event) { + var bench = event.target; + if (bench.error) { + suite.emit({ 'type': 'error', 'target': bench }); + } + suite.emit(event); + event.aborted = suite.aborted; + }, + 'onComplete': function(event) { + suite.running = false; + suite.emit(event); + } + }); + return suite; + } + + /*------------------------------------------------------------------------*/ + + /** + * Executes all registered listeners of the specified event type. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {Object|string} type The event type or object. + * @param {...*} [args] Arguments to invoke the listener with. + * @returns {*} Returns the return value of the last listener executed. + */ + function emit(type) { + var listeners, + object = this, + event = Event(type), + events = object.events, + args = (arguments[0] = event, arguments); + + event.currentTarget || (event.currentTarget = object); + event.target || (event.target = object); + delete event.result; + + if (events && (listeners = _.has(events, event.type) && events[event.type])) { + _.each(listeners.slice(), function(listener) { + if ((event.result = listener.apply(object, args)) === false) { + event.cancelled = true; + } + return !event.aborted; + }); + } + return event.result; + } + + /** + * Returns an array of event listeners for a given type that can be manipulated + * to add or remove listeners. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} type The event type. + * @returns {Array} The listeners array. + */ + function listeners(type) { + var object = this, + events = object.events || (object.events = {}); + + return _.has(events, type) ? events[type] : (events[type] = []); + } + + /** + * Unregisters a listener for the specified event type(s), + * or unregisters all listeners for the specified event type(s), + * or unregisters all listeners for all event types. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} [type] The event type. + * @param {Function} [listener] The function to unregister. + * @returns {Object} The current instance. + * @example + * + * // unregister a listener for an event type + * bench.off('cycle', listener); + * + * // unregister a listener for multiple event types + * bench.off('start cycle', listener); + * + * // unregister all listeners for an event type + * bench.off('cycle'); + * + * // unregister all listeners for multiple event types + * bench.off('start cycle complete'); + * + * // unregister all listeners for all event types + * bench.off(); + */ + function off(type, listener) { + var object = this, + events = object.events; + + if (!events) { + return object; + } + _.each(type ? type.split(' ') : events, function(listeners, type) { + var index; + if (typeof listeners == 'string') { + type = listeners; + listeners = _.has(events, type) && events[type]; + } + if (listeners) { + if (listener) { + index = _.indexOf(listeners, listener); + if (index > -1) { + listeners.splice(index, 1); + } + } else { + listeners.length = 0; + } + } + }); + return object; + } + + /** + * Registers a listener for the specified event type(s). + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} type The event type. + * @param {Function} listener The function to register. + * @returns {Object} The current instance. + * @example + * + * // register a listener for an event type + * bench.on('cycle', listener); + * + * // register a listener for multiple event types + * bench.on('start cycle', listener); + */ + function on(type, listener) { + var object = this, + events = object.events || (object.events = {}); + + _.each(type.split(' '), function(type) { + (_.has(events, type) + ? events[type] + : (events[type] = []) + ).push(listener); + }); + return object; + } + + /*------------------------------------------------------------------------*/ + + /** + * Aborts the benchmark without recording times. + * + * @memberOf Benchmark + * @returns {Object} The benchmark instance. + */ + function abort() { + var event, + bench = this, + resetting = calledBy.reset; + + if (bench.running) { + event = Event('abort'); + bench.emit(event); + if (!event.cancelled || resetting) { + // Avoid infinite recursion. + calledBy.abort = true; + bench.reset(); + delete calledBy.abort; + + if (support.timeout) { + clearTimeout(bench._timerId); + delete bench._timerId; + } + if (!resetting) { + bench.aborted = true; + bench.running = false; + } + } + } + return bench; + } + + /** + * Creates a new benchmark using the same test and options. + * + * @memberOf Benchmark + * @param {Object} options Options object to overwrite cloned options. + * @returns {Object} The new benchmark instance. + * @example + * + * var bizarro = bench.clone({ + * 'name': 'doppelganger' + * }); + */ + function clone(options) { + var bench = this, + result = new bench.constructor(_.assign({}, bench, options)); + + // Correct the `options` object. + result.options = _.assign({}, cloneDeep(bench.options), cloneDeep(options)); + + // Copy own custom properties. + _.forOwn(bench, function(value, key) { + if (!_.has(result, key)) { + result[key] = cloneDeep(value); + } + }); + + return result; + } + + /** + * Determines if a benchmark is faster than another. + * + * @memberOf Benchmark + * @param {Object} other The benchmark to compare. + * @returns {number} Returns `-1` if slower, `1` if faster, and `0` if indeterminate. + */ + function compare(other) { + var bench = this; + + // Exit early if comparing the same benchmark. + if (bench == other) { + return 0; + } + var critical, + zStat, + sample1 = bench.stats.sample, + sample2 = other.stats.sample, + size1 = sample1.length, + size2 = sample2.length, + maxSize = max(size1, size2), + minSize = min(size1, size2), + u1 = getU(sample1, sample2), + u2 = getU(sample2, sample1), + u = min(u1, u2); + + function getScore(xA, sampleB) { + return _.reduce(sampleB, function(total, xB) { + return total + (xB > xA ? 0 : xB < xA ? 1 : 0.5); + }, 0); + } + + function getU(sampleA, sampleB) { + return _.reduce(sampleA, function(total, xA) { + return total + getScore(xA, sampleB); + }, 0); + } + + function getZ(u) { + return (u - ((size1 * size2) / 2)) / sqrt((size1 * size2 * (size1 + size2 + 1)) / 12); + } + // Reject the null hypothesis the two samples come from the + // same population (i.e. have the same median) if... + if (size1 + size2 > 30) { + // ...the z-stat is greater than 1.96 or less than -1.96 + // http://www.statisticslectures.com/topics/mannwhitneyu/ + zStat = getZ(u); + return abs(zStat) > 1.96 ? (u == u1 ? 1 : -1) : 0; + } + // ...the U value is less than or equal the critical U value. + critical = maxSize < 5 || minSize < 3 ? 0 : uTable[maxSize][minSize - 3]; + return u <= critical ? (u == u1 ? 1 : -1) : 0; + } + + /** + * Reset properties and abort if running. + * + * @memberOf Benchmark + * @returns {Object} The benchmark instance. + */ + function reset() { + var bench = this; + if (bench.running && !calledBy.abort) { + // No worries, `reset()` is called within `abort()`. + calledBy.reset = true; + bench.abort(); + delete calledBy.reset; + return bench; + } + var event, + index = 0, + changes = [], + queue = []; + + // A non-recursive solution to check if properties have changed. + // For more information see http://www.jslab.dk/articles/non.recursive.preorder.traversal.part4. + var data = { + 'destination': bench, + 'source': _.assign({}, cloneDeep(bench.constructor.prototype), cloneDeep(bench.options)) + }; + + do { + _.forOwn(data.source, function(value, key) { + var changed, + destination = data.destination, + currValue = destination[key]; + + // Skip pseudo private properties like `_timerId` which could be a + // Java object in environments like RingoJS. + if (key.charAt(0) == '_') { + return; + } + if (value && typeof value == 'object') { + if (_.isArray(value)) { + // Check if an array value has changed to a non-array value. + if (!_.isArray(currValue)) { + changed = currValue = []; + } + // Check if an array has changed its length. + if (currValue.length != value.length) { + changed = currValue = currValue.slice(0, value.length); + currValue.length = value.length; + } + } + // Check if an object has changed to a non-object value. + else if (!currValue || typeof currValue != 'object') { + changed = currValue = {}; + } + // Register a changed object. + if (changed) { + changes.push({ 'destination': destination, 'key': key, 'value': currValue }); + } + queue.push({ 'destination': currValue, 'source': value }); + } + // Register a changed primitive. + else if (value !== currValue && !(value == null || _.isFunction(value))) { + changes.push({ 'destination': destination, 'key': key, 'value': value }); + } + }); + } + while ((data = queue[index++])); + + // If changed emit the `reset` event and if it isn't cancelled reset the benchmark. + if (changes.length && (bench.emit(event = Event('reset')), !event.cancelled)) { + _.each(changes, function(data) { + data.destination[data.key] = data.value; + }); + } + return bench; + } + + /** + * Displays relevant benchmark information when coerced to a string. + * + * @name toString + * @memberOf Benchmark + * @returns {string} A string representation of the benchmark instance. + */ + function toStringBench() { + var bench = this, + error = bench.error, + hz = bench.hz, + id = bench.id, + stats = bench.stats, + size = stats.sample.length, + pm = '\xb1', + result = bench.name || (_.isNaN(id) ? id : ''); + + if (error) { + var errorStr; + if (!_.isObject(error)) { + errorStr = String(error); + } else if (!_.isError(Error)) { + errorStr = join(error); + } else { + // Error#name and Error#message properties are non-enumerable. + errorStr = join(_.assign({ 'name': error.name, 'message': error.message }, error)); + } + result += ': ' + errorStr; + } + else { + result += ' x ' + formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + ' ops/sec ' + pm + + stats.rme.toFixed(2) + '% (' + size + ' run' + (size == 1 ? '' : 's') + ' sampled)'; + } + return result; + } + + /*------------------------------------------------------------------------*/ + + /** + * Clocks the time taken to execute a test per cycle (secs). + * + * @private + * @param {Object} bench The benchmark instance. + * @returns {number} The time taken. + */ + function clock() { + var options = Benchmark.options, + templateData = {}, + timers = [{ 'ns': timer.ns, 'res': max(0.0015, getRes('ms')), 'unit': 'ms' }]; + + // Lazy define for hi-res timers. + clock = function(clone) { + var deferred; + + if (clone instanceof Deferred) { + deferred = clone; + clone = deferred.benchmark; + } + var bench = clone._original, + stringable = isStringable(bench.fn), + count = bench.count = clone.count, + decompilable = stringable || (support.decompilation && (clone.setup !== _.noop || clone.teardown !== _.noop)), + id = bench.id, + name = bench.name || (typeof id == 'number' ? '' : id), + result = 0; + + // Init `minTime` if needed. + clone.minTime = bench.minTime || (bench.minTime = bench.options.minTime = options.minTime); + + // Compile in setup/teardown functions and the test loop. + // Create a new compiled test, instead of using the cached `bench.compiled`, + // to avoid potential engine optimizations enabled over the life of the test. + var funcBody = deferred + ? 'var d#=this,${fnArg}=d#,m#=d#.benchmark._original,f#=m#.fn,su#=m#.setup,td#=m#.teardown;' + + // When `deferred.cycles` is `0` then... + 'if(!d#.cycles){' + + // set `deferred.fn`, + 'd#.fn=function(){var ${fnArg}=d#;if(typeof f#=="function"){try{${fn}\n}catch(e#){f#(d#)}}else{${fn}\n}};' + + // set `deferred.teardown`, + 'd#.teardown=function(){d#.cycles=0;if(typeof td#=="function"){try{${teardown}\n}catch(e#){td#()}}else{${teardown}\n}};' + + // execute the benchmark's `setup`, + 'if(typeof su#=="function"){try{${setup}\n}catch(e#){su#()}}else{${setup}\n};' + + // start timer, + 't#.start(d#);' + + // and then execute `deferred.fn` and return a dummy object. + '}d#.fn();return{uid:"${uid}"}' + + : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count,n#=t#.ns;${setup}\n${begin};' + + 'while(i#--){${fn}\n}${end};${teardown}\nreturn{elapsed:r#,uid:"${uid}"}'; + + var compiled = bench.compiled = clone.compiled = createCompiled(bench, decompilable, deferred, funcBody), + isEmpty = !(templateData.fn || stringable); + + try { + if (isEmpty) { + // Firefox may remove dead code from `Function#toString` results. + // For more information see http://bugzil.la/536085. + throw new Error('The test "' + name + '" is empty. This may be the result of dead code removal.'); + } + else if (!deferred) { + // Pretest to determine if compiled code exits early, usually by a + // rogue `return` statement, by checking for a return object with the uid. + bench.count = 1; + compiled = decompilable && (compiled.call(bench, context, timer) || {}).uid == templateData.uid && compiled; + bench.count = count; + } + } catch(e) { + compiled = null; + clone.error = e || new Error(String(e)); + bench.count = count; + } + // Fallback when a test exits early or errors during pretest. + if (!compiled && !deferred && !isEmpty) { + funcBody = ( + stringable || (decompilable && !clone.error) + ? 'function f#(){${fn}\n}var r#,s#,m#=this,i#=m#.count' + : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count' + ) + + ',n#=t#.ns;${setup}\n${begin};m#.f#=f#;while(i#--){m#.f#()}${end};' + + 'delete m#.f#;${teardown}\nreturn{elapsed:r#}'; + + compiled = createCompiled(bench, decompilable, deferred, funcBody); + + try { + // Pretest one more time to check for errors. + bench.count = 1; + compiled.call(bench, context, timer); + bench.count = count; + delete clone.error; + } + catch(e) { + bench.count = count; + if (!clone.error) { + clone.error = e || new Error(String(e)); + } + } + } + // If no errors run the full test loop. + if (!clone.error) { + compiled = bench.compiled = clone.compiled = createCompiled(bench, decompilable, deferred, funcBody); + result = compiled.call(deferred || bench, context, timer).elapsed; + } + return result; + }; + + /*----------------------------------------------------------------------*/ + + /** + * Creates a compiled function from the given function `body`. + */ + function createCompiled(bench, decompilable, deferred, body) { + var fn = bench.fn, + fnArg = deferred ? getFirstArgument(fn) || 'deferred' : ''; + + templateData.uid = uid + uidCounter++; + + _.assign(templateData, { + 'setup': decompilable ? getSource(bench.setup) : interpolate('m#.setup()'), + 'fn': decompilable ? getSource(fn) : interpolate('m#.fn(' + fnArg + ')'), + 'fnArg': fnArg, + 'teardown': decompilable ? getSource(bench.teardown) : interpolate('m#.teardown()') + }); + + // Use API of chosen timer. + if (timer.unit == 'ns') { + _.assign(templateData, { + 'begin': interpolate('s#=n#()'), + 'end': interpolate('r#=n#(s#);r#=r#[0]+(r#[1]/1e9)') + }); + } + else if (timer.unit == 'us') { + if (timer.ns.stop) { + _.assign(templateData, { + 'begin': interpolate('s#=n#.start()'), + 'end': interpolate('r#=n#.microseconds()/1e6') + }); + } else { + _.assign(templateData, { + 'begin': interpolate('s#=n#()'), + 'end': interpolate('r#=(n#()-s#)/1e6') + }); + } + } + else if (timer.ns.now) { + _.assign(templateData, { + 'begin': interpolate('s#=n#.now()'), + 'end': interpolate('r#=(n#.now()-s#)/1e3') + }); + } + else { + _.assign(templateData, { + 'begin': interpolate('s#=new n#().getTime()'), + 'end': interpolate('r#=(new n#().getTime()-s#)/1e3') + }); + } + // Define `timer` methods. + timer.start = createFunction( + interpolate('o#'), + interpolate('var n#=this.ns,${begin};o#.elapsed=0;o#.timeStamp=s#') + ); + + timer.stop = createFunction( + interpolate('o#'), + interpolate('var n#=this.ns,s#=o#.timeStamp,${end};o#.elapsed=r#') + ); + + // Create compiled test. + return createFunction( + interpolate('window,t#'), + 'var global = window, clearTimeout = global.clearTimeout, setTimeout = global.setTimeout;\n' + + interpolate(body) + ); + } + + /** + * Gets the current timer's minimum resolution (secs). + */ + function getRes(unit) { + var measured, + begin, + count = 30, + divisor = 1e3, + ns = timer.ns, + sample = []; + + // Get average smallest measurable time. + while (count--) { + if (unit == 'us') { + divisor = 1e6; + if (ns.stop) { + ns.start(); + while (!(measured = ns.microseconds())) {} + } else { + begin = ns(); + while (!(measured = ns() - begin)) {} + } + } + else if (unit == 'ns') { + divisor = 1e9; + begin = (begin = ns())[0] + (begin[1] / divisor); + while (!(measured = ((measured = ns())[0] + (measured[1] / divisor)) - begin)) {} + divisor = 1; + } + else if (ns.now) { + begin = ns.now(); + while (!(measured = ns.now() - begin)) {} + } + else { + begin = new ns().getTime(); + while (!(measured = new ns().getTime() - begin)) {} + } + // Check for broken timers. + if (measured > 0) { + sample.push(measured); + } else { + sample.push(Infinity); + break; + } + } + // Convert to seconds. + return getMean(sample) / divisor; + } + + /** + * Interpolates a given template string. + */ + function interpolate(string) { + // Replaces all occurrences of `#` with a unique number and template tokens with content. + return _.template(string.replace(/\#/g, /\d+/.exec(templateData.uid)))(templateData); + } + + /*----------------------------------------------------------------------*/ + + // Detect Chrome's microsecond timer: + // enable benchmarking via the --enable-benchmarking command + // line switch in at least Chrome 7 to use chrome.Interval + try { + if ((timer.ns = new (context.chrome || context.chromium).Interval)) { + timers.push({ 'ns': timer.ns, 'res': getRes('us'), 'unit': 'us' }); + } + } catch(e) {} + + // Detect Node.js's nanosecond resolution timer available in Node.js >= 0.8. + if (processObject && typeof (timer.ns = processObject.hrtime) == 'function') { + timers.push({ 'ns': timer.ns, 'res': getRes('ns'), 'unit': 'ns' }); + } + // Pick timer with highest resolution. + timer = _.minBy(timers, 'res'); + + // Error if there are no working timers. + if (timer.res == Infinity) { + throw new Error('Benchmark.js was unable to find a working timer.'); + } + // Resolve time span required to achieve a percent uncertainty of at most 1%. + // For more information see http://spiff.rit.edu/classes/phys273/uncert/uncert.html. + options.minTime || (options.minTime = max(timer.res / 2 / 0.01, 0.05)); + return clock.apply(null, arguments); + } + + /*------------------------------------------------------------------------*/ + + /** + * Computes stats on benchmark results. + * + * @private + * @param {Object} bench The benchmark instance. + * @param {Object} options The options object. + */ + function compute(bench, options) { + options || (options = {}); + + var async = options.async, + elapsed = 0, + initCount = bench.initCount, + minSamples = bench.minSamples, + queue = [], + sample = bench.stats.sample; + + /** + * Adds a clone to the queue. + */ + function enqueue() { + queue.push(bench.clone({ + '_original': bench, + 'events': { + 'abort': [update], + 'cycle': [update], + 'error': [update], + 'start': [update] + } + })); + } + + /** + * Updates the clone/original benchmarks to keep their data in sync. + */ + function update(event) { + var clone = this, + type = event.type; + + if (bench.running) { + if (type == 'start') { + // Note: `clone.minTime` prop is inited in `clock()`. + clone.count = bench.initCount; + } + else { + if (type == 'error') { + bench.error = clone.error; + } + if (type == 'abort') { + bench.abort(); + bench.emit('cycle'); + } else { + event.currentTarget = event.target = bench; + bench.emit(event); + } + } + } else if (bench.aborted) { + // Clear abort listeners to avoid triggering bench's abort/cycle again. + clone.events.abort.length = 0; + clone.abort(); + } + } + + /** + * Determines if more clones should be queued or if cycling should stop. + */ + function evaluate(event) { + var critical, + df, + mean, + moe, + rme, + sd, + sem, + variance, + clone = event.target, + done = bench.aborted, + now = _.now(), + size = sample.push(clone.times.period), + maxedOut = size >= minSamples && (elapsed += now - clone.times.timeStamp) / 1e3 > bench.maxTime, + times = bench.times, + varOf = function(sum, x) { return sum + pow(x - mean, 2); }; + + // Exit early for aborted or unclockable tests. + if (done || clone.hz == Infinity) { + maxedOut = !(size = sample.length = queue.length = 0); + } + + if (!done) { + // Compute the sample mean (estimate of the population mean). + mean = getMean(sample); + // Compute the sample variance (estimate of the population variance). + variance = _.reduce(sample, varOf, 0) / (size - 1) || 0; + // Compute the sample standard deviation (estimate of the population standard deviation). + sd = sqrt(variance); + // Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean). + sem = sd / sqrt(size); + // Compute the degrees of freedom. + df = size - 1; + // Compute the critical value. + critical = tTable[Math.round(df) || 1] || tTable.infinity; + // Compute the margin of error. + moe = sem * critical; + // Compute the relative margin of error. + rme = (moe / mean) * 100 || 0; + + _.assign(bench.stats, { + 'deviation': sd, + 'mean': mean, + 'moe': moe, + 'rme': rme, + 'sem': sem, + 'variance': variance + }); + + // Abort the cycle loop when the minimum sample size has been collected + // and the elapsed time exceeds the maximum time allowed per benchmark. + // We don't count cycle delays toward the max time because delays may be + // increased by browsers that clamp timeouts for inactive tabs. For more + // information see https://developer.mozilla.org/en/window.setTimeout#Inactive_tabs. + if (maxedOut) { + // Reset the `initCount` in case the benchmark is rerun. + bench.initCount = initCount; + bench.running = false; + done = true; + times.elapsed = (now - times.timeStamp) / 1e3; + } + if (bench.hz != Infinity) { + bench.hz = 1 / mean; + times.cycle = mean * bench.count; + times.period = mean; + } + } + // If time permits, increase sample size to reduce the margin of error. + if (queue.length < 2 && !maxedOut) { + enqueue(); + } + // Abort the `invoke` cycle when done. + event.aborted = done; + } + + // Init queue and begin. + enqueue(); + invoke(queue, { + 'name': 'run', + 'args': { 'async': async }, + 'queued': true, + 'onCycle': evaluate, + 'onComplete': function() { bench.emit('complete'); } + }); + } + + /*------------------------------------------------------------------------*/ + + /** + * Cycles a benchmark until a run `count` can be established. + * + * @private + * @param {Object} clone The cloned benchmark instance. + * @param {Object} options The options object. + */ + function cycle(clone, options) { + options || (options = {}); + + var deferred; + if (clone instanceof Deferred) { + deferred = clone; + clone = clone.benchmark; + } + var clocked, + cycles, + divisor, + event, + minTime, + period, + async = options.async, + bench = clone._original, + count = clone.count, + times = clone.times; + + // Continue, if not aborted between cycles. + if (clone.running) { + // `minTime` is set to `Benchmark.options.minTime` in `clock()`. + cycles = ++clone.cycles; + clocked = deferred ? deferred.elapsed : clock(clone); + minTime = clone.minTime; + + if (cycles > bench.cycles) { + bench.cycles = cycles; + } + if (clone.error) { + event = Event('error'); + event.message = clone.error; + clone.emit(event); + if (!event.cancelled) { + clone.abort(); + } + } + } + // Continue, if not errored. + if (clone.running) { + // Compute the time taken to complete last test cycle. + bench.times.cycle = times.cycle = clocked; + // Compute the seconds per operation. + period = bench.times.period = times.period = clocked / count; + // Compute the ops per second. + bench.hz = clone.hz = 1 / period; + // Avoid working our way up to this next time. + bench.initCount = clone.initCount = count; + // Do we need to do another cycle? + clone.running = clocked < minTime; + + if (clone.running) { + // Tests may clock at `0` when `initCount` is a small number, + // to avoid that we set its count to something a bit higher. + if (!clocked && (divisor = divisors[clone.cycles]) != null) { + count = floor(4e6 / divisor); + } + // Calculate how many more iterations it will take to achieve the `minTime`. + if (count <= clone.count) { + count += Math.ceil((minTime - clocked) / period); + } + clone.running = count != Infinity; + } + } + // Should we exit early? + event = Event('cycle'); + clone.emit(event); + if (event.aborted) { + clone.abort(); + } + // Figure out what to do next. + if (clone.running) { + // Start a new cycle. + clone.count = count; + if (deferred) { + clone.compiled.call(deferred, context, timer); + } else if (async) { + delay(clone, function() { cycle(clone, options); }); + } else { + cycle(clone); + } + } + else { + // Fix TraceMonkey bug associated with clock fallbacks. + // For more information see http://bugzil.la/509069. + if (support.browser) { + runScript(uid + '=1;delete ' + uid); + } + // We're done. + clone.emit('complete'); + } + } + + /*------------------------------------------------------------------------*/ + + /** + * Runs the benchmark. + * + * @memberOf Benchmark + * @param {Object} [options={}] Options object. + * @returns {Object} The benchmark instance. + * @example + * + * // basic usage + * bench.run(); + * + * // or with options + * bench.run({ 'async': true }); + */ + function run(options) { + var bench = this, + event = Event('start'); + + // Set `running` to `false` so `reset()` won't call `abort()`. + bench.running = false; + bench.reset(); + bench.running = true; + + bench.count = bench.initCount; + bench.times.timeStamp = _.now(); + bench.emit(event); + + if (!event.cancelled) { + options = { 'async': ((options = options && options.async) == null ? bench.async : options) && support.timeout }; + + // For clones created within `compute()`. + if (bench._original) { + if (bench.defer) { + Deferred(bench); + } else { + cycle(bench, options); + } + } + // For original benchmarks. + else { + compute(bench, options); + } + } + return bench; + } + + /*------------------------------------------------------------------------*/ + + // Firefox 1 erroneously defines variable and argument names of functions on + // the function itself as non-configurable properties with `undefined` values. + // The bugginess continues as the `Benchmark` constructor has an argument + // named `options` and Firefox 1 will not assign a value to `Benchmark.options`, + // making it non-writable in the process, unless it is the first property + // assigned by for-in loop of `_.assign()`. + _.assign(Benchmark, { + + /** + * The default options copied by benchmark instances. + * + * @static + * @memberOf Benchmark + * @type Object + */ + 'options': { + + /** + * A flag to indicate that benchmark cycles will execute asynchronously + * by default. + * + * @memberOf Benchmark.options + * @type boolean + */ + 'async': false, + + /** + * A flag to indicate that the benchmark clock is deferred. + * + * @memberOf Benchmark.options + * @type boolean + */ + 'defer': false, + + /** + * The delay between test cycles (secs). + * @memberOf Benchmark.options + * @type number + */ + 'delay': 0.005, + + /** + * Displayed by `Benchmark#toString` when a `name` is not available + * (auto-generated if absent). + * + * @memberOf Benchmark.options + * @type string + */ + 'id': undefined, + + /** + * The default number of times to execute a test on a benchmark's first cycle. + * + * @memberOf Benchmark.options + * @type number + */ + 'initCount': 1, + + /** + * The maximum time a benchmark is allowed to run before finishing (secs). + * + * Note: Cycle delays aren't counted toward the maximum time. + * + * @memberOf Benchmark.options + * @type number + */ + 'maxTime': 5, + + /** + * The minimum sample size required to perform statistical analysis. + * + * @memberOf Benchmark.options + * @type number + */ + 'minSamples': 5, + + /** + * The time needed to reduce the percent uncertainty of measurement to 1% (secs). + * + * @memberOf Benchmark.options + * @type number + */ + 'minTime': 0, + + /** + * The name of the benchmark. + * + * @memberOf Benchmark.options + * @type string + */ + 'name': undefined, + + /** + * An event listener called when the benchmark is aborted. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onAbort': undefined, + + /** + * An event listener called when the benchmark completes running. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onComplete': undefined, + + /** + * An event listener called after each run cycle. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onCycle': undefined, + + /** + * An event listener called when a test errors. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onError': undefined, + + /** + * An event listener called when the benchmark is reset. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onReset': undefined, + + /** + * An event listener called when the benchmark starts running. + * + * @memberOf Benchmark.options + * @type Function + */ + 'onStart': undefined + }, + + /** + * Platform object with properties describing things like browser name, + * version, and operating system. See [`platform.js`](https://mths.be/platform). + * + * @static + * @memberOf Benchmark + * @type Object + */ + 'platform': context.platform || require('platform') || ({ + 'description': context.navigator && context.navigator.userAgent || null, + 'layout': null, + 'product': null, + 'name': null, + 'manufacturer': null, + 'os': null, + 'prerelease': null, + 'version': null, + 'toString': function() { + return this.description || ''; + } + }), + + /** + * The semantic version number. + * + * @static + * @memberOf Benchmark + * @type string + */ + 'version': '2.1.2' + }); + + _.assign(Benchmark, { + 'filter': filter, + 'formatNumber': formatNumber, + 'invoke': invoke, + 'join': join, + 'runInContext': runInContext, + 'support': support + }); + + // Add lodash methods to Benchmark. + _.each(['each', 'forEach', 'forOwn', 'has', 'indexOf', 'map', 'reduce'], function(methodName) { + Benchmark[methodName] = _[methodName]; + }); + + /*------------------------------------------------------------------------*/ + + _.assign(Benchmark.prototype, { + + /** + * The number of times a test was executed. + * + * @memberOf Benchmark + * @type number + */ + 'count': 0, + + /** + * The number of cycles performed while benchmarking. + * + * @memberOf Benchmark + * @type number + */ + 'cycles': 0, + + /** + * The number of executions per second. + * + * @memberOf Benchmark + * @type number + */ + 'hz': 0, + + /** + * The compiled test function. + * + * @memberOf Benchmark + * @type {Function|string} + */ + 'compiled': undefined, + + /** + * The error object if the test failed. + * + * @memberOf Benchmark + * @type Object + */ + 'error': undefined, + + /** + * The test to benchmark. + * + * @memberOf Benchmark + * @type {Function|string} + */ + 'fn': undefined, + + /** + * A flag to indicate if the benchmark is aborted. + * + * @memberOf Benchmark + * @type boolean + */ + 'aborted': false, + + /** + * A flag to indicate if the benchmark is running. + * + * @memberOf Benchmark + * @type boolean + */ + 'running': false, + + /** + * Compiled into the test and executed immediately **before** the test loop. + * + * @memberOf Benchmark + * @type {Function|string} + * @example + * + * // basic usage + * var bench = Benchmark({ + * 'setup': function() { + * var c = this.count, + * element = document.getElementById('container'); + * while (c--) { + * element.appendChild(document.createElement('div')); + * } + * }, + * 'fn': function() { + * element.removeChild(element.lastChild); + * } + * }); + * + * // compiles to something like: + * var c = this.count, + * element = document.getElementById('container'); + * while (c--) { + * element.appendChild(document.createElement('div')); + * } + * var start = new Date; + * while (count--) { + * element.removeChild(element.lastChild); + * } + * var end = new Date - start; + * + * // or using strings + * var bench = Benchmark({ + * 'setup': '\ + * var a = 0;\n\ + * (function() {\n\ + * (function() {\n\ + * (function() {', + * 'fn': 'a += 1;', + * 'teardown': '\ + * }())\n\ + * }())\n\ + * }())' + * }); + * + * // compiles to something like: + * var a = 0; + * (function() { + * (function() { + * (function() { + * var start = new Date; + * while (count--) { + * a += 1; + * } + * var end = new Date - start; + * }()) + * }()) + * }()) + */ + 'setup': _.noop, + + /** + * Compiled into the test and executed immediately **after** the test loop. + * + * @memberOf Benchmark + * @type {Function|string} + */ + 'teardown': _.noop, + + /** + * An object of stats including mean, margin or error, and standard deviation. + * + * @memberOf Benchmark + * @type Object + */ + 'stats': { + + /** + * The margin of error. + * + * @memberOf Benchmark#stats + * @type number + */ + 'moe': 0, + + /** + * The relative margin of error (expressed as a percentage of the mean). + * + * @memberOf Benchmark#stats + * @type number + */ + 'rme': 0, + + /** + * The standard error of the mean. + * + * @memberOf Benchmark#stats + * @type number + */ + 'sem': 0, + + /** + * The sample standard deviation. + * + * @memberOf Benchmark#stats + * @type number + */ + 'deviation': 0, + + /** + * The sample arithmetic mean (secs). + * + * @memberOf Benchmark#stats + * @type number + */ + 'mean': 0, + + /** + * The array of sampled periods. + * + * @memberOf Benchmark#stats + * @type Array + */ + 'sample': [], + + /** + * The sample variance. + * + * @memberOf Benchmark#stats + * @type number + */ + 'variance': 0 + }, + + /** + * An object of timing data including cycle, elapsed, period, start, and stop. + * + * @memberOf Benchmark + * @type Object + */ + 'times': { + + /** + * The time taken to complete the last cycle (secs). + * + * @memberOf Benchmark#times + * @type number + */ + 'cycle': 0, + + /** + * The time taken to complete the benchmark (secs). + * + * @memberOf Benchmark#times + * @type number + */ + 'elapsed': 0, + + /** + * The time taken to execute the test once (secs). + * + * @memberOf Benchmark#times + * @type number + */ + 'period': 0, + + /** + * A timestamp of when the benchmark started (ms). + * + * @memberOf Benchmark#times + * @type number + */ + 'timeStamp': 0 + } + }); + + _.assign(Benchmark.prototype, { + 'abort': abort, + 'clone': clone, + 'compare': compare, + 'emit': emit, + 'listeners': listeners, + 'off': off, + 'on': on, + 'reset': reset, + 'run': run, + 'toString': toStringBench + }); + + /*------------------------------------------------------------------------*/ + + _.assign(Deferred.prototype, { + + /** + * The deferred benchmark instance. + * + * @memberOf Benchmark.Deferred + * @type Object + */ + 'benchmark': null, + + /** + * The number of deferred cycles performed while benchmarking. + * + * @memberOf Benchmark.Deferred + * @type number + */ + 'cycles': 0, + + /** + * The time taken to complete the deferred benchmark (secs). + * + * @memberOf Benchmark.Deferred + * @type number + */ + 'elapsed': 0, + + /** + * A timestamp of when the deferred benchmark started (ms). + * + * @memberOf Benchmark.Deferred + * @type number + */ + 'timeStamp': 0 + }); + + _.assign(Deferred.prototype, { + 'resolve': resolve + }); + + /*------------------------------------------------------------------------*/ + + _.assign(Event.prototype, { + + /** + * A flag to indicate if the emitters listener iteration is aborted. + * + * @memberOf Benchmark.Event + * @type boolean + */ + 'aborted': false, + + /** + * A flag to indicate if the default action is cancelled. + * + * @memberOf Benchmark.Event + * @type boolean + */ + 'cancelled': false, + + /** + * The object whose listeners are currently being processed. + * + * @memberOf Benchmark.Event + * @type Object + */ + 'currentTarget': undefined, + + /** + * The return value of the last executed listener. + * + * @memberOf Benchmark.Event + * @type Mixed + */ + 'result': undefined, + + /** + * The object to which the event was originally emitted. + * + * @memberOf Benchmark.Event + * @type Object + */ + 'target': undefined, + + /** + * A timestamp of when the event was created (ms). + * + * @memberOf Benchmark.Event + * @type number + */ + 'timeStamp': 0, + + /** + * The event type. + * + * @memberOf Benchmark.Event + * @type string + */ + 'type': '' + }); + + /*------------------------------------------------------------------------*/ + + /** + * The default options copied by suite instances. + * + * @static + * @memberOf Benchmark.Suite + * @type Object + */ + Suite.options = { + + /** + * The name of the suite. + * + * @memberOf Benchmark.Suite.options + * @type string + */ + 'name': undefined + }; + + /*------------------------------------------------------------------------*/ + + _.assign(Suite.prototype, { + + /** + * The number of benchmarks in the suite. + * + * @memberOf Benchmark.Suite + * @type number + */ + 'length': 0, + + /** + * A flag to indicate if the suite is aborted. + * + * @memberOf Benchmark.Suite + * @type boolean + */ + 'aborted': false, + + /** + * A flag to indicate if the suite is running. + * + * @memberOf Benchmark.Suite + * @type boolean + */ + 'running': false + }); + + _.assign(Suite.prototype, { + 'abort': abortSuite, + 'add': add, + 'clone': cloneSuite, + 'emit': emit, + 'filter': filterSuite, + 'join': arrayRef.join, + 'listeners': listeners, + 'off': off, + 'on': on, + 'pop': arrayRef.pop, + 'push': push, + 'reset': resetSuite, + 'run': runSuite, + 'reverse': arrayRef.reverse, + 'shift': shift, + 'slice': slice, + 'sort': arrayRef.sort, + 'splice': arrayRef.splice, + 'unshift': unshift + }); + + /*------------------------------------------------------------------------*/ + + // Expose Deferred, Event, and Suite. + _.assign(Benchmark, { + 'Deferred': Deferred, + 'Event': Event, + 'Suite': Suite + }); + + /*------------------------------------------------------------------------*/ + + // Add lodash methods as Suite methods. + _.each(['each', 'forEach', 'indexOf', 'map', 'reduce'], function(methodName) { + var func = _[methodName]; + Suite.prototype[methodName] = function() { + var args = [this]; + push.apply(args, arguments); + return func.apply(_, args); + }; + }); + + // Avoid array-like object bugs with `Array#shift` and `Array#splice` + // in Firefox < 10 and IE < 9. + _.each(['pop', 'shift', 'splice'], function(methodName) { + var func = arrayRef[methodName]; + + Suite.prototype[methodName] = function() { + var value = this, + result = func.apply(value, arguments); + + if (value.length === 0) { + delete value[0]; + } + return result; + }; + }); + + // Avoid buggy `Array#unshift` in IE < 8 which doesn't return the new + // length of the array. + Suite.prototype.unshift = function() { + var value = this; + unshift.apply(value, arguments); + return value.length; + }; + + return Benchmark; + } + + /*--------------------------------------------------------------------------*/ + + var Benchmark = runInContext(); + + window.Benchmark = Benchmark; + + return Benchmark; + +}.call(this)); diff --git a/tgui/packages/tgui-bench/package.json b/tgui/packages/tgui-bench/package.json new file mode 100644 index 0000000000..ae83ca8666 --- /dev/null +++ b/tgui/packages/tgui-bench/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "name": "tgui-bench", + "version": "4.3.0", + "dependencies": { + "@fastify/static": "^5.0.2", + "common": "workspace:*", + "fastify": "^3.20.2", + "inferno": "^7.4.8", + "inferno-vnode-flags": "^7.4.8", + "lodash": "^4.17.21", + "platform": "^1.3.6", + "tgui": "workspace:*" + } +} diff --git a/tgui/packages/tgui-bench/tests/Button.test.tsx b/tgui/packages/tgui-bench/tests/Button.test.tsx new file mode 100644 index 0000000000..e3472cbbbf --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Button.test.tsx @@ -0,0 +1,99 @@ +import { linkEvent } from 'inferno'; +import { Button } from 'tgui/components'; +import { createRenderer } from 'tgui/renderer'; + +const render = createRenderer(); + +const handleClick = () => undefined; + +export const SingleButton = () => { + const node = ( + + ); + render(node); +}; + +export const SingleButtonWithCallback = () => { + const node = ( + + ); + render(node); +}; + +export const SingleButtonWithLinkEvent = () => { + const node = ( + + ); + render(node); +}; + +export const ListOfButtons = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithCallback = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithLinkEvent = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithIcons = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; + +export const ListOfButtonsWithTooltips = () => { + const nodes: JSX.Element[] = []; + for (let i = 0; i < 100; i++) { + const node = ( + + ); + nodes.push(node); + } + render(
{nodes}
); +}; diff --git a/tgui/packages/tgui-bench/tests/DisposalBin.test.tsx b/tgui/packages/tgui-bench/tests/DisposalBin.test.tsx new file mode 100644 index 0000000000..99367d2016 --- /dev/null +++ b/tgui/packages/tgui-bench/tests/DisposalBin.test.tsx @@ -0,0 +1,28 @@ +import { backendUpdate } from 'tgui/backend'; +import { DisposalBin } from 'tgui/interfaces/DisposalBin'; +import { createRenderer } from 'tgui/renderer'; +import { configureStore, StoreProvider } from 'tgui/store'; + +const store = configureStore({ sideEffets: false }); + +const renderUi = createRenderer((dataJson: string) => { + store.dispatch(backendUpdate({ + data: Byond.parseJson(dataJson), + })); + return ( + + + + ); +}); + +export const data = JSON.stringify({ + flush: 0, + full_pressure: 1, + pressure_charging: 0, + panel_open: 0, + per: 1, + isai: 0, +}); + +export const Default = () => renderUi(data); diff --git a/tgui/packages/tgui-bench/tests/Flex.test.tsx b/tgui/packages/tgui-bench/tests/Flex.test.tsx new file mode 100644 index 0000000000..e81ebc6f61 --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Flex.test.tsx @@ -0,0 +1,18 @@ +import { Flex } from 'tgui/components'; +import { createRenderer } from 'tgui/renderer'; + +const render = createRenderer(); + +export const Default = () => { + const node = ( + + + Text {Math.random()} + + + Text {Math.random()} + + + ); + render(node); +}; diff --git a/tgui/packages/tgui-bench/tests/Stack.test.tsx b/tgui/packages/tgui-bench/tests/Stack.test.tsx new file mode 100644 index 0000000000..952dba2029 --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Stack.test.tsx @@ -0,0 +1,18 @@ +import { Stack } from 'tgui/components'; +import { createRenderer } from 'tgui/renderer'; + +const render = createRenderer(); + +export const Default = () => { + const node = ( + + + Text {Math.random()} + + + Text {Math.random()} + + + ); + render(node); +}; diff --git a/tgui/packages/tgui-bench/tests/Tooltip.test.tsx b/tgui/packages/tgui-bench/tests/Tooltip.test.tsx new file mode 100644 index 0000000000..b953fc911d --- /dev/null +++ b/tgui/packages/tgui-bench/tests/Tooltip.test.tsx @@ -0,0 +1,20 @@ +import { Box, Tooltip } from "tgui/components"; +import { createRenderer } from "tgui/renderer"; + +const render = createRenderer(); + +export const ListOfTooltips = () => { + const nodes: JSX.Element[] = []; + + for (let i = 0; i < 100; i++) { + nodes.push( + + + Tooltip #{i} + + + ); + } + + render(
{nodes}
); +}; diff --git a/tgui/packages/tgui-panel/Notifications.js b/tgui/packages/tgui-panel/Notifications.js deleted file mode 100644 index a64ddd8e30..0000000000 --- a/tgui/packages/tgui-panel/Notifications.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { Flex } from 'tgui/components'; - -export const Notifications = props => { - const { children } = props; - return ( -
- {children} -
- ); -}; - -const NotificationsItem = props => { - const { - rightSlot, - children, - } = props; - return ( - - - {children} - - {rightSlot && ( - - {rightSlot} - - )} - - ); -}; - -Notifications.Item = NotificationsItem; diff --git a/tgui/packages/tgui-panel/Panel.js b/tgui/packages/tgui-panel/Panel.js deleted file mode 100644 index d6cc9eb7ef..0000000000 --- a/tgui/packages/tgui-panel/Panel.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { Button, Section, Stack } from 'tgui/components'; -import { Pane } from 'tgui/layouts'; -import { NowPlayingWidget, useAudio } from './audio'; -import { ChatPanel, ChatTabs } from './chat'; -import { useGame } from './game'; -import { Notifications } from './Notifications'; -import { PingIndicator } from './ping'; -import { SettingsPanel, useSettings } from './settings'; - -export const Panel = (props, context) => { - // IE8-10: Needs special treatment due to missing Flex support - if (Byond.IS_LTE_IE10) { - return ( - - ); - } - const audio = useAudio(context); - const settings = useSettings(context); - const game = useGame(context); - if (process.env.NODE_ENV !== 'production') { - const { useDebug, KitchenSink } = require('tgui/debug'); - const debug = useDebug(context); - if (debug.kitchenSink) { - return ( - - ); - } - } - return ( - - - -
- - - - - - - - -
-
- {audio.visible && ( - -
- -
-
- )} - {settings.visible && ( - - - - )} - -
- - - - - {game.connectionLostAt && ( - Byond.command('.reconnect')}> - Reconnect - - )}> - You are either AFK, experiencing lag or the connection - has closed. - - )} - {game.roundRestartedAt && ( - - The connection has been closed because the server is - restarting. Please wait while you automatically reconnect. - - )} - -
-
-
-
- ); -}; - -const HoboPanel = (props, context) => { - const settings = useSettings(context); - return ( - - - - {settings.visible && ( - - ) || ( - - )} - - - ); -}; diff --git a/tgui/packages/tgui-panel/audio/NowPlayingWidget.js b/tgui/packages/tgui-panel/audio/NowPlayingWidget.js deleted file mode 100644 index e4fe04eed1..0000000000 --- a/tgui/packages/tgui-panel/audio/NowPlayingWidget.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { toFixed } from 'common/math'; -import { useDispatch, useSelector } from 'common/redux'; -import { Button, Flex, Knob } from 'tgui/components'; -import { useSettings } from '../settings'; -import { selectAudio } from './selectors'; - -export const NowPlayingWidget = (props, context) => { - const audio = useSelector(context, selectAudio); - const dispatch = useDispatch(context); - const settings = useSettings(context); - const title = audio.meta?.title; - return ( - - {audio.playing && ( - <> - - Now playing: - - - {title || 'Unknown Track'} - - - ) || ( - - Nothing to play. - - )} - {audio.playing && ( - - - - - -
- {MESSAGE_TYPES - .filter(typeDef => !typeDef.important && !typeDef.admin) - .map(typeDef => ( - dispatch(toggleAcceptedType({ - pageId: page.id, - type: typeDef.type, - }))}> - {typeDef.name} - - ))} - - {MESSAGE_TYPES - .filter(typeDef => !typeDef.important && typeDef.admin) - .map(typeDef => ( - dispatch(toggleAcceptedType({ - pageId: page.id, - type: typeDef.type, - }))}> - {typeDef.name} - - ))} - -
- - ); -}; diff --git a/tgui/packages/tgui-panel/chat/ChatPanel.js b/tgui/packages/tgui-panel/chat/ChatPanel.js deleted file mode 100644 index 0a5deaf9fe..0000000000 --- a/tgui/packages/tgui-panel/chat/ChatPanel.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { shallowDiffers } from 'common/react'; -import { Component, createRef } from 'inferno'; -import { Button } from 'tgui/components'; -import { chatRenderer } from './renderer'; - -export class ChatPanel extends Component { - constructor() { - super(); - this.ref = createRef(); - this.state = { - scrollTracking: true, - }; - this.handleScrollTrackingChange = value => this.setState({ - scrollTracking: value, - }); - } - - componentDidMount() { - chatRenderer.mount(this.ref.current); - chatRenderer.events.on('scrollTrackingChanged', - this.handleScrollTrackingChange); - this.componentDidUpdate(); - } - - componentWillUnmount() { - chatRenderer.events.off('scrollTrackingChanged', - this.handleScrollTrackingChange); - } - - componentDidUpdate(prevProps) { - requestAnimationFrame(() => { - chatRenderer.ensureScrollTracking(); - }); - const shouldUpdateStyle = ( - !prevProps || shallowDiffers(this.props, prevProps) - ); - if (shouldUpdateStyle) { - chatRenderer.assignStyle({ - 'width': '100%', - 'white-space': 'pre-wrap', - 'font-size': this.props.fontSize, - 'line-height': this.props.lineHeight, - }); - } - } - - render() { - const { - scrollTracking, - } = this.state; - return ( - <> -
- {!scrollTracking && ( - - )} - - ); - } -} diff --git a/tgui/packages/tgui-panel/chat/ChatTabs.js b/tgui/packages/tgui-panel/chat/ChatTabs.js deleted file mode 100644 index a0e6cc59e5..0000000000 --- a/tgui/packages/tgui-panel/chat/ChatTabs.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { useDispatch, useSelector } from 'common/redux'; -import { Box, Tabs, Flex, Button } from 'tgui/components'; -import { changeChatPage, addChatPage } from './actions'; -import { selectChatPages, selectCurrentChatPage } from './selectors'; -import { openChatSettings } from '../settings/actions'; - -const UnreadCountWidget = ({ value }) => ( - - {Math.min(value, 99)} - -); - -export const ChatTabs = (props, context) => { - const pages = useSelector(context, selectChatPages); - const currentPage = useSelector(context, selectCurrentChatPage); - const dispatch = useDispatch(context); - return ( - - - - {pages.map(page => ( - 0 && ( - - )} - onClick={() => dispatch(changeChatPage({ - pageId: page.id, - }))}> - {page.name} - - ))} - - - -