"
- output += "
[global.using_map.get_map_info()]"
- output +="
"
- output += "
Setup Character "
-
- if(GAME_STATE > RUNLEVEL_LOBBY)
- output += "
View the Crew Manifest "
-
- output += "
Observe "
-
- output += "
Current character:
[client.prefs.real_name][client.prefs.job_high ? ", [client.prefs.job_high]" : null]
"
- if(GAME_STATE <= RUNLEVEL_LOBBY)
- if(ready)
- output += "
Un-Ready"
- else
- output += "
Ready Up"
- else
- output += "
Join Game!"
+ var/output = list("
")
+
+ var/decl/lobby_handler/lobby_handler = GET_DECL(global.using_map.lobby_handler)
+ var/lobby_header = lobby_handler.get_lobby_header(src)
+ if(lobby_header)
+ output += lobby_header
+ for(var/datum/lobby_option/option in lobby_handler.lobby_options)
+ if(!option.visible(src))
+ continue
+ var/option_string = option.get_lobby_menu_string(src)
+ if(option_string)
+ output += option_string
+ var/lobby_footer = lobby_handler.get_lobby_footer(src)
+ if(lobby_footer)
+ output += lobby_footer
output += "
"
panel = new(src, "Welcome","Welcome to [global.using_map.full_name]", 560, 280, src)
@@ -370,10 +366,6 @@ INITIALIZE_IMMEDIATE(/mob/new_player)
spawning = 0 //abort
return null
new_character = new(spawn_turf, chosen_species.name)
- if(chosen_species.has_organ[BP_POSIBRAIN] && client && client.prefs.is_shackled)
- var/obj/item/organ/internal/posibrain/B = new_character.get_organ(BP_POSIBRAIN, /obj/item/organ/internal/posibrain)
- if(B)
- B.shackle(client.prefs.get_lawset())
if(!new_character)
new_character = new(spawn_turf)
@@ -488,3 +480,7 @@ INITIALIZE_IMMEDIATE(/mob/new_player)
/hook/roundstart/proc/update_lobby_browsers()
global.using_map.refresh_lobby_browsers()
return TRUE
+
+/mob/new_player/change_mob_type(var/new_type, var/turf/location, var/new_name, var/delete_old_mob = FALSE, var/subspecies)
+ to_chat(usr, SPAN_WARNING("You cannot convert players who have not entered the game yet!"))
+ return FALSE
diff --git a/code/modules/mob/new_player/preferences_setup.dm b/code/modules/mob/new_player/preferences_setup.dm
index 89e22c0e6fe..2f46e5c436a 100644
--- a/code/modules/mob/new_player/preferences_setup.dm
+++ b/code/modules/mob/new_player/preferences_setup.dm
@@ -33,7 +33,8 @@
if(istype(descriptor))
appearance_descriptors[descriptor.name] = descriptor.randomize_value()
- backpack = GET_DECL(pick(subtypesof(/decl/backpack_outfit)))
+ var/list/all_backpacks = decls_repository.get_decls_of_subtype(/decl/backpack_outfit)
+ backpack = all_backpacks[pick(all_backpacks)]
b_type = pickweight(current_species.blood_types)
if(H)
copy_to(H)
diff --git a/code/modules/mob/observer/eye/blueprints_eye.dm b/code/modules/mob/observer/eye/blueprints_eye.dm
index 5f84f16e311..e478d4c7472 100644
--- a/code/modules/mob/observer/eye/blueprints_eye.dm
+++ b/code/modules/mob/observer/eye/blueprints_eye.dm
@@ -82,7 +82,8 @@
return
to_chat(owner, SPAN_NOTICE("You scrub [A.proper_name] off the blueprints."))
log_and_message_admins("deleted area [A.proper_name] via station blueprints.")
- qdel(A)
+ for(var/turf/T in A.contents)
+ ChangeArea(T, global.space_area)
/mob/observer/eye/blueprints/proc/edit_area()
var/area/A = get_area(src)
diff --git a/code/modules/mob/observer/eye/eye.dm b/code/modules/mob/observer/eye/eye.dm
index 3dabaf05660..99c20856052 100644
--- a/code/modules/mob/observer/eye/eye.dm
+++ b/code/modules/mob/observer/eye/eye.dm
@@ -15,11 +15,12 @@
var/acceleration = 1
var/owner_follows_eye = 0
var/living_eye = TRUE // Whether or not the eye uses normal living vision handling.
-
+
var/click_handler_type = /datum/click_handler/eye/ // Set if the eye uses special click handling. Distinct from parent mob click handling for AI etc.
-
+
see_in_dark = 7
invisibility = INVISIBILITY_EYE
+ is_spawnable_type = FALSE // No, don't.
ghost_image_flag = GHOST_IMAGE_ALL
var/mob/owner = null
@@ -159,7 +160,7 @@
/mob/observer/eye/ClickOn(var/atom/A, var/params)
if(owner)
return owner.ClickOn(A, params)
-
+
return ..()
/datum/click_handler/eye/OnClick(var/atom/A, params)
diff --git a/code/modules/mob/observer/ghost/ghost.dm b/code/modules/mob/observer/ghost/ghost.dm
index 98128937a19..dff96ea14cc 100644
--- a/code/modules/mob/observer/ghost/ghost.dm
+++ b/code/modules/mob/observer/ghost/ghost.dm
@@ -11,13 +11,12 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images
anchored = 1 // don't get pushed around
universal_speak = TRUE
mob_sort_value = 9
+ is_spawnable_type = FALSE
mob_flags = MOB_FLAG_HOLY_BAD
movement_handlers = list(/datum/movement_handler/mob/multiz_connected, /datum/movement_handler/mob/incorporeal)
- var/next_visibility_toggle = 0
var/can_reenter_corpse
- var/bootime = 0
var/started_as_observer //This variable is set to 1 when you enter the game as an observer.
//If you died in the game and are a ghost - this will remain as null.
//Note that this is not a reliable way to determine if admins started as observers, since they change mobs a lot.
@@ -57,8 +56,9 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images
mind = body.mind //we don't transfer the mind but we keep a reference to it.
else
spawn(10) // wait for the observer mob to receive the client's key
- mind = new /datum/mind(key)
- mind.current = src
+ if(!QDELETED(src))
+ mind = new /datum/mind(key)
+ mind.current = src
if(!T)
var/list/spawn_locs = global.latejoin_locations | global.latejoin_cryo_locations | global.latejoin_gateway_locations
if(length(spawn_locs))
diff --git a/code/modules/mob/observer/observer.dm b/code/modules/mob/observer/observer.dm
index 6c15f3fc371..d5456a928ad 100644
--- a/code/modules/mob/observer/observer.dm
+++ b/code/modules/mob/observer/observer.dm
@@ -20,7 +20,7 @@ var/global/const/GHOST_IMAGE_ALL = ~GHOST_IMAGE_NONE
/mob/observer/Initialize()
. = ..()
- glide_size = 0
+ glide_size = 0 // Set in Initialize() because the compiler doesn't like it set in the definition.
ghost_image = image(src.icon,src)
ghost_image.plane = plane
ghost_image.layer = layer
@@ -95,3 +95,6 @@ var/global/const/GHOST_IMAGE_ALL = ~GHOST_IMAGE_NONE
/mob/observer/set_glide_size(var/delay)
glide_size = 0
+
+/mob/observer/get_speech_bubble_state_modifier()
+ return "ghost"
diff --git a/code/modules/mob/observer/virtual/base.dm b/code/modules/mob/observer/virtual/base.dm
index d9b8c71f386..0fecb711fde 100644
--- a/code/modules/mob/observer/virtual/base.dm
+++ b/code/modules/mob/observer/virtual/base.dm
@@ -6,6 +6,7 @@ var/global/list/all_virtual_listeners = list()
see_in_dark = SEE_IN_DARK_DEFAULT
see_invisible = SEE_INVISIBLE_LIVING
sight = SEE_SELF
+ is_spawnable_type = FALSE // needs a host
virtual_mob = null
z_flags = ZMM_IGNORE
@@ -13,7 +14,6 @@ var/global/list/all_virtual_listeners = list()
var/atom/movable/host
var/host_type = /atom/movable
var/abilities = VIRTUAL_ABILITY_HEAR|VIRTUAL_ABILITY_SEE
- var/list/broadcast_methods
var/static/list/overlay_icons
diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm
index beb53ac8ddf..10e7f46608a 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -9,7 +9,7 @@
/mob/verb/say_verb(message as text)
set name = "Say"
set category = "IC"
- remove_typing_indicator()
+ SStyping.set_indicator_state(client, FALSE)
if(!filter_block_message(usr, message))
usr.say(message)
@@ -17,7 +17,7 @@
set name = "Me"
set category = "IC"
- remove_typing_indicator()
+ SStyping.set_indicator_state(client, FALSE)
if(!filter_block_message(usr, message))
message = sanitize(message)
if(use_me)
@@ -28,41 +28,17 @@
/mob/proc/say_dead(var/message)
communicate(/decl/communication_channel/dsay, client, message)
-/mob/proc/say_understands(var/mob/other,var/decl/language/speaking = null)
-
- if (src.stat == 2) //Dead
- return 1
-
- //Universal speak makes everything understandable, for obvious reasons.
- else if(src.universal_speak || src.universal_understand)
- return 1
-
- //Languages are handled after.
- if (!speaking)
- if(!other)
- return 1
- if(other.universal_speak)
- return 1
- if(isAI(src) && ispAI(other))
- return 1
- if (istype(other, src.type) || istype(src, other.type))
- return 1
- return 0
-
- if(speaking.flags & INNATE)
- return 1
-
- //Language check.
- for(var/decl/language/L in src.languages)
- if(speaking.name == L.name)
- return 1
-
- return 0
+/mob/proc/say_understands(mob/speaker, decl/language/speaking)
+ if(stat == DEAD || universal_speak || universal_understand)
+ return TRUE
+ if(speaking)
+ return speaking.can_be_understood_by(speaker, src)
+ return (!speaker || speaker.universal_speak || istype(speaker, type) || istype(src, speaker.type))
/mob/proc/say_quote(var/message, var/decl/language/speaking = null)
var/ending = copytext(message, length(message))
if(speaking)
- return speaking.get_spoken_verb(ending)
+ return speaking.get_spoken_verb(src, ending)
var/verb = pick(speak_emote)
if(verb == "says") //a little bit of a hack, but we can't let speak_emote default to an empty list without breaking other things
@@ -80,13 +56,13 @@
return get_turf(src)
-/mob/proc/say_test(var/text)
+/mob/proc/check_speech_punctuation_state(var/text)
var/ending = copytext(text, length(text))
if (ending == "?")
- return "1"
+ return "question"
else if (ending == "!")
- return "2"
- return "0"
+ return "exclamation"
+ return "statement"
//parses the message mode code (e.g. :h, :w) from text, such as that supplied to say.
//returns the message mode string or null for no message mode.
diff --git a/code/modules/mob/skills/skill.dm b/code/modules/mob/skills/skill.dm
index 4aab41ac741..113558d9389 100644
--- a/code/modules/mob/skills/skill.dm
+++ b/code/modules/mob/skills/skill.dm
@@ -4,6 +4,7 @@ var/global/list/skills = list()
name = "None" // Name of the skill. This is what the player sees.
abstract_type = /decl/hierarchy/skill // Don't mess with this without changing how Initialize works.
+ expected_type = /decl/hierarchy/skill
var/sort_priority = 0 // Used for sort order in lists/presentation.
var/desc = "Placeholder skill" // Generic description of this skill.
var/difficulty = SKILL_AVERAGE //Used to compute how expensive the skill is
@@ -34,7 +35,7 @@ var/global/list/skills = list()
/decl/hierarchy/skill/Initialize()
. = ..()
GET_DECL(/decl/hierarchy/skill) // Make sure the full skill decl list is populated.
- if(is_abstract())
+ if(INSTANCE_IS_ABSTRACT(src))
for(var/decl/hierarchy/skill/C in children)
global.skills |= C.get_descendents()
diff --git a/code/modules/mob/skills/skill_buffs.dm b/code/modules/mob/skills/skill_buffs.dm
index eea8a06cbea..18f538055c3 100644
--- a/code/modules/mob/skills/skill_buffs.dm
+++ b/code/modules/mob/skills/skill_buffs.dm
@@ -22,7 +22,7 @@
for(var/skill_type in temp_buffs)
var/has_now = target.get_skill_value(skill_type)
var/current_buff = buffs[skill_type]
- var/new_buff = Clamp(has_now + current_buff, SKILL_MIN, SKILL_MAX) - has_now
+ var/new_buff = clamp(has_now + current_buff, SKILL_MIN, SKILL_MAX) - has_now
new_buff ? (buffs[skill_type] = new_buff) : (buffs -= skill_type)
return length(buffs)
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index de63c3bc30e..51ddf615dfe 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -274,7 +274,6 @@
adjust_blood(species.blood_volume - vessel.total_volume)
for (var/o in get_external_organs())
var/obj/item/organ/organ = o
- organ.vital = 0
if (!BP_IS_PROSTHETIC(organ))
organ.rejuvenate(1)
organ.max_damage *= 3
diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm
deleted file mode 100644
index 11282ca1809..00000000000
--- a/code/modules/mob/typing_indicator.dm
+++ /dev/null
@@ -1,77 +0,0 @@
-/*Typing indicators, when a mob uses the F3/F4 keys to bring the say/emote input boxes up this little buddy is
-made and follows them around until they are done (or something bad happens), helps tell nearby people that 'hey!
-I IS TYPIN'!'
-*/
-
-/mob
- var/atom/movable/overlay/typing_indicator/typing_indicator = null
-
-/atom/movable/overlay/typing_indicator
- follow_proc = /atom/movable/proc/move_to_turf_or_null
- icon = 'icons/mob/talk.dmi'
- icon_state = "typing"
-
-/atom/movable/overlay/typing_indicator/Initialize()
- . = ..()
- if(!istype(master, /mob))
- PRINT_STACK_TRACE("Master of typing_indicator has invalid type: [master.type].")
-
-/atom/movable/overlay/typing_indicator/Destroy()
- var/mob/M = master
- M.typing_indicator = null
- . = ..()
-
-/atom/movable/overlay/typing_indicator/SetInitLoc()
- forceMove(get_turf(master))
-
-/mob/proc/create_typing_indicator()
- if(client && !stat && get_preference_value(/datum/client_preference/show_typing_indicator) == PREF_SHOW && !src.is_cloaked() && isturf(src.loc))
- if(!typing_indicator)
- typing_indicator = new(src)
- typing_indicator.set_invisibility(0)
-
- var/matrix/M = matrix()
- M.Scale(0,0)
- typing_indicator.transform = M
- typing_indicator.alpha = 0
- animate(typing_indicator, transform = 0, alpha = 255, time = 0.2 SECONDS, easing = EASE_IN)
-
-/mob/proc/remove_typing_indicator() // A bit excessive, but goes with the creation of the indicator I suppose
- if(typing_indicator)
- animate(typing_indicator, alpha = 0, time = 0.5 SECONDS, easing = EASE_IN)
- addtimer(CALLBACK(typing_indicator, /atom/proc/set_invisibility, INVISIBILITY_MAXIMUM), 0.5 SECONDS)
-
-/mob/set_stat(new_stat)
- . = ..()
- if(.)
- remove_typing_indicator()
-
-/mob/verb/say_wrapper()
- set name = ".Say"
- set hidden = 1
-
- create_typing_indicator()
- var/message = input("","say (text)") as text|null
- remove_typing_indicator()
- if(message)
- say_verb(message)
-
-/mob/verb/whisper_wrapper()
- set name = ".Whisper"
- set hidden = 1
-
- create_typing_indicator()
- var/message = input(src, "", "whisper (text)") as text|null
- remove_typing_indicator()
- if(message)
- whisper(message)
-
-/mob/verb/me_wrapper()
- set name = ".Me"
- set hidden = 1
-
- create_typing_indicator()
- var/message = input("","me (text)") as text|null
- remove_typing_indicator()
- if(message)
- me_verb(message)
diff --git a/code/modules/mob_holder/_holder.dm b/code/modules/mob_holder/_holder.dm
index c6db17b37f5..c6adb41d6be 100644
--- a/code/modules/mob_holder/_holder.dm
+++ b/code/modules/mob_holder/_holder.dm
@@ -10,6 +10,7 @@
origin_tech = "{'biotech':1}"
use_single_icon = TRUE
item_state = null
+ is_spawnable_type = FALSE
var/last_holder
/obj/item/holder/Initialize()
diff --git a/code/modules/modular_computers/computers/modular_computer/assembly_computer.dm b/code/modules/modular_computers/computers/modular_computer/assembly_computer.dm
index 5d1a810562f..0e4cc01553a 100644
--- a/code/modules/modular_computers/computers/modular_computer/assembly_computer.dm
+++ b/code/modules/modular_computers/computers/modular_computer/assembly_computer.dm
@@ -35,15 +35,13 @@
return
. = ..()
if(.)
- if(istype(P, /obj/item/stock_parts/computer/scanner))
- var/obj/item/stock_parts/computer/scanner/scanner = P
- scanner.do_after_install(user, holder)
+ P.do_after_install(holder, !!user)
return TRUE
/datum/extension/assembly/modular_computer/uninstall_component(var/mob/living/user, var/obj/item/stock_parts/P)
- if(istype(P, /obj/item/stock_parts/computer/scanner))
- var/obj/item/stock_parts/computer/scanner/scanner = P
- scanner.do_before_uninstall()
+ if(istype(P, /obj/item/stock_parts/computer))
+ var/obj/item/stock_parts/computer/C = P
+ C.do_before_uninstall(holder, !!user)
return ..()
/datum/extension/assembly/modular_computer/critical_shutdown()
diff --git a/code/modules/modular_computers/computers/modular_computer/assembly_telescreen.dm b/code/modules/modular_computers/computers/modular_computer/assembly_telescreen.dm
deleted file mode 100644
index da391684fb9..00000000000
--- a/code/modules/modular_computers/computers/modular_computer/assembly_telescreen.dm
+++ /dev/null
@@ -1,11 +0,0 @@
-/datum/extension/assembly/modular_computer/telescreen
- hardware_flag = PROGRAM_TELESCREEN
- max_hardware_size = 2
- base_idle_power_usage = 75
- base_active_power_usage = 300
- steel_sheet_cost = 10
- max_damage = 300
-
-/datum/extension/assembly/modular_computer/telescreen/New()
- broken_damage = max_damage / 2
- ..()
\ No newline at end of file
diff --git a/code/modules/modular_computers/computers/modular_computer/core.dm b/code/modules/modular_computers/computers/modular_computer/core.dm
index 494d12656b4..885b424103f 100644
--- a/code/modules/modular_computers/computers/modular_computer/core.dm
+++ b/code/modules/modular_computers/computers/modular_computer/core.dm
@@ -47,7 +47,7 @@
var/datum/computer_file/program/prog_file = prog_type
if(initial(prog_file.usage_flags) & assembly.hardware_flag)
prog_file = new prog_file
- HDD.store_file(prog_file)
+ HDD.store_file(prog_file, OS_PROGRAMS_DIR, TRUE)
/obj/item/modular_computer/Initialize()
START_PROCESSING(SSobj, src)
@@ -89,7 +89,7 @@
return 1
/obj/item/modular_computer/on_update_icon()
- cut_overlays()
+ . = ..()
for(var/decal_state in decals)
var/image/I = image(icon, "[icon_state]-[decal_state]")
I.color = decals[decal_state]
diff --git a/code/modules/modular_computers/computers/modular_computer/interaction.dm b/code/modules/modular_computers/computers/modular_computer/interaction.dm
index 4e9b7b76b6e..883ba315be1 100644
--- a/code/modules/modular_computers/computers/modular_computer/interaction.dm
+++ b/code/modules/modular_computers/computers/modular_computer/interaction.dm
@@ -46,7 +46,7 @@
to_chat(usr, "
You can't reach it.")
return
- if(istype(stored_pen))
+ if(IS_PEN(stored_pen))
to_chat(usr, "
You remove [stored_pen] from [src].")
usr.put_in_hands(stored_pen) // Silicons will drop it anyway.
stored_pen = null
@@ -86,7 +86,7 @@
update_verbs()
return
- if(istype(W, /obj/item/pen) && stores_pen)
+ if(IS_PEN(W) && (W.w_class <= ITEM_SIZE_TINY) && stores_pen)
if(istype(stored_pen))
to_chat(user, "
There is already a pen in [src].")
return
@@ -134,14 +134,19 @@
/obj/item/modular_computer/get_alt_interactions(var/mob/user)
. = ..()
LAZYADD(., /decl/interaction_handler/remove_id/modular_computer)
+ LAZYADD(., /decl/interaction_handler/remove_pen/modular_computer)
+ LAZYADD(., /decl/interaction_handler/emergency_shutdown)
+//
+// Remove ID
+//
/decl/interaction_handler/remove_id/modular_computer
expected_target_type = /obj/item/modular_computer
/decl/interaction_handler/remove_id/modular_computer/is_possible(atom/target, mob/user, obj/item/prop)
. = ..()
if(.)
- var/datum/extension/assembly/assembly = get_extension(src, /datum/extension/assembly)
+ var/datum/extension/assembly/assembly = get_extension(target, /datum/extension/assembly)
. = !!(assembly?.get_component(PART_CARD))
/decl/interaction_handler/remove_id/modular_computer/invoked(atom/target, mob/user, obj/item/prop)
@@ -149,3 +154,34 @@
var/obj/item/stock_parts/computer/card_slot/card_slot = assembly.get_component(PART_CARD)
if(card_slot.stored_card)
card_slot.eject_id(user)
+
+//
+// Remove Pen
+//
+/decl/interaction_handler/remove_pen/modular_computer
+ expected_target_type = /obj/item/modular_computer
+
+/decl/interaction_handler/remove_pen/modular_computer/is_possible(obj/item/modular_computer/target, mob/user, obj/item/prop)
+ return ..() && target.stores_pen && target.stored_pen
+
+/decl/interaction_handler/remove_pen/modular_computer/invoked(obj/item/modular_computer/target, mob/user, obj/item/prop)
+ target.remove_pen()
+
+//
+// Emergency Shutdown
+//
+/decl/interaction_handler/emergency_shutdown
+ name = "Emergency Shutdown"
+ icon = 'icons/screen/radial.dmi'
+ icon_state = "radial_power_off"
+ expected_target_type = /obj/item/modular_computer
+
+/decl/interaction_handler/emergency_shutdown/is_possible(atom/target, mob/user, obj/item/prop)
+ . = ..()
+ if(!.)
+ return
+ var/datum/extension/assembly/modular_computer/assembly = get_extension(target, /datum/extension/assembly)
+ return !isnull(assembly) && assembly.enabled
+
+/decl/interaction_handler/emergency_shutdown/invoked(obj/item/modular_computer/target, mob/user, obj/item/prop)
+ target.emergency_shutdown()
\ No newline at end of file
diff --git a/code/modules/modular_computers/computers/modular_computer/variables.dm b/code/modules/modular_computers/computers/modular_computer/variables.dm
index 5bfb6b817f6..f8c88612879 100644
--- a/code/modules/modular_computers/computers/modular_computer/variables.dm
+++ b/code/modules/modular_computers/computers/modular_computer/variables.dm
@@ -6,6 +6,7 @@
icon_state = ICON_STATE_WORLD
center_of_mass = null
randpixel = 0
+ material = /decl/material/solid/plastic
var/screen_icon
var/list/decals
@@ -19,5 +20,4 @@
var/interact_sound_volume = 40
var/list/default_hardware = list()
var/list/default_programs = list()
- var/initial_hardware_flag = 0
var/computer_type = /datum/extension/assembly/modular_computer
diff --git a/code/modules/modular_computers/computers/subtypes/dev_console.dm b/code/modules/modular_computers/computers/subtypes/dev_console.dm
index b43928efee6..8e3b1abef44 100644
--- a/code/modules/modular_computers/computers/subtypes/dev_console.dm
+++ b/code/modules/modular_computers/computers/subtypes/dev_console.dm
@@ -1,11 +1,15 @@
/obj/machinery/computer/modular
name = "modular console"
- maximum_component_parts = list(/obj/item/stock_parts = 14) //There's a lot of stuff that goes in these
- var/list/interact_sounds = list("keyboard", "keystroke")
- var/wired_connection = FALSE // Whether or not this console will start with a wired connection beneath it.
+ maximum_component_parts = list(/obj/item/stock_parts = 14) //There's a lot of stuff that goes in these
+ icon = 'icons/obj/modular_computers/modular_console.dmi'
+ icon_state = "console-off"
+ var/list/interact_sounds = list("keyboard", "keystroke")
+ var/wired_connection = FALSE // Whether or not this console will start with a wired connection beneath it.
+ var/tmp/max_hardware_size = 3 //Enum to tell whether computer parts are too big to fit in this machine.
+ var/tmp/os_type = /datum/extension/interactive/os/console //The type of the OS extension to create for this machine.
/obj/machinery/computer/modular/Initialize()
- set_extension(src, /datum/extension/interactive/os/console)
+ set_extension(src, os_type)
. = ..()
/obj/machinery/computer/modular/populate_parts(full_populate)
@@ -82,6 +86,15 @@
if(os)
os.open_terminal(user)
+//Check for handling wall-mounted modular computer stuff
+/obj/machinery/computer/modular/can_add_component(obj/item/stock_parts/component, mob/user)
+ var/obj/item/stock_parts/computer/C = component
+ if(istype(C))
+ if(C.hardware_size > max_hardware_size)
+ to_chat(user, "This component is too large for \the [src].")
+ return 0
+ . = ..()
+
/obj/machinery/computer/modular/verb/emergency_shutdown()
set name = "Forced Shutdown"
set category = "Object"
diff --git a/code/modules/modular_computers/computers/subtypes/dev_holo.dm b/code/modules/modular_computers/computers/subtypes/dev_holo.dm
index 084ea62792e..d74e6d13fee 100644
--- a/code/modules/modular_computers/computers/subtypes/dev_holo.dm
+++ b/code/modules/modular_computers/computers/subtypes/dev_holo.dm
@@ -50,14 +50,14 @@
preset.filename = "temp[rand(1,100)]"
preset.stored_data = preset_text
- D.store_file(preset)
- var/datum/computer_file/program/wordprocessor/word = D.find_file_by_name("wordprocessor")
+ D.store_file(preset, OS_DOCUMENTS_DIR)
+ var/datum/computer_file/program/wordprocessor/word = D.find_file_by_name("wordprocessor", OS_PROGRAMS_DIR)
word.open_file = preset.filename
word.loaded_data = preset.stored_data
// Visual.
/obj/item/modular_computer/holotablet/on_update_icon()
- cut_overlays()
+ . = ..()
var/datum/extension/interactive/os/os = get_extension(src, /datum/extension/interactive/os)
var/datum/extension/assembly/modular_computer/assembly = get_extension(src, /datum/extension/assembly)
if(assembly && assembly.enabled)
diff --git a/code/modules/modular_computers/computers/subtypes/dev_laptop.dm b/code/modules/modular_computers/computers/subtypes/dev_laptop.dm
index a53020e0b89..ad7c698d6d8 100644
--- a/code/modules/modular_computers/computers/subtypes/dev_laptop.dm
+++ b/code/modules/modular_computers/computers/subtypes/dev_laptop.dm
@@ -9,6 +9,11 @@
interact_sounds = list("keyboard", "keystroke")
interact_sound_volume = 20
computer_type = /datum/extension/assembly/modular_computer/laptop
+ matter = list(
+ /decl/material/solid/metal/aluminium = MATTER_AMOUNT_SECONDARY,
+ /decl/material/solid/metal/copper = MATTER_AMOUNT_REINFORCEMENT,
+ /decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT,
+ )
var/icon_state_closed = "laptop-closed"
/obj/item/modular_computer/laptop/on_update_icon()
diff --git a/code/modules/modular_computers/computers/subtypes/dev_pda.dm b/code/modules/modular_computers/computers/subtypes/dev_pda.dm
index 7e574f5fbe0..859bc5eef12 100644
--- a/code/modules/modular_computers/computers/subtypes/dev_pda.dm
+++ b/code/modules/modular_computers/computers/subtypes/dev_pda.dm
@@ -34,10 +34,5 @@
desc = "A box of spare PDA microcomputers."
icon_state = "pdabox"
-/obj/item/storage/box/PDAs/Initialize()
- . = ..()
- new /obj/item/modular_computer/pda(src)
- new /obj/item/modular_computer/pda(src)
- new /obj/item/modular_computer/pda(src)
- new /obj/item/modular_computer/pda(src)
- new /obj/item/modular_computer/pda(src)
+/obj/item/storage/box/PDAs/WillContain()
+ return list(/obj/item/modular_computer/pda = 5)
diff --git a/code/modules/modular_computers/computers/subtypes/dev_tablet.dm b/code/modules/modular_computers/computers/subtypes/dev_tablet.dm
index 41d1c06193c..d30e47c9eda 100644
--- a/code/modules/modular_computers/computers/subtypes/dev_tablet.dm
+++ b/code/modules/modular_computers/computers/subtypes/dev_tablet.dm
@@ -3,13 +3,16 @@
desc = "A small, portable microcomputer."
icon = 'icons/obj/modular_computers/modular_tablet.dmi'
icon_state = "tablet"
-
w_class = ITEM_SIZE_SMALL
light_strength = 2 // same as PDAs
-
interact_sounds = list('sound/machines/pda_click.ogg')
interact_sound_volume = 20
computer_type = /datum/extension/assembly/modular_computer/tablet
+ matter = list(
+ /decl/material/solid/metal/aluminium = MATTER_AMOUNT_SECONDARY,
+ /decl/material/solid/metal/copper = MATTER_AMOUNT_REINFORCEMENT,
+ /decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT,
+ )
/obj/item/modular_computer/tablet/lease
desc = "A small, portable microcomputer. This one has a gold and blue stripe, and a serial number stamped into the case."
diff --git a/code/modules/modular_computers/computers/subtypes/dev_telescreen.dm b/code/modules/modular_computers/computers/subtypes/dev_telescreen.dm
index f127f49b293..d9c110eb7da 100644
--- a/code/modules/modular_computers/computers/subtypes/dev_telescreen.dm
+++ b/code/modules/modular_computers/computers/subtypes/dev_telescreen.dm
@@ -1,54 +1,116 @@
-/obj/item/modular_computer/telescreen
- name = "telescreen"
- desc = "A wall-mounted touchscreen computer."
+//////////////////////////////////////////////////////////////////
+// Telescreen Circuit
+//////////////////////////////////////////////////////////////////
+
+/obj/item/stock_parts/circuitboard/modular_computer/telescreen
+ name = "circuitboard (modular telescreen)"
+ board_type = "wall"
+ build_path = /obj/machinery/computer/modular/telescreen
+ req_components = list(
+ /obj/item/stock_parts/console_screen = 1,
+ /obj/item/stock_parts/computer/processor_unit = 1
+ )
+ additional_spawn_components = list(
+ /obj/item/stock_parts/power/apc/buildable = 1,
+ /obj/item/stock_parts/computer/network_card = 1,
+ /obj/item/stock_parts/computer/hard_drive/super = 1
+ )
+
+//////////////////////////////////////////////////////////////////
+// Telescreen Frame
+//////////////////////////////////////////////////////////////////
+
+/obj/item/frame/modular_telescreen
+ name = "modular telescreen frame"
+ desc = "Used for building wall-mounted modular telescreen computers."
icon = 'icons/obj/modular_computers/modular_telescreen.dmi'
- icon_state = "telescreen"
- anchored = TRUE
- density = 0
- light_strength = 4
- w_class = ITEM_SIZE_HUGE
- computer_type = /datum/extension/assembly/modular_computer/telescreen
-
-/obj/item/modular_computer/telescreen/Initialize()
+ icon_state = "frame"
+ build_machine_type = /obj/machinery/computer/modular/telescreen
+
+/obj/item/frame/modular_telescreen/kit
+ fully_construct = TRUE
+ name = "modular telescreen kit"
+ desc = "An all-in-one wall-mounted modular telescreen computer kit, comes preassembled."
+ icon_state = "frame_kit"
+
+//////////////////////////////////////////////////////////////////
+// Telescreen OS
+//////////////////////////////////////////////////////////////////
+
+/datum/extension/interactive/os/console/telescreen
+ screen_icon_file = 'icons/obj/modular_computers/modular_telescreen.dmi'
+ expected_type = /obj/machinery/computer/modular/telescreen
+
+/datum/extension/interactive/os/console/telescreen/get_hardware_flag()
+ return PROGRAM_TELESCREEN
+
+//////////////////////////////////////////////////////////////////
+// Telescreen Computer
+//////////////////////////////////////////////////////////////////
+
+/obj/machinery/computer/modular/telescreen
+ name = "telescreen"
+ desc = "A wall-mounted touchscreen computer."
+ icon = 'icons/obj/modular_computers/modular_telescreen.dmi'
+ icon_state = "telescreen"
+ anchored = TRUE
+ density = FALSE
+ obj_flags = OBJ_FLAG_MOVES_UNSUPPORTED
+ directional_offset = "{'NORTH':{'y':-20}, 'SOUTH':{'y':24}, 'EAST':{'x':-24}, 'WEST':{'x':24}}"
+ idle_power_usage = 75
+ active_power_usage = 300
+ max_hardware_size = 2 //make sure we can only put smaller components in here
+ construct_state = /decl/machine_construction/wall_frame/panel_closed
+ base_type = /obj/machinery/computer/modular/telescreen
+ frame_type = /obj/item/frame/modular_telescreen
+ //Behaves like a touchscreen
+ stat_immune = NOINPUT
+ icon_keyboard = null
+ interact_sounds = null
+ clicksound = null
+ required_interaction_dexterity = DEXTERITY_TOUCHSCREENS
+ os_type = /datum/extension/interactive/os/console/telescreen
+
+/obj/machinery/computer/modular/telescreen/update_directional_offset(force = FALSE)
+ if(!force && (!length(directional_offset) || !is_wall_mounted()))
+ return
. = ..()
- // Allows us to create "north bump" "south bump" etc. named objects, for more comfortable mapping.
- name = "telescreen"
-
-/obj/item/modular_computer/telescreen/attackby(var/obj/item/W, var/mob/user)
- var/datum/extension/assembly/modular_computer/assembly = get_extension(src, /datum/extension/assembly/modular_computer)
- if(IS_CROWBAR(W))
- if(anchored)
- shutdown_computer()
- anchored = FALSE
- assembly.screen_on = FALSE
- default_pixel_x = 0
- default_pixel_y = 0
- reset_offsets(0)
- to_chat(user, "You unsecure \the [src].")
- else
- var/choice = input(user, "Where do you want to place \the [src]?", "Offset selection") in list("North", "South", "West", "East", "This tile", "Cancel")
- switch(choice)
- if("North")
- default_pixel_x = 0
- default_pixel_y = 32
- if("South")
- default_pixel_x = 0
- default_pixel_y = -32
- if("West")
- default_pixel_x = -32
- default_pixel_y = 0
- if("East")
- default_pixel_x = 32
- default_pixel_y = 0
- if("This tile")
- default_pixel_x = 0
- default_pixel_y = 0
- else
- return
- reset_offsets(0)
-
- anchored = TRUE
- assembly.screen_on = TRUE
- to_chat(user, "You secure \the [src].")
- return
- ..()
\ No newline at end of file
+
+/obj/machinery/computer/modular/telescreen/on_update_icon()
+ cut_overlays()
+ icon_state = initial(icon_state)
+
+ var/can_see_circuit = FALSE
+ if(panel_open)
+ add_overlay("panel_open")
+ can_see_circuit = TRUE
+ else if(reason_broken & MACHINE_BROKEN_GENERIC)
+ add_overlay("inside_broken")
+ can_see_circuit = TRUE
+
+ if(can_see_circuit)
+ var/obj/item/stock_parts/circuitboard/C = get_component_of_type(/obj/item/stock_parts/circuitboard)
+ if(C)
+ if(!C.is_functional())
+ add_overlay("circuit_broken")
+ else if(istype(construct_state, /decl/machine_construction/wall_frame/no_wires)) //only way to check if unwired
+ add_overlay("circuit_unwired")
+ else
+ add_overlay("circuit")
+
+ if(!panel_open && (reason_broken & MACHINE_BROKEN_GENERIC))
+ add_overlay("panel_broken")
+
+ if(inoperable())
+ set_light(0)
+ var/screen = get_component_of_type(/obj/item/stock_parts/console_screen)
+ if(screen)
+ if(reason_broken & MACHINE_BROKEN_GENERIC)
+ add_overlay("comp_screen_broken")
+ else
+ add_overlay("comp_screen")
+ else
+ set_light(light_range_on, light_power_on, light_color)
+ var/screen_overlay = get_screen_overlay()
+ if(screen_overlay)
+ add_overlay(screen_overlay)
diff --git a/code/modules/modular_computers/computers/subtypes/preset_console.dm b/code/modules/modular_computers/computers/subtypes/preset_console.dm
index 903640a8c3d..5f891efbde6 100644
--- a/code/modules/modular_computers/computers/subtypes/preset_console.dm
+++ b/code/modules/modular_computers/computers/subtypes/preset_console.dm
@@ -26,10 +26,15 @@
. = ..()
var/datum/extension/interactive/os/os = get_extension(src, /datum/extension/interactive/os)
if(os)
+ // We access the harddrive directly because the filesystem is yet to be initialized.
+ var/obj/item/stock_parts/computer/hard_drive/HDD = os.get_component(PART_HDD)
for(var/program_type in default_software)
- os.store_file(new program_type())
+ HDD.store_file(new program_type(), OS_PROGRAMS_DIR, create_directories = TRUE)
if(autorun_program)
- os.set_autorun(initial(autorun_program.filename))
+ var/datum/computer_file/data/autorun = new()
+ autorun.filename = "autorun"
+ autorun.stored_data = initial(autorun_program.filename)
+ HDD.store_file(autorun)
/obj/machinery/computer/modular/preset/engineering
default_software = list(
diff --git a/code/modules/modular_computers/computers/subtypes/preset_pda.dm b/code/modules/modular_computers/computers/subtypes/preset_pda.dm
index 744401edaf0..49cb461c8c0 100644
--- a/code/modules/modular_computers/computers/subtypes/preset_pda.dm
+++ b/code/modules/modular_computers/computers/subtypes/preset_pda.dm
@@ -22,11 +22,23 @@
default_programs |= /datum/computer_file/program/uplink
return ..()
-/obj/item/modular_computer/pda/syndicate
- color = COLOR_GRAY20
+/obj/item/modular_computer/pda/mercenary
+ color = COLOR_DARK_RED
decals = list(
"stripe" = COLOR_RED,
- "stripe2" = COLOR_DARK_RED
+ )
+
+/obj/item/modular_computer/pda/ert
+ color = COLOR_OFF_WHITE
+ decals = list(
+ "stripe" = COLOR_DARK_BLUE_GRAY,
+ "stripe2" = COLOR_GOLD
+ )
+
+/obj/item/modular_computer/pda/ninja
+ color = COLOR_GRAY20
+ decals = list(
+ "stripe" = COLOR_BLACK
)
/obj/item/modular_computer/pda/heads
@@ -64,7 +76,7 @@
/obj/item/modular_computer/pda/heads/captain/install_default_hardware()
default_hardware |= /obj/item/stock_parts/computer/scanner/paper
- . = ..()
+ . = ..()
/obj/item/modular_computer/pda/science
color = COLOR_OFF_WHITE
@@ -104,4 +116,4 @@
/obj/item/modular_computer/pda/cargo/install_default_programs()
default_programs |= /datum/computer_file/program/reports
- . = ..()
\ No newline at end of file
+ . = ..()
diff --git a/code/modules/modular_computers/computers/subtypes/preset_telescreen.dm b/code/modules/modular_computers/computers/subtypes/preset_telescreen.dm
index 6b5b2dcb0f6..40609a4a05d 100644
--- a/code/modules/modular_computers/computers/subtypes/preset_telescreen.dm
+++ b/code/modules/modular_computers/computers/subtypes/preset_telescreen.dm
@@ -1,27 +1,73 @@
-/obj/item/modular_computer/telescreen/preset
- default_hardware = list(
+//////////////////////////////////////////////////////////////////
+// Telescreen Preset
+//////////////////////////////////////////////////////////////////
+
+/obj/machinery/computer/modular/telescreen/preset
+ base_type = /obj/machinery/computer/modular/telescreen
+ uncreated_component_parts = list(
/obj/item/stock_parts/computer/processor_unit,
/obj/item/stock_parts/computer/tesla_link,
/obj/item/stock_parts/computer/hard_drive,
- /obj/item/stock_parts/computer/network_card
+ /obj/item/stock_parts/computer/network_card,
+ /obj/item/stock_parts/computer/card_slot,
+ )
+ var/list/default_software
+ var/datum/computer_file/program/autorun_program
+
+/obj/machinery/computer/modular/telescreen/preset/Initialize(mapload, d=0, populate_parts = TRUE)
+ . = ..()
+ var/datum/extension/interactive/os/os = get_extension(src, /datum/extension/interactive/os)
+ if(os)
+ //#TODO: Maybe that file system stuff should really handle this a bit better so we don't have to mess with internals like that?
+ // We access the harddrive directly because the filesystem is yet to be initialized.
+ var/obj/item/stock_parts/computer/hard_drive/HDD = os.get_component(PART_HDD)
+ if(!HDD)
+ log_warning("Telescreen preset '[type]' doesn't have a hard drive! This is most likely not desired.")
+ return .
+ for(var/program_type in default_software)
+ HDD.store_file(new program_type(), OS_PROGRAMS_DIR, create_directories = TRUE)
+ if(autorun_program)
+ var/datum/computer_file/data/autorun = new()
+ autorun.filename = "autorun"
+ autorun.stored_data = initial(autorun_program.filename)
+ HDD.store_file(autorun)
+
+//////////////////////////////////////////////////////////////////
+// Pressets
+//////////////////////////////////////////////////////////////////
+
+/obj/machinery/computer/modular/telescreen/preset/supply_public
+ default_software = list(
+ /datum/computer_file/program/supply,
)
+ autorun_program = /datum/computer_file/program/supply
-/obj/item/modular_computer/telescreen/preset/generic
- default_programs = list(
+/obj/machinery/computer/modular/telescreen/preset/civilian
+ default_software = list(
+ /datum/computer_file/program/camera_monitor,
+ /datum/computer_file/program/records,
+ /datum/computer_file/program/email_client,
+ /datum/computer_file/program/wordprocessor
+ )
+
+/obj/machinery/computer/modular/telescreen/preset/generic
+ default_software = list(
/datum/computer_file/program/alarm_monitor,
/datum/computer_file/program/camera_monitor
)
-/obj/item/modular_computer/telescreen/preset/medical
- default_programs = list(
+/obj/machinery/computer/modular/telescreen/preset/medical
+ default_software = list(
/datum/computer_file/program/camera_monitor,
/datum/computer_file/program/records,
- /datum/computer_file/program/suit_sensors
+ /datum/computer_file/program/suit_sensors,
+ /datum/computer_file/program/wordprocessor
)
-/obj/item/modular_computer/telescreen/preset/engineering
- default_programs = list(
+
+/obj/machinery/computer/modular/telescreen/preset/engineering
+ default_software = list(
/datum/computer_file/program/alarm_monitor,
/datum/computer_file/program/camera_monitor,
/datum/computer_file/program/shields_monitor,
/datum/computer_file/program/supermatter_monitor
- )
+ )
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/computer_file.dm b/code/modules/modular_computers/file_system/computer_file.dm
index 0f7ea405b4e..34e1d1e2126 100644
--- a/code/modules/modular_computers/file_system/computer_file.dm
+++ b/code/modules/modular_computers/file_system/computer_file.dm
@@ -8,9 +8,10 @@ var/global/file_uid = 0
var/filename = "NewFile" // Placeholder. No spacebars
var/filetype = "XXX" // File full names are [filename].[filetype] so like NewFile.XXX in this case
var/size = 1 // File size in GQ. Integers only!
- var/obj/item/stock_parts/computer/hard_drive/holder // Holder that contains this file.
+ var/weakref/holder // Holder that contains this file. Refers to a obj/item/stock_parts/computer/hard_drive.
var/unsendable = 0 // Whether the file may be sent to someone via file transfer or other means.
var/undeletable = 0 // Whether the file may be deleted. Setting to 1 prevents deletion/renaming/etc.
+ var/unrenamable = 0 // Whether the file may be renamed. Setting to 1 prevents renaming.
var/uid // UID of this file
var/list/metadata // Any metadata the file uses.
var/papertype = /obj/item/paper
@@ -28,29 +29,36 @@ var/global/file_uid = 0
metadata = md.Copy()
/datum/computer_file/Destroy()
+ var/obj/item/stock_parts/computer/hard_drive/hard_drive = holder?.resolve()
+ if(hard_drive)
+ hard_drive.remove_file(src, forced = TRUE)
. = ..()
- if(!holder)
- return
- holder.remove_file(src)
-
-// Returns independent copy of this file.
-/datum/computer_file/proc/clone(var/rename = 0)
- var/datum/computer_file/temp = new type
- temp.unsendable = unsendable
- temp.undeletable = undeletable
- temp.size = size
+/datum/computer_file/PopulateClone(datum/computer_file/clone)
+ clone = ..()
+ clone.unsendable = unsendable
+ clone.undeletable = undeletable
+ clone.size = size
if(metadata)
- temp.metadata = metadata.Copy()
- if(rename)
- temp.filename = filename + copy_string
- else
- temp.filename = filename
- temp.filetype = filetype
- temp.read_access = read_access
- temp.write_access = write_access
- temp.mod_access = mod_access
- return temp
+ clone.metadata = listDeepClone(metadata, TRUE)
+ clone.filetype = filetype
+ clone.read_access = deepCopyList(read_access)
+ clone.write_access = deepCopyList(write_access)
+ clone.mod_access = deepCopyList(mod_access)
+ return clone
+
+/**
+ * Returns independent copy of this file.
+ * rename: Whether the clone shold be auto-renamed.
+ */
+/datum/computer_file/Clone(var/rename = FALSE)
+ var/datum/computer_file/clone = ..(null) //Don't propagate our rename param
+ if(clone)
+ if(rename)
+ clone.filename = filename + copy_string
+ else
+ clone.filename = filename
+ return clone
/datum/computer_file/proc/get_file_perms(var/list/accesses, var/mob/user)
. = 0
@@ -62,7 +70,7 @@ var/global/file_uid = 0
. |= OS_WRITE_ACCESS
if(!LAZYLEN(mod_access) || has_access(mod_access, accesses))
. |= OS_MOD_ACCESS
-
+
/datum/computer_file/proc/get_perms_readable()
var/list/msg = list()
msg += "Permissions for file [filename]:"
@@ -92,7 +100,7 @@ var/global/file_uid = 0
return FALSE
if(perm == (OS_READ_ACCESS || OS_WRITE_ACCESS))
- var/list/modded_list = (perm == OS_READ_ACCESS ? read_access : write_access)
+ var/list/modded_list = (perm == OS_READ_ACCESS ? read_access : write_access)
if(change == "+")
if(!LAZYLEN(modded_list))
modded_list = list()
@@ -109,7 +117,7 @@ var/global/file_uid = 0
return TRUE
else
return FALSE // Something unexpected was passed into the change argument.
-
+
else if(perm == OS_MOD_ACCESS)
var/list/test_list // You can't modify access such that you can't access the file any longer, so we test changes first.
if(change == "+")
@@ -138,4 +146,18 @@ var/global/file_uid = 0
mod_access = null
return TRUE
else
- return FALSE
\ No newline at end of file
+ return FALSE
+
+/datum/computer_file/proc/get_directory()
+ var/obj/item/stock_parts/computer/hard_drive/hard_drive = holder?.resolve()
+ if(hard_drive)
+ return hard_drive.stored_files[src]
+
+/datum/computer_file/proc/get_file_path()
+ var/datum/computer_file/parent = get_directory()
+ var/list/dir_names = list()
+ while(istype(parent))
+ dir_names.Insert(1, parent.filename)
+ parent = parent.get_directory()
+
+ return jointext(dir_names, "/")
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/data.dm b/code/modules/modular_computers/file_system/data.dm
index ea4d3bf75de..369f1b15b4b 100644
--- a/code/modules/modular_computers/file_system/data.dm
+++ b/code/modules/modular_computers/file_system/data.dm
@@ -7,10 +7,10 @@
var/do_not_edit = 0 // Whether the user will be reminded that the file probably shouldn't be edited.
var/read_only = 0 // Protects files that should never be edited by the user due to special properties.
-/datum/computer_file/data/clone()
- var/datum/computer_file/data/temp = ..()
- temp.stored_data = stored_data
- return temp
+/datum/computer_file/data/PopulateClone(datum/computer_file/data/clone)
+ clone = ..()
+ clone.stored_data = stored_data
+ return clone
// Calculates file size from amount of characters in saved string
/datum/computer_file/data/proc/calculate_size()
diff --git a/code/modules/modular_computers/file_system/directory.dm b/code/modules/modular_computers/file_system/directory.dm
new file mode 100644
index 00000000000..97e7221078d
--- /dev/null
+++ b/code/modules/modular_computers/file_system/directory.dm
@@ -0,0 +1,90 @@
+/datum/computer_file/directory
+ filetype = "DIR"
+ size = 0
+
+ var/list/held_files = list() // Weakrefs of files held by this directory.
+ var/inherit_perms = TRUE
+
+ var/list/temp_file_refs = list() // List used to temporarily hold references to stored files post cloning, so they
+ // don't get GC'd. Cleared on storing or deleting the file.
+
+/datum/computer_file/directory/Destroy()
+ for(var/weakref/file_ref in held_files)
+ var/datum/computer_file/held_file = file_ref.resolve()
+ var/obj/item/stock_parts/computer/hard_drive/hard_drive = holder?.resolve()
+ if(held_file && hard_drive)
+ hard_drive.remove_file(held_file, forced = TRUE)
+
+ QDEL_NULL(temp_file_refs)
+ . = ..()
+
+/datum/computer_file/directory/proc/get_held_files()
+ . = list()
+ for(var/weakref/file_ref in held_files)
+ var/datum/computer_file/held_file = file_ref.resolve()
+ if(!held_file)
+ held_files -= file_ref
+ continue
+ . += held_file
+
+// Returns the total file size of held files. We don't override the actual size of the directory so we don't double count file size for capacity.
+/datum/computer_file/directory/proc/get_held_size(list/counted_dirs = list())
+ . = 0
+ counted_dirs |= src // Keep track of files which have been counted in case there's a directory loop.
+ for(var/weakref/file_ref in held_files)
+ var/datum/computer_file/held_file = file_ref.resolve()
+ if(!held_file)
+ held_files -= file_ref
+ continue
+ if(istype(held_file, /datum/computer_file/directory))
+ var/datum/computer_file/directory/dir = held_file
+ if(dir in counted_dirs)
+ continue
+ . += dir.get_held_size(counted_dirs)
+ else
+ . += held_file.size
+
+// Get the permissions for mass file actions, generally:
+// * OS_READ_ACCESS on all contained files is required for cloning directories.
+// * OS_WRITE_ACCESS on all contained files is required for transferring/deleting directories.
+/datum/computer_file/directory/proc/get_held_perms(list/accesses, mob/user, list/counted_dirs = list())
+ . = get_file_perms(accesses, user)
+
+ if(!accesses || (isghost(user) && check_rights(R_ADMIN, 0, user))) // As with normal file perms, either internal use or admin-ghost usage.
+ return
+
+ counted_dirs |= src // As above.
+ for(var/weakref/file_ref in held_files)
+ var/datum/computer_file/held_file = file_ref.resolve()
+ if(!held_file)
+ held_files -= file_ref
+
+ if(istype(held_file, /datum/computer_file/directory))
+ var/datum/computer_file/directory/dir = held_file
+ if(dir in counted_dirs)
+ continue
+ . += dir.get_held_perms(accesses, user, counted_dirs)
+ else
+ . &= held_file.get_file_perms(accesses, user)
+
+ if(. == 0) // We've already lost all permissions, don't bother checking anything else.
+ return
+
+/datum/computer_file/directory/get_file_path()
+ var/parent_paths = ..()
+ if(parent_paths)
+ return parent_paths + "/" + filename
+ return filename
+
+/datum/computer_file/directory/PopulateClone(datum/computer_file/directory/clone)
+ clone = ..()
+ clone.inherit_perms = inherit_perms
+ // Add copies of all of our stored files
+ for(var/datum/computer_file/stored in get_held_files())
+ // Do not rename cloned files.
+ var/datum/computer_file/stored_clone = stored.Clone(FALSE)
+ if(stored_clone)
+ clone.held_files += weakref(stored_clone)
+ clone.temp_file_refs += stored_clone
+
+ return clone
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm
index abfbab7e49d..d0e5ce447d9 100644
--- a/code/modules/modular_computers/file_system/program.dm
+++ b/code/modules/modular_computers/file_system/program.dm
@@ -7,7 +7,7 @@
var/datum/nano_module/NM = null // If the program uses NanoModule, put it here and it will be automagically opened. Otherwise implement ui_interact.
var/nanomodule_path = null // Path to nanomodule, make sure to set this if implementing new program.
var/program_state = PROGRAM_STATE_KILLED // PROGRAM_STATE_KILLED or PROGRAM_STATE_BACKGROUND or PROGRAM_STATE_ACTIVE - specifies whether this program is running.
- var/datum/extension/interactive/os/computer // OS that runs this program.
+ var/datum/extension/interactive/os/computer // OS that runs this program.
var/filedesc = "Unknown Program" // User-friendly name of this program.
var/extended_desc = "N/A" // Short description of this program's function.
var/category = PROG_MISC
@@ -18,8 +18,7 @@
var/requires_network_feature = 0 // Optional, if above is set to 1 checks for specific function of network (currently NETWORK_SOFTWAREDOWNLOAD, NETWORK_PEERTOPEER, NETWORK_SYSTEMCONTROL and NETWORK_COMMUNICATION)
var/usage_flags = PROGRAM_ALL & ~PROGRAM_PDA // Bitflags (PROGRAM_CONSOLE, PROGRAM_LAPTOP, PROGRAM_TABLET, PROGRAM_PDA combination) or PROGRAM_ALL
var/network_destination = null // Optional string that describes what network server/system this program connects to. Used in default logging.
- var/available_on_network = 1 // Whether the program can be downloaded from network. Set to 0 to disable.
- var/available_on_syndinet = 0 // Whether the program can be downloaded from SyndiNet (accessible via emagging the computer). Set to 1 to enable.
+ var/available_on_network = TRUE // Whether the program can be downloaded from network. Set to FALSE to disable.
var/computer_emagged = 0 // Set to 1 if computer that's running us was emagged. Computer updates this every Process() tick
var/ui_header = null // Example: "something.gif" - a header image that will be rendered in computer's UI when this program is running at background. Images are taken from /nano/images/status_icons. Be careful not to use too large images!
var/operator_skill = SKILL_MIN // Holder for skill value of current/recent operator for programs that tick.
@@ -35,23 +34,23 @@
/datum/computer_file/program/nano_host()
return computer && computer.nano_host()
-/datum/computer_file/program/clone()
- var/datum/computer_file/program/temp = ..()
- temp.read_access = read_access
- temp.nanomodule_path = nanomodule_path
- temp.filedesc = filedesc
- temp.program_icon_state = program_icon_state
- temp.requires_network = requires_network
- temp.requires_network_feature = requires_network_feature
- temp.usage_flags = usage_flags
- return temp
+/datum/computer_file/program/PopulateClone(datum/computer_file/program/clone)
+ clone = ..()
+ clone.read_access = deepCopyList(read_access)
+ clone.nanomodule_path = nanomodule_path
+ clone.filedesc = filedesc
+ clone.program_icon_state = program_icon_state
+ clone.requires_network = requires_network
+ clone.requires_network_feature = requires_network_feature
+ clone.usage_flags = usage_flags
+ return clone
// Used by programs that manipulate files.
-/datum/computer_file/program/proc/get_file(var/filename)
- return computer.get_file(filename)
+/datum/computer_file/program/proc/get_file(var/filename, var/directory, var/list/accesses, var/mob/user)
+ return computer.get_file(filename, directory, accesses, user)
-/datum/computer_file/program/proc/create_file(var/newname, var/data = "", var/file_type = /datum/computer_file/data, var/list/metadata = null)
- return computer.create_file(newname, data, file_type, metadata)
+/datum/computer_file/program/proc/create_file(var/newname, var/directory, var/data = "", var/file_type = /datum/computer_file/data, var/list/metadata = null, var/list/accesses, var/mob/user)
+ return computer.create_file(newname, directory, data, file_type, metadata, accesses, user)
// Relays icon update to the computer.
/datum/computer_file/program/proc/update_computer_icon()
@@ -100,9 +99,9 @@
// This attempts to retrieve header data for NanoUIs. If implementing completely new device of different type than existing ones
// always include the device here in this proc. This proc basically relays the request to whatever is running the program.
-/datum/computer_file/program/proc/get_header_data()
+/datum/computer_file/program/proc/get_header_data(file_browser = FALSE)
if(computer)
- return computer.get_header_data()
+ return computer.get_header_data(file_browser)
return list()
// This is performed on program startup. May be overriden to add extra logic. Remember to include ..() call.
@@ -113,7 +112,7 @@
if(nanomodule_path)
NM = new nanomodule_path(src, new /datum/topic_manager/program(src), src)
if(user)
- NM.using_access = computer.get_access() // Programs nab access from users in their get_access() proc so don't bother adding it
+ NM.using_access = computer.get_access() // Nano modules nab access from users in their get_access() proc so don't bother adding it
// to the using list as well.
if(requires_network && network_destination)
generate_network_log("Connection opened to [network_destination].")
@@ -134,15 +133,20 @@
NM = null
return 1
+// Called on active or minimized programs when a mounted file storage is removed from the OS.
+/datum/computer_file/program/proc/on_file_storage_removal(var/datum/file_storage/removed)
+
// This is called every tick when the program is enabled. Ensure you do parent call if you override it. If parent returns 1 continue with UI initialisation.
// It returns 0 if it can't run or if NanoModule was used instead. I suggest using NanoModules where applicable.
/datum/computer_file/program/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
SHOULD_CALL_PARENT(TRUE)
..()
- if(program_state != PROGRAM_STATE_ACTIVE) // Our program was closed. Close the ui if it exists.
+ if(program_state < PROGRAM_STATE_ACTIVE) // Our program was closed. Close the ui if it exists.
if(ui)
ui.close()
return computer.ui_interact(user)
+ if(program_state == PROGRAM_STATE_BROWSER)
+ return 0
if(istype(NM))
NM.ui_interact(user, ui_key, null, force_open)
return 0
@@ -156,7 +160,8 @@
// CONVENTIONS, READ THIS WHEN CREATING NEW PROGRAM AND OVERRIDING THIS PROC:
// Topic calls are automagically forwarded from NanoModule this program contains.
// Calls beginning with "PRG_" are reserved for programs handling.
-// Calls beginning with "PC_" are reserved for computer handling (by whatever runs the program)
+// Calls beginning with "PC_" are reserved for computer handling (by whatever runs the program).
+// Calls beginning with "BRS_" are reserved for program file browser handling (not to be confused with the file manager program).
// ALWAYS INCLUDE PARENT CALL ..() OR DIE IN FIRE.
/datum/computer_file/program/Topic(href, href_list)
if(..())
diff --git a/code/modules/modular_computers/file_system/programs/antagonist/access_decrypter.dm b/code/modules/modular_computers/file_system/programs/antagonist/access_decrypter.dm
index 998702f116c..070f698817a 100644
--- a/code/modules/modular_computers/file_system/programs/antagonist/access_decrypter.dm
+++ b/code/modules/modular_computers/file_system/programs/antagonist/access_decrypter.dm
@@ -7,7 +7,6 @@
extended_desc = "This highly advanced script can very slowly decrypt operational codes used in almost any network. These codes can be downloaded to an ID card to expand the available access. The system administrator will probably notice this."
size = 12
available_on_network = 0
- available_on_syndinet = 1
nanomodule_path = /datum/nano_module/program/access_decrypter/
var/message = ""
var/running = FALSE
diff --git a/code/modules/modular_computers/file_system/programs/antagonist/hacked_camera.dm b/code/modules/modular_computers/file_system/programs/antagonist/hacked_camera.dm
index ba37d762f03..a76c4bc4805 100644
--- a/code/modules/modular_computers/file_system/programs/antagonist/hacked_camera.dm
+++ b/code/modules/modular_computers/file_system/programs/antagonist/hacked_camera.dm
@@ -9,7 +9,6 @@
size = 20
requires_network_feature = 0
available_on_network = 0
- available_on_syndinet = 1
/datum/computer_file/program/camera_monitor/hacked/process_tick()
..()
diff --git a/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm b/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm
index bf784a79ae2..f842cedfe41 100644
--- a/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm
+++ b/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm
@@ -7,7 +7,6 @@
extended_desc = "This virus can destroy hard drive of system it is executed on. It may be obfuscated to look like another non-malicious program. Once armed, it will destroy the system upon next execution."
size = 13
available_on_network = 0
- available_on_syndinet = 1
nanomodule_path = /datum/nano_module/program/revelation/
var/armed = 0
@@ -45,10 +44,10 @@
break
return 1
-/datum/computer_file/program/revelation/clone()
- var/datum/computer_file/program/revelation/temp = ..()
- temp.armed = armed
- return temp
+/datum/computer_file/program/revelation/PopulateClone(datum/computer_file/program/revelation/clone)
+ clone = ..()
+ clone.armed = armed
+ return clone
/datum/nano_module/program/revelation
name = "Revelation Virus"
diff --git a/code/modules/modular_computers/file_system/programs/command/comm.dm b/code/modules/modular_computers/file_system/programs/command/comm.dm
index a595ff5f52c..12703f015fe 100644
--- a/code/modules/modular_computers/file_system/programs/command/comm.dm
+++ b/code/modules/modular_computers/file_system/programs/command/comm.dm
@@ -20,11 +20,10 @@
category = PROG_COMMAND
var/datum/comm_message_listener/message_core = new
-/datum/computer_file/program/comm/clone()
- var/datum/computer_file/program/comm/temp = ..()
- temp.message_core.messages = null
- temp.message_core.messages = message_core.messages.Copy()
- return temp
+/datum/computer_file/program/comm/PopulateClone(datum/computer_file/program/comm/clone)
+ clone = ..()
+ clone.message_core = message_core.Clone()
+ return clone
/datum/nano_module/program/comm
name = "Command and Communications Program"
@@ -319,6 +318,11 @@ var/global/last_message_id = 0
/datum/comm_message_listener/proc/Remove(var/list/message)
messages -= list(message)
+/datum/comm_message_listener/PopulateClone(datum/comm_message_listener/clone)
+ clone = ..()
+ clone.messages = listDeepClone(messages)
+ return clone
+
/proc/post_status(var/command, var/data1, var/data2)
var/datum/radio_frequency/frequency = radio_controller.return_frequency(1435)
diff --git a/code/modules/modular_computers/file_system/programs/engineering/alarm_monitor.dm b/code/modules/modular_computers/file_system/programs/engineering/alarm_monitor.dm
index e80aa22e752..48e93654906 100644
--- a/code/modules/modular_computers/file_system/programs/engineering/alarm_monitor.dm
+++ b/code/modules/modular_computers/file_system/programs/engineering/alarm_monitor.dm
@@ -34,7 +34,6 @@
/datum/nano_module/alarm_monitor
name = "Alarm monitor"
- var/list_cameras = 0 // Whether or not to list camera references. A future goal would be to merge this with the enginering/security camera console. Currently really only for AI-use.
var/list/datum/alarm_handler/alarm_handlers // The particular list of alarm handlers this alarm monitor should present to the user.
available_to_ai = FALSE
diff --git a/code/modules/modular_computers/file_system/programs/engineering/network_monitoring.dm b/code/modules/modular_computers/file_system/programs/engineering/network_monitoring.dm
index 94dfe5aea21..0a0dd383145 100644
--- a/code/modules/modular_computers/file_system/programs/engineering/network_monitoring.dm
+++ b/code/modules/modular_computers/file_system/programs/engineering/network_monitoring.dm
@@ -61,7 +61,7 @@
var/list/logs[0]
for(var/datum/extension/network_device/mainframe/M in network.get_mainframes_by_role(MF_ROLE_LOG_SERVER, user))
var/list/logdata[0]
- var/datum/computer_file/data/logfile/F = M.get_file("network_log")
+ var/datum/computer_file/data/logfile/F = M.get_file("network_log", OS_LOGS_DIR, TRUE)
if(F)
logdata["server"] = M.network_tag
logdata["log"] = F.generate_file_data()
@@ -96,7 +96,11 @@
var/datum/extension/network_device/mainframe/M = locate(href_list["purgelogs"])
if(!istype(M))
return TOPIC_HANDLED
- M.delete_file("network_log")
+ var/datum/computer_file/log_file = M.get_file("network_log", OS_LOGS_DIR)
+ if(!log_file)
+ return TOPIC_HANDLED
+ M.delete_file(log_file)
+ return TOPIC_REFRESH
if(href_list["updatemaxlogs"])
var/datum/extension/network_device/mainframe/M = locate(href_list["updatemaxlogs"])
@@ -104,7 +108,7 @@
return TOPIC_HANDLED
var/logcount = input(user,"Enter amount of logs to keep on the disk ([MIN_NETWORK_LOGS]-[MAX_NETWORK_LOGS]):", M.max_log_count) as null|num
if(logcount && CanUseTopic(user, state))
- logcount = Clamp(logcount, MIN_NETWORK_LOGS, MAX_NETWORK_LOGS)
+ logcount = clamp(logcount, MIN_NETWORK_LOGS, MAX_NETWORK_LOGS)
M.max_log_count = logcount
return TOPIC_REFRESH
return TOPIC_HANDLED
diff --git a/code/modules/modular_computers/file_system/programs/file_browser.dm b/code/modules/modular_computers/file_system/programs/file_browser.dm
new file mode 100644
index 00000000000..5c95c51818a
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/file_browser.dm
@@ -0,0 +1,286 @@
+// Generic interface for selecting/saving files with programs. The file manager program does not use this.
+/datum/computer_file/program
+ var/datum/nano_module/program/file_browser/browser_module
+
+/datum/computer_file/program/on_shutdown(forced)
+ QDEL_NULL(browser_module)
+ . = ..()
+
+/datum/computer_file/program/proc/view_file_browser(mob/user, selecting_key, selecting_filetype, req_perm, browser_desc, datum/computer_file/saving_file)
+ if(!browser_module)
+ browser_module = new (src, new /datum/topic_manager/program(src), src)
+ browser_module.using_access = computer.get_access()
+
+ browser_module.reset_browser(selecting_key, selecting_filetype, req_perm, browser_desc, saving_file)
+ browser_module.ui_interact(user)
+ return TRUE
+
+/datum/computer_file/program/proc/on_file_select(datum/file_storage/disk, datum/computer_file/directory/dir, datum/computer_file/selected, selecting_key)
+ browser_module.reset_browser()
+ SSnano.close_uis(browser_module)
+ SSnano.update_uis(src)
+
+/datum/nano_module/program/file_browser
+ var/disk_name = "local"
+ var/weakref/dir_ref
+ var/browser_error
+
+ var/selecting_key // String identifying the action being performed, passed to proc/on_file_select on the program itself.
+ var/selecting_filetype // /datum/computer_file subtype being selected/created.
+
+ var/weakref/selected_file // Reference to the file being selected.
+
+ var/datum/computer_file/saving_file // The file, if any, that's being saved. For the sake of GC, this should be a completely new file
+ // not stored on any drive, and a reference to it should not be kept until after the browser finishes.
+
+ var/req_perm
+ var/browser_desc // String describing the action being performed.
+
+/datum/nano_module/program/file_browser/Destroy()
+ QDEL_NULL(saving_file)
+ selected_file = null
+ dir_ref = null
+ . = ..()
+
+/datum/nano_module/program/file_browser/Topic(href, href_list)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/user = usr
+ var/list/accesses = get_access(user)
+ var/datum/extension/interactive/os/computer = program.computer
+
+ if(href_list["BRS_back"])
+ if(browser_error)
+ browser_error = null
+ return TOPIC_REFRESH
+ SSnano.close_uis(src)
+ reset_browser()
+ return TOPIC_REFRESH
+
+ if(href_list["BRS_select_disk"])
+ var/selected_disk = href_list["BRS_select_disk"]
+ if(selected_disk in computer.mounted_storage)
+ disk_name = selected_disk
+ return TOPIC_REFRESH
+ return TOPIC_HANDLED
+
+ if(!disk_name)
+ return TOPIC_REFRESH
+
+ var/datum/file_storage/current_disk = computer.mounted_storage[disk_name]
+ if(!current_disk)
+ disk_name = null
+ return TOPIC_REFRESH
+
+ var/errors = current_disk.check_errors()
+ if(errors)
+ browser_error = errors
+ return TOPIC_REFRESH
+
+ var/datum/computer_file/directory/current_directory = dir_ref?.resolve()
+ if(current_directory && !(current_directory in current_disk.get_all_files()))
+ dir_ref = null
+ return TOPIC_REFRESH
+
+ if(href_list["BRS_up_directory"])
+ if(!current_directory)
+ disk_name = null
+ var/datum/computer_file/directory/parent_dir = current_directory?.get_directory()
+ dir_ref = weakref(parent_dir)
+ return TOPIC_REFRESH
+
+ if(href_list["BRS_change_directory"])
+ var/datum/computer_file/directory/dir = current_disk.get_file(href_list["BRS_change_directory"], current_directory)
+ if(!istype(dir))
+ return TOPIC_HANDLED
+ if(!(dir.get_file_perms(accesses, user) & OS_READ_ACCESS))
+ to_chat(user, SPAN_WARNING("You do not have permission to open this directory."))
+ return TOPIC_HANDLED
+
+ dir_ref = weakref(dir)
+ return TOPIC_REFRESH
+
+ if(href_list["BRS_select_file"])
+ var/datum/computer_file/F = current_disk.get_file(href_list["BRS_select_file"], current_directory)
+ if(!F)
+ return TOPIC_HANDLED
+
+ if(saving_file)
+ saving_file.filename = F.filename
+ return TOPIC_REFRESH
+
+ if(!istype(F, selecting_filetype))
+ to_chat(user, SPAN_WARNING("This file is not the proper type."))
+ return TOPIC_HANDLED
+
+ if(!(F.get_file_perms(accesses, user) & req_perm))
+ to_chat(user, SPAN_WARNING("You do not have permission to open this file."))
+ return TOPIC_HANDLED
+
+ selected_file = weakref(F)
+ return TOPIC_REFRESH
+
+ if(href_list["BRS_rename_file"])
+ if(!saving_file)
+ return TOPIC_HANDLED
+ var/newname = sanitize_for_file(input(usr, "Enter file name:", "Save file", saving_file.filename))
+ if(!length(newname))
+ to_chat(user, SPAN_WARNING("Invalid file name!"))
+ return TOPIC_HANDLED
+
+ saving_file.filename = newname
+ return TOPIC_REFRESH
+
+ if(href_list["BRS_save_file"])
+ if(!saving_file)
+ return TOPIC_HANDLED
+
+ var/success = current_disk.store_file(saving_file, current_directory, accesses = accesses, user = user)
+ if(success != OS_FILE_SUCCESS)
+ if(success == OS_FILE_NO_WRITE)
+ to_chat(user, SPAN_WARNING("You do not have permission to write/overwrite the file in this directory."))
+ return TOPIC_HANDLED
+ if(success == OS_HARDDRIVE_SPACE)
+ to_chat(user, SPAN_WARNING("Insufficient harddrive space."))
+
+ // Since we know the directory exists, any other error indicates something is wrong with the drive or network.
+ to_chat(user, SPAN_WARNING("I/O ERROR: Drive is non-functional or could not be accessed on the network."))
+ return TOPIC_REFRESH
+
+ var/datum/computer_file/saved_file = saving_file
+ saving_file = null // We null this out so it doesn't get QDEL when the nanomodule is reset, but any other action that resets the browser does.
+ to_chat(user, SPAN_NOTICE("Created file [saved_file.filename].[saved_file.filetype]."))
+ program.on_file_select(current_disk, current_directory, saved_file, selecting_key)
+ return TOPIC_HANDLED
+
+ if(href_list["BRS_finalize_select"])
+ if(saving_file || !selected_file)
+ return TOPIC_HANDLED
+
+ var/datum/computer_file/F = selected_file.resolve()
+ if(!istype(F))
+ selected_file = null
+ return TOPIC_REFRESH
+
+ if(!istype(F, selecting_filetype))
+ to_chat(user, SPAN_WARNING("This file is not the proper type."))
+ selected_file = null
+ return TOPIC_HANDLED
+
+ if(!(F.get_file_perms(accesses, user) & req_perm))
+ to_chat(user, SPAN_WARNING("You do not have permission to open this file."))
+ selected_file = null
+ return TOPIC_HANDLED
+
+ program.on_file_select(current_disk, current_directory, F, selecting_key)
+ SSnano.close_uis(src)
+ reset_browser()
+ return TOPIC_REFRESH
+
+ if(href_list["BRS_create_dir"])
+ if(!saving_file)
+ return TOPIC_HANDLED
+ var/dirname = sanitize_for_file(input(usr, "Enter directory name or leave blank to cancel:", "Directory creation"))
+ if(!length(dirname))
+ return TOPIC_HANDLED
+
+ var/datum/computer_file/directory/created_dir = current_disk.create_directory(dirname, current_directory, accesses, user)
+ if(!istype(created_dir))
+ if(created_dir == OS_HARDDRIVE_ERROR || created_dir == OS_NETWORK_ERROR)
+ to_chat(user, SPAN_WARNING("I/O ERROR: Drive is non-functional or could not be accessed on the network."))
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("You lack permission to create a directory in this location."))
+ return TOPIC_HANDLED
+
+ to_chat(user, SPAN_NOTICE("Created directory [created_dir.filename]"))
+ return TOPIC_HANDLED
+
+/datum/nano_module/program/file_browser/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state)
+ var/list/data = program.get_header_data(TRUE)
+
+ var/datum/extension/interactive/os/computer = program.computer
+
+ var/datum/file_storage/current_disk = disk_name ? computer.mounted_storage[disk_name] : null
+ var/datum/computer_file/current_directory = dir_ref?.resolve()
+
+ var/list/accesses = get_access(user)
+ if(browser_error)
+ data["error"] = browser_error
+ else
+ data["filetypes"] = get_readable_filetypes()
+ if(saving_file)
+ data["saving_file"] = TRUE
+ data["curr_file_name"] = saving_file.filename
+ data["curr_file_type"] = saving_file.filetype
+ else
+ data["saving_file"] = FALSE
+ var/datum/computer_file/selected = selected_file?.resolve()
+ if(istype(selected))
+ data["curr_file_name"] = selected.filename
+ data["curr_file_type"] = selected.filetype
+ if(current_disk)
+ // Safety check.
+ if(current_directory && !(current_directory in current_disk.get_all_files()))
+ dir_ref = null
+
+ data["current_disk"] = current_disk.get_dir_path(current_directory, TRUE)
+
+ var/list/files[0]
+ for(var/datum/computer_file/F in current_disk.get_dir_files(current_directory))
+ files.Add(list(list(
+ "name" = F.filename,
+ "type" = F.filetype,
+ "dir" = istype(F, /datum/computer_file/directory),
+ "size" = F.size,
+ "selectable" = istype(F, selecting_filetype)
+ )))
+ data["files"] = files
+ else // No disk selected.
+ dir_ref = null
+ disk_name = null
+ var/list/avail_disks[0]
+
+ for(var/root_name in computer.mounted_storage)
+ var/datum/file_storage/avail_disk = computer.mounted_storage[root_name]
+ if(!avail_disk.hidden && avail_disk.check_access(accesses))
+ avail_disks.Add(list(list(
+ "name" = root_name,
+ "desc" = avail_disk.desc
+ )))
+
+ data["avail_disks"] = avail_disks
+
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "file_browser.tmpl", browser_desc, 500, 600, state = state)
+ ui.auto_update_layout = 1
+ ui.set_initial_data(data)
+ ui.set_auto_update(1)
+ ui.open()
+
+ return 1
+
+/datum/nano_module/program/file_browser/proc/reset_browser(new_key, new_filetype, new_req_perm, new_desc, datum/computer_file/new_saving_file)
+ disk_name = "local"
+ dir_ref = null
+ browser_error = null
+ selected_file = null
+
+ selecting_key = new_key
+ selecting_filetype = new_filetype
+ req_perm = new_req_perm
+ browser_desc = new_desc
+
+ if(saving_file)
+ QDEL_NULL(saving_file)
+ saving_file = new_saving_file
+
+/datum/nano_module/program/file_browser/proc/get_readable_filetypes()
+ var/list/filetype_strings = list()
+ for(var/T in typesof(selecting_filetype))
+ var/datum/computer_file/option = T
+ filetype_strings += ".[initial(option.filetype)]"
+ return english_list(filetype_strings)
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/programs/generic/deck_management.dm b/code/modules/modular_computers/file_system/programs/generic/deck_management.dm
index baa886f2688..e968e9a2978 100644
--- a/code/modules/modular_computers/file_system/programs/generic/deck_management.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/deck_management.dm
@@ -265,7 +265,7 @@
if(href_list["flight_plan"])
prog_state = DECK_REPORT_EDIT
if(selected_mission.flight_plan)
- selected_report = selected_mission.flight_plan.clone()//We always make a new one to buffer changes until submitted.
+ selected_report = selected_mission.flight_plan.Clone()//We always make a new one to buffer changes until submitted.
else
selected_report = create_report(/datum/computer_file/report/flight_plan, selected_shuttle)
else
@@ -278,9 +278,9 @@
prog_state = DECK_REPORT_EDIT
var/datum/computer_file/report/old_report = locate(prototype.type) in selected_mission.other_reports
if(old_report)
- selected_report = old_report.clone()
+ selected_report = old_report.Clone()
else
- var/datum/computer_file/report/recipient/shuttle/new_report = prototype.clone()
+ var/datum/computer_file/report/recipient/shuttle/new_report = prototype.Clone()
new_report.shuttle.set_value(selected_shuttle.name)
new_report.mission.set_value(selected_mission.name)
selected_report = new_report
diff --git a/code/modules/modular_computers/file_system/programs/generic/email_client.dm b/code/modules/modular_computers/file_system/programs/generic/email_client.dm
index 7f73cccf8f1..dd73361bea8 100644
--- a/code/modules/modular_computers/file_system/programs/generic/email_client.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/email_client.dm
@@ -236,15 +236,17 @@
download_progress = 0
return 1
- if(drive.store_file(downloading))
+ var/success = drive.store_file()
+ if(success == OS_FILE_SUCCESS)
error = "File successfully downloaded to local device."
+ else if(success == OS_HARDDRIVE_SPACE)
+ error = "Error saving file: The hard drive is full"
else
- error = "Error saving file: I/O Error: The hard drive may be full or nonfunctional."
+ error = "Error saving file: I/O Error: The hard drive may be nonfunctional."
downloading = null
download_progress = 0
return 1
-
/datum/nano_module/program/email_client/Topic(href, href_list)
if(..())
return 1
@@ -432,7 +434,7 @@
if(CF.unsendable)
continue
if(CF.filename == picked_file)
- msg_attachment = CF.clone()
+ msg_attachment = CF.Clone()
break
if(!istype(msg_attachment))
msg_attachment = null
@@ -453,7 +455,7 @@
if(!drive)
return 1
- downloading = current_message.attachment.clone()
+ downloading = current_message.attachment.Clone()
download_progress = 0
return 1
diff --git a/code/modules/modular_computers/file_system/programs/generic/file_browser.dm b/code/modules/modular_computers/file_system/programs/generic/file_browser.dm
deleted file mode 100644
index 4173500b956..00000000000
--- a/code/modules/modular_computers/file_system/programs/generic/file_browser.dm
+++ /dev/null
@@ -1,275 +0,0 @@
-/datum/computer_file/program/filemanager
- filename = "filemanager"
- filedesc = "OS File Manager"
- extended_desc = "This program allows management of files."
- program_icon_state = "generic"
- program_key_state = "generic_key"
- program_menu_icon = "folder-collapsed"
- size = 8
- available_on_network = 0
- undeletable = 1
- usage_flags = PROGRAM_ALL
- category = PROG_UTIL
- var/open_file
- var/error
- var/list/file_sources = list(
- /datum/file_storage/disk,
- /datum/file_storage/disk/removable,
- /datum/file_storage/network
- )
- var/datum/file_storage/current_filesource = /datum/file_storage/disk
- var/datum/file_transfer/current_transfer //ongoing file transfer between filesources
-
-/datum/computer_file/program/filemanager/on_startup(var/mob/living/user, var/datum/extension/interactive/os/new_host)
- ..()
- for(var/T in file_sources)
- file_sources[T] = new T(new_host)
- current_filesource = file_sources[initial(current_filesource)]
-
-/datum/computer_file/program/filemanager/on_shutdown()
- for(var/T in file_sources)
- var/datum/file_storage/FS = file_sources[T]
- qdel(FS)
- file_sources[T] = null
- current_filesource = initial(current_filesource)
- ui_header = null
- if(current_transfer)
- qdel(current_transfer)
- return ..()
-
-/datum/computer_file/program/filemanager/Topic(href, href_list, state)
- . = ..()
- if(.)
- return
-
- var/mob/user = usr
- var/list/accesses = computer.get_access(user)
-
- if(href_list["PRG_change_filesource"])
- . = TOPIC_HANDLED
- var/list/choices = list()
- for(var/T in file_sources)
- var/datum/file_storage/FS = file_sources[T]
- if(FS == current_filesource)
- continue
- choices[FS.name] = FS
- var/file_source = input(usr, "Choose a storage medium to use:", "Select Storage Medium") as null|anything in choices
- if(file_source)
- if(istype(current_filesource, /datum/file_storage/network))
- var/datum/computer_network/network = computer.get_network()
- if(!network)
- return TOPIC_REFRESH
- if(!computer.get_network_status(NET_FEATURE_FILESYSTEM))
- to_chat(usr, SPAN_WARNING("The network rejected access to the filesystems with the current connection."))
- return TOPIC_HANDLED
-
- current_filesource = choices[file_source]
- // Helper for some user-friendliness. Try to select the first available mainframe.
- var/list/file_servers = network.get_file_server_tags(MF_ROLE_FILESERVER, accesses)
- var/datum/file_storage/network/N = current_filesource
- if(!file_servers.len)
- N.server = null // Don't allow players to see files on mainframes they cannot access.
- return TOPIC_REFRESH
- N.server = file_servers[1]
- else
- current_filesource = choices[file_source]
- return TOPIC_REFRESH
-
- if(href_list["PRG_changefileserver"])
- . = TOPIC_HANDLED
- var/datum/computer_network/network = computer.get_network()
- if(!network)
- return
- var/list/file_servers = network.get_file_server_tags(MF_ROLE_FILESERVER, accesses)
- var/file_server = input(usr, "Choose a fileserver to view files on:", "Select File Server") as null|anything in file_servers
- if(file_server)
- var/datum/file_storage/network/N = file_sources[/datum/file_storage/network]
- N.server = file_server
- return TOPIC_REFRESH
-
- var/errors = current_filesource.check_errors()
- if(errors)
- error = errors
- return TOPIC_HANDLED
-
- if(href_list["PRG_openfile"])
- . = TOPIC_HANDLED
- var/datum/computer_file/data/F = current_filesource.get_file(href_list["PRG_openfile"])
- if(F && (F.get_file_perms(accesses, user)) & OS_READ_ACCESS)
- open_file = href_list["PRG_openfile"]
- . = TOPIC_REFRESH
- else
- to_chat(user, SPAN_WARNING("You do not have permission to read this file."))
- . = TOPIC_HANDLED
- if(href_list["PRG_newtextfile"])
- . = TOPIC_HANDLED
- var/newname = sanitize(input(usr, "Enter file name or leave blank to cancel:", "File rename"))
- if(!newname)
- return TOPIC_HANDLED
-
- if(current_filesource.create_file(newname))
- return TOPIC_REFRESH
-
- if(href_list["PRG_deletefile"])
- . = TOPIC_REFRESH
- current_filesource.delete_file(href_list["PRG_deletefile"])
-
- if(href_list["PRG_usbdeletefile"])
- . = TOPIC_REFRESH
- current_filesource.delete_file(href_list["PRG_usbdeletefile"])
-
- if(href_list["PRG_closefile"])
- . = TOPIC_REFRESH
- open_file = null
- error = null
-
- if(href_list["PRG_clone"])
- . = TOPIC_REFRESH
- current_filesource.clone_file(href_list["PRG_clone"])
-
- if(href_list["PRG_rename"])
- . = TOPIC_REFRESH
- var/datum/computer_file/F = current_filesource.get_file(href_list["PRG_rename"])
- if(!F || !istype(F))
- return
- var/newname = sanitize(input(usr, "Enter new file name:", "File rename", F.filename))
- if(F && newname)
- F.filename = newname
-
- if(href_list["PRG_edit"])
- . = TOPIC_HANDLED
- if(!open_file)
- return
- var/datum/computer_file/data/F = current_filesource.get_file(open_file)
- if(!F || !istype(F))
- return
- if(F.do_not_edit && (alert("WARNING: This file is not compatible with editor. Editing it may result in permanently corrupted formatting or damaged data consistency. Edit anyway?", "Incompatible File", "No", "Yes") == "No"))
- return
- if(F.read_only)
- error = "This file is read only. You cannot edit it."
- return
- if(!(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
- error = "You do not have write access to this file."
- return
- var/oldtext = html_decode(F.stored_data)
- oldtext = replacetext(oldtext, "\[br\]", "\n")
-
- var/newtext = sanitize(replacetext(input(usr, "Editing file [open_file]. You may use most tags used in paper formatting:", "Text Editor", oldtext) as message|null, "\n", "\[br\]"), MAX_TEXTFILE_LENGTH)
- if(!newtext || !CanInteract(user, state))
- return
-
- if(F)
- current_filesource.save_file(F.filename, newtext, accesses, user)
- return TOPIC_REFRESH
-
- if(href_list["PRG_printfile"])
- . = 1
- if(!open_file)
- return
- var/datum/computer_file/data/F = current_filesource.get_file(open_file)
- if(!F || !istype(F))
- return
- if(!computer.print_paper(digitalPencode2html(F.stored_data),F.filename,F.papertype, F.metadata))
- error = "Hardware error: Unable to print the file."
- return TOPIC_REFRESH
-
- if(href_list["PRG_stoptransfer"])
- QDEL_NULL(current_transfer)
- ui_header = null
- return TOPIC_REFRESH
-
- if(href_list["PRG_transferto"])
- . = TOPIC_REFRESH
- var/datum/computer_file/F = current_filesource.get_file(href_list["PRG_transferto"])
- if(!F || !istype(F) || F.unsendable)
- error = "I/O ERROR: Unable to transfer file."
- return
- var/copying = alert(usr, "Would you like to copy the file or transfer it? Transfering files requires write access.", "Copying file", "Copy", "Transfer")
- if(copying == "Transfer")
- if(!(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
- error = "ACCESS ERROR: You do not have permission to transfer this file"
- return
- else
- if(!(F.get_file_perms(accesses, user) & OS_READ_ACCESS))
- error = "ACCESS ERROR: You do not have permission to copy this file"
- return
- var/list/choices = list()
- for(var/T in file_sources)
- var/datum/file_storage/FS = file_sources[T]
- if(FS == current_filesource)
- continue
- choices[FS.name] = FS
- var/file_source = input(usr, "Choose a destination storage medium:", "Transfer To Another Medium") as null|anything in choices
- if(file_source)
- var/datum/file_storage/dst = choices[file_source]
- var/nope = dst.check_errors()
- if(nope)
- to_chat(user, SPAN_WARNING("Cannot transfer file to [dst] for following reason: [nope]"))
- return
- current_transfer = new(current_filesource, dst, F, copying == "Copy" ? TRUE : FALSE)
- ui_header = "downloader_running.gif"
-
-/datum/computer_file/program/filemanager/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state)
- . = ..()
- if(!.)
- return
- var/list/data = computer.initial_data()
-
- if(error)
- data["error"] = error
- else if(current_filesource)
- data["error"] = current_filesource.check_errors()
-
- data["current_source"] = current_filesource.name
- if(istype(current_filesource, /datum/file_storage/network))
- var/datum/file_storage/network/N = current_filesource
- data["fileserver"] = N.server
- if(open_file)
- var/datum/computer_file/data/F
- F = current_filesource.get_file(open_file)
- if(!istype(F))
- data["error"] = "I/O ERROR: Unable to open file."
- else
- data["filedata"] = F.generate_file_data(user)
- data["filename"] = "[F.filename].[F.filetype]"
- else
- var/list/files[0]
- for(var/datum/computer_file/F in current_filesource.get_all_files())
- files.Add(list(list(
- "name" = F.filename,
- "type" = F.filetype,
- "size" = F.size,
- "undeletable" = F.undeletable,
- "unsendable" = F.unsendable
- )))
- data["files"] = files
-
- // Don't show transfers that will be over in a tick, screw flickering
- if(current_transfer && current_transfer.get_eta() > 2)
- data |= current_transfer.get_ui_data()
-
- ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
- if (!ui)
- ui = new(user, src, ui_key, "file_manager.tmpl", "OS File Manager", 600, 700, state = state)
- ui.auto_update_layout = 1
- ui.set_initial_data(data)
- ui.set_auto_update(1)
- ui.open()
-
-/datum/computer_file/program/filemanager/process_tick()
- if(!current_transfer)
- return
- var/result = current_transfer.update_progress()
- if(!result) //something went wrong
- if(QDELETED(current_transfer)) //either completely
- error = "I/O ERROR: Unknown error during the file transfer."
- else //or during the saving at the destination
- error = "I/O ERROR: Unable to store '[current_transfer.transferring.filename]' at [current_transfer.transfer_to]"
- qdel(current_transfer)
- current_transfer = null
- ui_header = null
- return
- else if(!current_transfer.left_to_transfer) //done
- QDEL_NULL(current_transfer)
- ui_header = null
-
diff --git a/code/modules/modular_computers/file_system/programs/generic/file_manager.dm b/code/modules/modular_computers/file_system/programs/generic/file_manager.dm
new file mode 100644
index 00000000000..0e40f38133b
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/generic/file_manager.dm
@@ -0,0 +1,452 @@
+/datum/computer_file/program/filemanager
+ filename = "filemanager"
+ filedesc = "OS File Manager"
+ extended_desc = "This program allows management of files."
+ program_icon_state = "generic"
+ program_key_state = "generic_key"
+ program_menu_icon = "folder-collapsed"
+ size = 8
+ available_on_network = 0
+ undeletable = 1
+ usage_flags = PROGRAM_ALL
+ category = PROG_UTIL
+ var/open_file
+ var/error
+ var/list/disks = list() // List of list(/datum/file_storage, weakref(directory file)).
+ var/current_index
+
+ var/datum/file_transfer/current_transfer //ongoing file transfer between file_storage datums
+
+/datum/computer_file/program/filemanager/on_shutdown()
+ disks.Cut()
+ ui_header = null
+ current_index = null
+ if(current_transfer)
+ qdel(current_transfer)
+ return ..()
+
+/datum/computer_file/program/filemanager/Topic(href, href_list, state)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/user = usr
+ var/list/accesses = computer.get_access(user)
+
+ if(href_list["PRG_select_disk"])
+ var/disk_name = href_list["PRG_select_disk"]
+ var/datum/file_storage/selected_disk = computer.mounted_storage[disk_name]
+ if(selected_disk)
+ disks += list(list(selected_disk, null))
+ current_index = disks.len
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_mount_network"])
+ var/datum/computer_network/network = computer.get_network()
+ if(!network)
+ to_chat(user, SPAN_WARNING("NETWORK ERROR: No connectivity to the network."))
+ return TOPIC_HANDLED
+ if(!computer.get_network_status(NET_FEATURE_FILESYSTEM))
+ to_chat(user, SPAN_WARNING("NETWORK ERROR: The network denied filesystem access."))
+ return TOPIC_HANDLED
+ var/list/available_mainframes = network.get_file_server_tags(MF_ROLE_FILESERVER, accesses)
+ if(!length(available_mainframes))
+ to_chat(user, SPAN_WARNING("NETWORK ERROR: No available mainframes on the network."))
+ var/fileserver_tag = input(user, "Choose a mainframe you would like to mount as a disk:", "Mainframe Mount") as null|anything in available_mainframes
+ if(!fileserver_tag)
+ return TOPIC_HANDLED
+ var/root_name = sanitize(input(user, "Enter the name of the root directory for the newly mounted disk.", "Root directory") as null|text)
+ if(!root_name)
+ return TOPIC_HANDLED
+ var/feedback = computer.mount_mainframe(root_name, fileserver_tag)
+ to_chat(user, SPAN_NOTICE(feedback))
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_unmount_network"])
+ var/root_name = href_list["PRG_unmount_network"]
+ var/datum/file_storage/network/avail_disk = computer.mounted_storage[root_name]
+ if(!istype(avail_disk))
+ return TOPIC_REFRESH
+ if(!avail_disk.hidden && avail_disk.check_access(accesses))
+ computer.unmount_storage(root_name)
+ return TOPIC_REFRESH
+
+ to_chat(user, SPAN_WARNING("You lack permission to unmount this network drive!"))
+ return TOPIC_HANDLED
+
+ if(href_list["PRG_change_disk"])
+ var/disk_index = text2num(href_list["PRG_change_disk"])
+ if(disk_index == 0 || disk_index > disks.len)
+ current_index = 0
+ return TOPIC_REFRESH
+ current_index = disk_index
+ return TOPIC_REFRESH
+
+ if(!current_index || current_index > disks.len)
+ current_index = 0
+ return TOPIC_REFRESH
+
+ var/datum/file_storage/current_disk = disks[current_index]?[1]
+ if(!current_disk)
+ return TOPIC_HANDLED
+
+ if(href_list["PRG_exit_disk"])
+ if(!current_index)
+ return TOPIC_HANDLED
+ disks -= list(disks[current_index]) // We have to enclose the list in another list to get it to actually remove it from disks.
+ current_index--
+ return TOPIC_REFRESH
+
+ var/errors = current_disk.check_errors()
+ if(errors)
+ error = errors
+ return TOPIC_REFRESH
+
+ var/weakref/dir_ref = disks[current_index][2]
+ var/datum/computer_file/directory/current_directory
+ if(istype(dir_ref))
+ current_directory = dir_ref.resolve()
+ if(!current_directory || !(current_directory in current_disk.get_all_files()))
+ disks[current_index][2] = null
+ current_directory = null
+ else
+ disks[current_index][2] = null
+
+ if(href_list["PRG_up_directory"])
+ var/datum/computer_file/directory/parent_dir = current_directory?.get_directory()
+ if(parent_dir)
+ disks[current_index][2] = weakref(parent_dir)
+ else
+ disks[current_index][2] = null
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_openfile"])
+ . = TOPIC_HANDLED
+ var/datum/computer_file/F = current_disk.get_file(href_list["PRG_openfile"], current_directory)
+ if(!F)
+ return TOPIC_HANDLED
+ if(istype(F, /datum/computer_file/directory))
+ if(F.get_file_perms(accesses, user) & OS_READ_ACCESS)
+ disks[current_index][2] = weakref(F)
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("You do not have permission to read this file."))
+ return TOPIC_HANDLED
+
+ if(istype(F, /datum/computer_file/data))
+ if(F.get_file_perms(accesses, user) & OS_READ_ACCESS)
+ open_file = href_list["PRG_openfile"]
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("You do not have permission to open this directory."))
+ return TOPIC_HANDLED
+
+ to_chat(user, SPAN_WARNING("This file is not in a readable format."))
+ return TOPIC_HANDLED
+
+ if(href_list["PRG_newtextfile"])
+ . = TOPIC_REFRESH
+ var/newname = sanitize_for_file(input(usr, "Enter file name or leave blank to cancel:", "New file"))
+ if(!length(newname))
+ return TOPIC_HANDLED
+
+ var/created_file = current_disk.create_file(newname, current_directory, file_type = /datum/computer_file/data/text, accesses = accesses, user = user)
+ if(created_file != OS_FILE_SUCCESS)
+ switch(created_file)
+ if(OS_FILE_NO_WRITE)
+ to_chat(user, SPAN_WARNING("You lack permission to create a file in this directory."))
+ return TOPIC_HANDLED
+ if(OS_NETWORK_ERROR)
+ to_chat(user, SPAN_WARNING("Unable to access directory on the network."))
+ return TOPIC_REFRESH
+ if(OS_HARDDRIVE_SPACE)
+ to_chat(user, SPAN_WARNING("Unable to create new file. The hard drive is full."))
+ return TOPIC_HANDLED
+ else
+ to_chat(user, SPAN_WARNING("Unable to create new file. The hard drive may be non-functional."))
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_newdir"])
+ . = TOPIC_REFRESH
+ var/newname = sanitize_for_file(input(usr, "Enter directory name or leave blank to cancel:", "New directory"))
+ if(!length(newname))
+ return TOPIC_HANDLED
+ var/created_dir = current_disk.create_directory(newname, current_directory, accesses, user)
+ if(created_dir != OS_FILE_SUCCESS)
+ switch(created_dir)
+ if(OS_FILE_NO_WRITE)
+ to_chat(user, SPAN_WARNING("You lack permission to create a directory in this directory."))
+ return TOPIC_HANDLED
+ if(OS_NETWORK_ERROR)
+ to_chat(user, SPAN_WARNING("Unable to access directory on the network."))
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("Unable to create new directory. The hard drive may be non-functional."))
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_deletefile"])
+ var/datum/computer_file/del_file = current_disk.get_file(href_list["PRG_deletefile"], current_directory, accesses, user)
+ var/deleted = current_disk.delete_file(del_file, accesses, user)
+ if(deleted != OS_FILE_SUCCESS)
+ switch(deleted)
+ if(OS_FILE_NO_WRITE)
+ to_chat(user, SPAN_WARNING("You lack permission to delete '[href_list["PRG_deletefile"]]'."))
+ return TOPIC_HANDLED
+ if(OS_NETWORK_ERROR)
+ to_chat(user, SPAN_WARNING("Unable to access '[href_list["PRG_deletefile"]]' on the network."))
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("Failed to delete '[href_list["PRG_deletefile"]]'. The hard drive may be non-functional."))
+ return TOPIC_REFRESH
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_closefile"])
+ . = TOPIC_REFRESH
+ open_file = null
+ error = null
+
+ if(href_list["PRG_clone"])
+ var/cloned = current_disk.clone_file(href_list["PRG_clone"], current_directory, accesses, user)
+ if(cloned != OS_FILE_SUCCESS)
+ switch(cloned)
+ if(OS_FILE_NO_READ)
+ to_chat(user, SPAN_WARNING("You lack permission to clone the file '[href_list["PRG_clone"]]'."))
+ return TOPIC_HANDLED
+ if(OS_NETWORK_ERROR)
+ to_chat(user, SPAN_WARNING("Unable to access file '[href_list["PRG_clone"]]' on the network."))
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("Unable to clone file '[href_list["PRG_clone"]]'. The hard drive may be non-functional."))
+ return TOPIC_REFRESH
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_rename"])
+ var/datum/computer_file/F = current_disk.get_file(href_list["PRG_rename"], current_directory)
+ if(!F || !istype(F))
+ return TOPIC_REFRESH
+ if(F.unrenamable)
+ to_chat(user, SPAN_WARNING("You do not have permission to rename that file."))
+ return TOPIC_HANDLED
+ if(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS)
+ var/newname = sanitize_for_file(input(user, "Enter new file name:", "File rename", F.filename))
+ if(F && length(newname))
+ F.filename = newname
+ return TOPIC_REFRESH
+ else
+ to_chat(user, SPAN_WARNING("You do not have permission to rename that file."))
+ return TOPIC_HANDLED
+
+ if(href_list["PRG_edit"])
+ . = TOPIC_HANDLED
+ if(!open_file)
+ return
+ var/datum/computer_file/data/F = current_disk.get_file(open_file, current_directory)
+ if(!F || !istype(F))
+ return
+ if(F.do_not_edit && (alert("WARNING: This file is not compatible with editor. Editing it may result in permanently corrupted formatting or damaged data consistency. Edit anyway?", "Incompatible File", "No", "Yes") == "No"))
+ return
+ if(F.read_only)
+ error = "This file is read only. You cannot edit it."
+ return
+ if(!(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
+ error = "You do not have write access to this file."
+ return
+ var/oldtext = html_decode(F.stored_data)
+ oldtext = replacetext(oldtext, "\[br\]", "\n")
+
+ var/newtext = sanitize(replacetext(input(user, "Editing file [open_file]. You may use most tags used in paper formatting:", "Text Editor", oldtext) as message|null, "\n", "\[br\]"), MAX_TEXTFILE_LENGTH)
+ if(!newtext || !CanInteract(user, state))
+ return
+
+ if(F)
+ current_disk.save_file(F.filename, current_directory, newtext, null, accesses, user)
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_printfile"])
+ . = 1
+ if(!open_file)
+ return
+ var/datum/computer_file/data/F = current_disk.get_file(open_file, current_directory)
+ if(!F || !istype(F))
+ return
+ if(!(F.get_file_perms(accesses, user) & OS_READ_ACCESS))
+ error = "You do not have read access to this file."
+ return
+ if(!computer.print_paper(digitalPencode2html(F.stored_data), F.filename))
+ error = "Hardware error: Unable to print the file."
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_stoptransfer"])
+ QDEL_NULL(current_transfer)
+ ui_header = null
+ return TOPIC_REFRESH
+
+ if(href_list["PRG_transferto"])
+ . = TOPIC_REFRESH
+ var/datum/computer_file/F = current_disk.get_file(href_list["PRG_transferto"], current_directory)
+ if(!F || !istype(F) || F.unsendable)
+ error = "I/O ERROR: Unable to transfer file."
+ return
+
+ var/copying = alert(usr, "Would you like to copy the file or transfer it? Transfering files requires write access.", "Copying file", "Copy", "Transfer")
+ var/list/choices = list()
+ var/list/curr_fs_list = disks[current_index]
+ for(var/list/fs_list in disks)
+ // Skip over the disk if its referencing the same directory.
+ if(curr_fs_list[1] == fs_list[1] && curr_fs_list[2] == fs_list[2])
+ continue
+
+ var/datum/file_storage/FS = fs_list[1]
+ var/weakref/FS_dir_ref = fs_list[2]
+
+ var/datum/computer_file/directory/FS_dir = FS_dir_ref?.resolve()
+ choices[FS.get_dir_path(FS_dir, TRUE)] = fs_list
+
+ if(!length(choices))
+ to_chat(usr, SPAN_WARNING("You must open another disk to transfer files."))
+ return TOPIC_HANDLED
+
+ var/storage = input(usr, "Choose a destination storage medium:", "Transfer To Another Medium") as null|anything in choices
+ if(storage)
+ var/list/chosen_list = choices[storage]
+ var/datum/file_storage/dst = chosen_list[1]
+ var/nope = dst.check_errors()
+ if(nope)
+ to_chat(user, SPAN_WARNING("Cannot transfer file to [dst] for following reason: [nope]"))
+ return
+
+ var/weakref/dst_dir_ref = chosen_list[2]
+ var/datum/computer_file/directory/dst_dir = dst_dir_ref?.resolve()
+
+ var/error = check_file_transfer(dst_dir, F, copying == "Copy", accesses, user)
+ if(error)
+ to_chat(user, SPAN_WARNING("Cannot transfer file. [error]."))
+ return TOPIC_HANDLED
+ current_transfer = new(current_disk, dst, dst_dir, F, copying == "Copy" ? TRUE : FALSE)
+ ui_header = "downloader_running.gif"
+
+/datum/computer_file/program/filemanager/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state)
+ . = ..()
+ if(!.)
+ return
+ var/list/data = computer.initial_data()
+ var/list/accesses = computer.get_access(user)
+
+ if(current_index > disks.len) // Safety check.
+ current_index = 0
+
+ var/datum/file_storage/current_disk
+ var/datum/computer_file/directory/current_directory
+
+ if(current_index)
+ var/list/current_disk_list = disks[current_index]
+ current_disk = current_disk_list[1]
+
+ var/weakref/dir_ref = current_disk_list[2]
+ if(istype(dir_ref))
+ current_directory = dir_ref.resolve()
+ else
+ current_disk_list[2] = null
+
+ if(error)
+ data["error"] = error
+ else if(current_disk)
+ data["error"] = current_disk.check_errors()
+
+ if(!data["error"])
+ var/list/ui_disks[0]
+ var/disk_index = 1
+ for(var/list/ui_disk_list in disks)
+ var/datum/file_storage/ui_disk = ui_disk_list[1]
+
+ var/weakref/ui_dir_ref = ui_disk_list[2]
+ var/datum/computer_file/directory/ui_directory
+ if(istype(ui_dir_ref))
+ ui_directory = ui_dir_ref.resolve()
+ else
+ ui_disk_list[2] = null
+
+ ui_disks.Add(list(list(
+ "name" = ui_disk.get_dir_path(ui_directory),
+ "index" = disk_index,
+ "selected" = disk_index == current_index
+ )))
+ disk_index++
+ data["disks"] = ui_disks
+
+ if(current_disk)
+ data["up_directory"] = !!current_directory
+ data["current_disk"] = current_disk.get_dir_path(current_directory, TRUE)
+
+ if(open_file)
+ var/datum/computer_file/data/F
+ F = current_disk.get_file(open_file, current_directory)
+ if(!istype(F))
+ data["error"] = "I/O ERROR: Unable to open file."
+ else
+ data["filedata"] = F.generate_file_data(user)
+ data["filename"] = "[F.filename].[F.filetype]"
+ else
+ var/list/files[0]
+ for(var/datum/computer_file/F in current_disk.get_dir_files(current_directory))
+ files.Add(list(list(
+ "name" = F.filename,
+ "type" = F.filetype,
+ "dir" = istype(F, /datum/computer_file/directory),
+ "size" = F.size,
+ "undeletable" = F.undeletable,
+ "unrenamable" = F.unrenamable,
+ "unsendable" = F.unsendable
+ )))
+ data["files"] = files
+ else // No disk selected, option to create a new one.
+ var/list/avail_disks[0]
+
+ for(var/root_name in computer.mounted_storage)
+ var/datum/file_storage/avail_disk = computer.mounted_storage[root_name]
+ if(!avail_disk.hidden && avail_disk.check_access(accesses))
+ avail_disks.Add(list(list(
+ "name" = root_name,
+ "desc" = avail_disk.desc,
+ "is_network" = istype(avail_disk, /datum/file_storage/network)
+ )))
+
+ data["avail_disks"] = avail_disks
+ // Don't show transfers that will be over in a tick, screw flickering
+ if(current_transfer && current_transfer.get_eta() > 2)
+ data |= current_transfer.get_ui_data()
+
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "file_manager.tmpl", "OS File Manager", 900, 700, state = state)
+ ui.auto_update_layout = 1
+ ui.set_initial_data(data)
+ ui.set_auto_update(1)
+ ui.open()
+
+/datum/computer_file/program/filemanager/process_tick()
+ if(!current_transfer)
+ return
+ var/result = current_transfer.update_progress()
+ if(result != OS_FILE_SUCCESS) //something went wrong
+ if(QDELETED(current_transfer)) //either completely
+ error = "I/O ERROR: Unknown error during the file transfer."
+ else //or during the saving at the destination
+ error = "I/O ERROR: Unable to store '[current_transfer.transferring.filename]' at '[current_transfer.transfer_to.get_dir_path(current_transfer.directory_to, TRUE)]'"
+ qdel(current_transfer)
+ current_transfer = null
+ ui_header = null
+ return
+ else if(!current_transfer.left_to_transfer) //done
+ QDEL_NULL(current_transfer)
+ ui_header = null
+
+/datum/computer_file/program/filemanager/on_file_storage_removal(datum/file_storage/removed)
+ var/list/current_disk_list = disks[current_index]
+ for(var/list/disk_list in disks)
+ var/datum/file_storage/disk = disk_list[1]
+ if(disk == removed)
+ if(current_disk_list == disk_list)
+ current_index = null
+
+ disks -= list(disk_list)
\ No newline at end of file
diff --git a/code/modules/modular_computers/file_system/programs/generic/game.dm b/code/modules/modular_computers/file_system/programs/generic/game.dm
index e2592d455dc..77c40f0223e 100644
--- a/code/modules/modular_computers/file_system/programs/generic/game.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/game.dm
@@ -27,10 +27,10 @@
filedesc = "Defeat [picked_enemy_name]"
// Important in order to ensure that copied versions will have the same enemy name.
-/datum/computer_file/program/game/clone()
- var/datum/computer_file/program/game/G = ..()
- G.picked_enemy_name = picked_enemy_name
- return G
+/datum/computer_file/program/game/PopulateClone(datum/computer_file/program/game/clone)
+ clone = ..()
+ clone.picked_enemy_name = picked_enemy_name
+ return clone
// When running the program, we also want to pass our enemy name to the nano module.
/datum/computer_file/program/game/on_startup()
diff --git a/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm
index c67162cf60b..54aadb3a8b0 100644
--- a/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm
@@ -12,15 +12,21 @@
available_on_network = 0
nanomodule_path = /datum/nano_module/program/computer_appdownloader/
ui_header = "downloader_finished.gif"
- var/hacked_download = 0
var/downloaderror
var/list/downloads_queue[0]
- var/server
usage_flags = PROGRAM_ALL
category = PROG_UTIL
var/datum/file_transfer/current_transfer
+/datum/computer_file/program/appdownloader/on_startup(mob/living/user, datum/extension/interactive/os/new_host)
+ . = ..()
+ // Initialize the internal "appdownload" disk if necessary.
+ var/datum/file_storage/network/app_download = new_host.mounted_storage["appdownload"]
+ if(!istype(app_download)) // If you had another network disk named appdownload it will be appropriated.
+ qdel(app_download)
+ new_host.mounted_storage["appdownload"] = new /datum/file_storage/network(new_host, "appdownload", TRUE)
+
/datum/computer_file/program/appdownloader/on_shutdown()
..()
QDEL_NULL(current_transfer)
@@ -33,16 +39,23 @@
var/datum/computer_network/net = computer.get_network()
if(!net)
return 0
-
+
if(!check_file_download(filename))
return 0
- var/datum/computer_file/program/PRG = net.find_file_by_name(filename, MF_ROLE_SOFTWARE)
- var/datum/file_storage/disk/destination = new(computer)
- var/datum/file_storage/network/source = new(computer)
- source.server = net.find_file_location(filename, MF_ROLE_SOFTWARE)
+ var/datum/computer_file/program/PRG = net.find_file_by_name(filename, OS_PROGRAMS_DIR, MF_ROLE_SOFTWARE)
+ var/datum/file_storage/disk/destination = computer.mounted_storage["local"]
+ if(!destination)
+ return 0
+ var/datum/file_storage/network/source = computer.mounted_storage["appdownload"]
+ if(!source)
+ return 0
+ var/datum/computer_file/directory/programs_directory = destination.parse_directory(OS_PROGRAMS_DIR, TRUE)
+ if(!programs_directory)
+ return 0
+ source.set_server(net.find_file_location(PRG, mainframe_role = MF_ROLE_SOFTWARE))
if(source.check_errors() || destination.check_errors())
return 0
- current_transfer = new(source, destination, PRG, TRUE)
+ current_transfer = new(source, destination, programs_directory, PRG, TRUE)
ui_header = "downloader_running.gif"
generate_network_log("Downloading file [filename] from [source.server].")
@@ -52,12 +65,12 @@
var/datum/computer_network/net = computer.get_network()
if(!net)
return 0
- var/datum/computer_file/program/PRG = net.find_file_by_name(filename, MF_ROLE_SOFTWARE)
+ var/datum/computer_file/program/PRG = net.find_file_by_name(filename, OS_PROGRAMS_DIR, MF_ROLE_SOFTWARE)
- if(!PRG || !istype(PRG))
+ if(!istype(PRG))
return 0
- if(!computer || !computer.try_store_file(PRG))
+ if(!computer || (computer.try_store_file(PRG, computer.programs_dir) != OS_FILE_SUCCESS))
return 0
return 1
@@ -71,9 +84,9 @@
/datum/computer_file/program/appdownloader/process_tick()
if(!current_transfer)
return
-
+
var/result = current_transfer.update_progress()
- if(!result) //something went wrong
+ if(result != OS_FILE_SUCCESS) //something went wrong
if(QDELETED(current_transfer)) //either completely
downloaderror = "I/O ERROR: Unknown error during the file transfer."
else //or during the saving at the destination
diff --git a/code/modules/modular_computers/file_system/programs/generic/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/generic/ntnrc_client.dm
index e9215b389d7..f63674d87b3 100644
--- a/code/modules/modular_computers/file_system/programs/generic/ntnrc_client.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/ntnrc_client.dm
@@ -123,7 +123,7 @@
logfile.stored_data += "[logstring]\[BR\]"
logfile.stored_data += "\[b\]Logfile dump completed.\[/b\]"
logfile.calculate_size()
- if(!computer.store_file(logfile))
+ if(!computer.store_file(logfile, OS_LOGS_DIR, create_directories = TRUE))
computer.show_error(user, "I/O Error - Check hard drive and free space. Required space: [logfile.size]GQ.")
if(href_list["PRG_renamechannel"])
. = 1
diff --git a/code/modules/modular_computers/file_system/programs/generic/records.dm b/code/modules/modular_computers/file_system/programs/generic/records.dm
index 174f94b722d..d5b290f7269 100644
--- a/code/modules/modular_computers/file_system/programs/generic/records.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/records.dm
@@ -57,7 +57,7 @@
var/datum/extension/interactive/os/os = get_extension(PC, /datum/extension/interactive/os)
if(os && os.emagged())
user_access = user_access ? user_access.Copy() : list()
- user_access |= access_syndicate
+ user_access |= access_hacked
return user_access
@@ -97,17 +97,16 @@
if(!network)
to_chat(usr, SPAN_WARNING("Network error."))
return
-
- if(!length(network.get_mainframes_by_role(MF_ROLE_CREW_RECORDS, get_access(usr))))
+ var/list/accesses = get_access(usr)
+ if(!network.get_mainframes_by_role(MF_ROLE_CREW_RECORDS, accesses))
to_chat(usr, SPAN_WARNING("You may not have access to generate new crew records, or there may not be a crew record mainframe active on the network."))
return
-
- var/datum/computer_file/report/crew_record/new_record = new()
- if(!network.store_file(new_record, MF_ROLE_CREW_RECORDS, get_access(usr)))
- to_chat(usr, SPAN_WARNING("Failed to generate a new crew record. All crew record mainframes on the network may be non-functional or out of storage space."))
- qdel(new_record)
+ active_record = new/datum/computer_file/report/crew_record()
+ if(network.store_file(active_record, OS_RECORDS_DIR, TRUE, accesses, usr, mainframe_role = MF_ROLE_CREW_RECORDS) != OS_FILE_SUCCESS)
+ to_chat(usr, SPAN_WARNING("Unable to store new crew record. The file server may be non-functional or out of disk space."))
+ qdel(active_record)
+ active_record = null
return
- active_record = new_record
global.all_crew_records.Add(active_record)
return 1
if(href_list["print_active"])
diff --git a/code/modules/modular_computers/file_system/programs/generic/reports.dm b/code/modules/modular_computers/file_system/programs/generic/reports.dm
index e547030d046..4897bd58385 100644
--- a/code/modules/modular_computers/file_system/programs/generic/reports.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/reports.dm
@@ -69,7 +69,7 @@
selected_report.rename_file()
if(program.computer.store_file(selected_report))
saved_report = selected_report
- selected_report = saved_report.clone()
+ selected_report = saved_report.Clone()
to_chat(user, "The report has been saved as '[saved_report.filename].[saved_report.filetype]'.")
else
to_chat(user, "Error storing file. Please check your hard drive.")
@@ -96,7 +96,7 @@
return
can_view_only = 0
saved_report = chosen_report
- selected_report = chosen_report.clone()
+ selected_report = chosen_report.Clone()
return 1
/datum/nano_module/program/reports/Topic(href, href_list)
@@ -160,7 +160,7 @@
selected_report.rename_file()
file.stored_data = selected_report.generate_pencode(get_access(user), user, no_html = 1) //TXT files can't have html; they use pencode only.
file.filename = selected_report.filename
- if(program.computer.store_file(file))
+ if(program.computer.store_file(file, "reports", create_directories = TRUE))
to_chat(user, "The report has been exported as '[file.filename].[file.filetype]'.")
else
to_chat(user, "Error storing file. Please check your hard drive.")
@@ -174,7 +174,7 @@
var/datum/computer_network/net = program.computer.get_network()
for(var/datum/computer_file/report/report in net.fetch_reports(get_access(user), user))
if(report.uid == uid)
- selected_report = report.clone()
+ selected_report = report.Clone()
can_view_only = 0
switch_state(REPORTS_VIEW)
return 1
diff --git a/code/modules/modular_computers/file_system/programs/generic/scanner.dm b/code/modules/modular_computers/file_system/programs/generic/scanner.dm
index c79f23471de..ff721374a1c 100644
--- a/code/modules/modular_computers/file_system/programs/generic/scanner.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/scanner.dm
@@ -7,8 +7,8 @@
size = 6
available_on_network = 1
usage_flags = PROGRAM_ALL
- nanomodule_path = /datum/nano_module/program/scanner
category = PROG_UTIL
+ nanomodule_path = /datum/nano_module/program/scanner
var/using_scanner = 0 //Whether or not the program is synched with the scanner module.
var/data_buffer = "" //Buffers scan output for saving/viewing.
@@ -36,13 +36,6 @@
metadata_buffer.Cut()
return 1
-/datum/computer_file/program/scanner/proc/save_scan(name)
- if(!data_buffer)
- return 0
- if(!create_file(name, data_buffer, scan_file_type, metadata_buffer.Copy()))
- return 0
- return 1
-
/datum/computer_file/program/scanner/proc/check_scanning()
if(!computer)
return 0
@@ -61,7 +54,7 @@
/datum/computer_file/program/scanner/Topic(href, href_list)
if(..())
- return 1
+ return TOPIC_HANDLED
if(href_list["connect_scanner"])
if(text2num(href_list["connect_scanner"]))
@@ -69,22 +62,26 @@
to_chat(usr, "Scanner installation failed.")
else
disconnect_scanner()
- return 1
+ return TOPIC_REFRESH
if(href_list["scan"])
if(check_scanning())
metadata_buffer.Cut()
var/obj/item/stock_parts/computer/scanner/scanner = computer.get_component(PART_SCANNER)
scanner.run_scan(usr, src)
- return 1
+ return TOPIC_REFRESH
if(href_list["save"])
- var/name = sanitize(input(usr, "Enter file name:", "Save As") as text|null)
- if(!save_scan(name))
- to_chat(usr, "Scan save failed.")
+ if(!data_buffer)
+ to_chat(usr, SPAN_WARNING("No data to export!"))
+ return TOPIC_HANDLED
+
+ var/datum/computer_file/data/scan_file = new scan_file_type()
+ scan_file.stored_data = data_buffer
- if(.)
- SSnano.update_uis(NM)
+ // This saves the file, so no additional handling on the program's end is required.
+ view_file_browser(usr, "saving_file", scan_file_type, OS_WRITE_ACCESS, "Save scan file", scan_file)
+ return TOPIC_HANDLED
/datum/nano_module/program/scanner
name = "Scanner"
diff --git a/code/modules/modular_computers/file_system/programs/generic/wordprocessor.dm b/code/modules/modular_computers/file_system/programs/generic/wordprocessor.dm
index 6c6afa5b8f1..d83417438bc 100644
--- a/code/modules/modular_computers/file_system/programs/generic/wordprocessor.dm
+++ b/code/modules/modular_computers/file_system/programs/generic/wordprocessor.dm
@@ -6,24 +6,32 @@
program_key_state = "atmos_key"
size = 4
available_on_network = 1
- nanomodule_path = /datum/nano_module/program/computer_wordprocessor/
usage_flags = PROGRAM_ALL
category = PROG_OFFICE
- var/browsing
- var/open_file
+ var/open_file // Name of the file currently open.
+ var/file_directory // Directory of the file currently open.
+
var/loaded_data
var/error
var/is_edited
/datum/computer_file/program/wordprocessor/on_shutdown(forced)
. = ..()
- browsing = null
open_file = null
+ file_directory = null
loaded_data = null
error = null
- is_edited = null
+ is_edited = FALSE
+
+/datum/computer_file/program/wordprocessor/on_file_select(datum/file_storage/disk, datum/computer_file/directory/dir, datum/computer_file/selected, selecting_key, mob/user)
+ var/datum/computer_file/data/text/T = selected
+ loaded_data = T.stored_data
+ open_file = T.filename
+ file_directory = disk.get_dir_path(dir, TRUE)
+ is_edited = FALSE
+ . = ..()
/datum/computer_file/program/wordprocessor/proc/open_file(var/openingfile, var/list/accesses, var/mob/user)
var/datum/computer_file/data/F = get_file(openingfile)
@@ -36,10 +44,23 @@
return TRUE
error = "I/O error: Unable to open file '[openingfile]'."
-/datum/computer_file/program/wordprocessor/proc/save_file(var/savingfile)
- . = computer.save_file(savingfile, loaded_data, /datum/computer_file/data/text)
- if(.)
- is_edited = 0
+/datum/computer_file/program/wordprocessor/proc/save_file(mob/user)
+ var/datum/computer_file/result = computer.save_file(open_file, file_directory, loaded_data, /datum/computer_file/data/text, null, computer.get_access(user), user)
+ . = FALSE
+ if(istype(result))
+ to_chat(user, SPAN_NOTICE("Successfully saved file '[open_file]'."))
+ is_edited = FALSE
+ return TRUE
+ // Errored!
+ switch(result)
+ if(OS_BAD_NAME)
+ error = "I/O error: Invalid file name '[open_file]'."
+ if(OS_FILE_NOT_FOUND)
+ error = "I/O error: Directory not found."
+ if(OS_FILE_NO_WRITE)
+ error = "I/O error: You do not have permission to modify file '[open_file]'"
+ else
+ error = "I/O error: Harddrive may be non-functional."
#define MAX_FIELDS_NUM 50
@@ -47,98 +68,67 @@
if(..())
return 1
- if(href_list["PRG_txtrpeview"])
+ if(href_list["PRG_txtpreview"])
show_browser(usr,"
[open_file][digitalPencode2html(loaded_data)]", "window=[open_file]")
- return 1
+ return TOPIC_HANDLED
if(href_list["PRG_taghelp"])
var/datum/codex_entry/entry = SScodex.get_codex_entry("pen")
if(entry)
SScodex.present_codex_entry(usr, entry)
- return 1
-
- if(href_list["PRG_closebrowser"])
- browsing = 0
- return 1
+ return TOPIC_HANDLED
if(href_list["PRG_backtomenu"])
error = null
- return 1
-
- if(href_list["PRG_loadmenu"])
- browsing = 1
- return 1
+ return TOPIC_REFRESH
if(href_list["PRG_openfile"])
- . = 1
if(is_edited)
if(alert("Would you like to save your changes first?",,"Yes","No") == "Yes")
- save_file(open_file)
- browsing = 0
- open_file(href_list["PRG_openfile"], NM.get_access(usr), usr)
+ if(!save_file(usr))
+ return TOPIC_HANDLED
+ var/browser_desc = "Select a file to open"
+ view_file_browser(usr, "open_file", /datum/computer_file/data/text, OS_READ_ACCESS, browser_desc)
+ return TOPIC_HANDLED
if(href_list["PRG_newfile"])
- . = 1
if(is_edited)
if(alert("Would you like to save your changes first?",,"Yes","No") == "Yes")
- save_file(open_file)
-
- var/newname = sanitize(input(usr, "Enter file name:", "New File") as text|null)
- if(!newname)
- return 1
- var/datum/computer_file/data/F = create_file(newname, "", /datum/computer_file/data/text)
- if(F)
- open_file = F.filename
- loaded_data = ""
-
- // Set the write/mod access to the current account if it exists.
- var/datum/computer_file/data/account/A = computer.get_account()
- if(A)
- var/datum/computer_network/network = computer.get_network()
- LAZYADD(F.write_access, list(list("[A.login]@[network.network_id]")))
- LAZYADD(F.mod_access, list(list("[A.login]@[network.network_id]")))
- return 1
- else
- error = "I/O error: Unable to create file '[href_list["PRG_saveasfile"]]'."
+ if(!save_file(usr))
+ return TOPIC_HANDLED
+
+ var/browser_desc = "Create new file"
+ var/datum/computer_file/data/text/saving = new()
+ view_file_browser(usr, "create_file", /datum/computer_file/data/text, OS_WRITE_ACCESS, browser_desc, saving)
+ return TOPIC_HANDLED
if(href_list["PRG_saveasfile"])
- . = 1
- var/newname = sanitize(input(usr, "Enter file name:", "Save As") as text|null)
- if(!newname)
- return 1
- var/datum/computer_file/data/F = create_file(newname, loaded_data, /datum/computer_file/data/text)
- if(F)
- var/datum/computer_file/data/account/A = computer.get_account()
- if(A)
- var/datum/computer_network/network = computer.get_network()
- LAZYADD(F.write_access, list(list("[A.login]@[network.network_id]")))
- LAZYADD(F.mod_access, list(list("[A.login]@[network.network_id]")))
-
- open_file = F.filename
- else
- error = "I/O error: Unable to create file '[href_list["PRG_saveasfile"]]'."
- return 1
+ var/browser_desc = "Save file as"
+ var/datum/computer_file/data/text/saving = new()
+ saving.filename = open_file ? open_file : "NewFile"
+ saving.stored_data = loaded_data
+ view_file_browser(usr, "saveas_file", /datum/computer_file/data/text, OS_WRITE_ACCESS, browser_desc, saving)
+ return TOPIC_HANDLED
if(href_list["PRG_savefile"])
- . = 1
if(!open_file)
- open_file = sanitize(input(usr, "Enter file name:", "Save As") as text|null)
- if(!open_file)
- return 0
- if(!save_file(open_file))
- error = "I/O error: Unable to save file '[open_file]'. Access may be denied."
- return 1
+ var/browser_desc = "Save file as"
+ var/datum/computer_file/data/text/saving = new()
+ saving.stored_data = loaded_data
+ view_file_browser(usr, "saveas_file", /datum/computer_file/data/text, OS_WRITE_ACCESS, browser_desc, saving)
+ return TOPIC_HANDLED
+
+ save_file(usr)
+ return TOPIC_REFRESH
if(href_list["PRG_editfile"])
var/oldtext = html_decode(loaded_data)
oldtext = replacetext(oldtext, "\[br\]", "\n")
- var/datum/computer_file/data/F = get_file(open_file)
- if(!F)
- error = "I/O error: File not found."
- return 1
- if(!(F.get_file_perms(NM.get_access(usr), usr) & OS_WRITE_ACCESS))
- error = "I/O error: You do not have permission to edit this file."
- return 1
+ if(open_file)
+ var/datum/computer_file/data/F = get_file(open_file, file_directory, computer.get_access(usr), usr)
+ if(istype(F) && !(F.get_file_perms(computer.get_access(usr), usr) & OS_WRITE_ACCESS))
+ error = "I/O error: You do not have permission to edit this file."
+ return TOPIC_REFRESH
var/newtext = sanitize(replacetext(input(usr, "Editing file '[open_file]'. You may use most tags used in paper formatting:", "Text Editor", oldtext) as message|null, "\n", "\[br\]"), MAX_TEXTFILE_LENGTH)
if(!newtext)
return
@@ -155,56 +145,28 @@
loaded_data = newtext
is_edited = 1
- return 1
+ return TOPIC_REFRESH
if(href_list["PRG_printfile"])
- . = 1
if(!computer.print_paper(digitalPencode2html(loaded_data)))
error = "Hardware error: Printer missing or out of paper."
- return 1
+ return TOPIC_HANDLED
#undef MAX_FIELDS_NUM
-/datum/nano_module/program/computer_wordprocessor
- name = "Word Processor"
-
-/datum/nano_module/program/computer_wordprocessor/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state)
- var/list/data = host.initial_data()
- var/datum/computer_file/program/wordprocessor/PRG
- PRG = program
-
- if(PRG.error)
- data["error"] = PRG.error
- if(PRG.browsing)
- data["browsing"] = PRG.browsing
- if(!PRG.computer || !PRG.computer.has_component(PART_HDD))
- data["error"] = "I/O ERROR: Unable to access hard drive."
- else
- var/list/files[0]
- for(var/datum/computer_file/F in PRG.computer.get_all_files())
- if(F.filetype == "TXT")
- files.Add(list(list(
- "name" = F.filename,
- "size" = F.size
- )))
- data["files"] = files
-
- var/obj/item/stock_parts/computer/drive_slot/RHDD = PRG.computer.get_component(PART_D_SLOT)
- if(istype(RHDD) && istype(RHDD.stored_drive))
- data["usbconnected"] = 1
- var/list/usbfiles[0]
- for(var/datum/computer_file/F in PRG.computer.get_all_files(RHDD.stored_drive))
- if(F.filetype == "TXT")
- usbfiles.Add(list(list(
- "name" = F.filename,
- "size" = F.size,
- )))
- data["usbfiles"] = usbfiles
- else if(PRG.open_file)
- data["filedata"] = digitalPencode2html(PRG.loaded_data)
- data["filename"] = PRG.is_edited ? "[PRG.open_file]*" : PRG.open_file
+/datum/computer_file/program/wordprocessor/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state)
+ . = ..()
+ if(!.)
+ return
+ var/list/data = computer.initial_data()
+
+ if(error)
+ data["error"] = error
+ if(open_file)
+ data["filedata"] = digitalPencode2html(loaded_data)
+ data["filename"] = is_edited ? "[open_file]*" : open_file
else
- data["filedata"] = digitalPencode2html(PRG.loaded_data)
+ data["filedata"] = digitalPencode2html(loaded_data)
data["filename"] = "UNNAMED"
ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
diff --git a/code/modules/modular_computers/file_system/programs/research/ai_restorer.dm b/code/modules/modular_computers/file_system/programs/research/ai_restorer.dm
index 37aa8a4fcd6..a304cca0318 100644
--- a/code/modules/modular_computers/file_system/programs/research/ai_restorer.dm
+++ b/code/modules/modular_computers/file_system/programs/research/ai_restorer.dm
@@ -55,7 +55,7 @@
if(href_list["PRG_addCustomSuppliedLaw"])
var/law_to_add = sanitize(input("Please enter a new law for the AI.", "Custom Law Entry"))
var/sector = input("Please enter the priority for your new law. Can only write to law sectors 15 and above.", "Law Priority (15+)") as num
- sector = between(MIN_SUPPLIED_LAW_NUMBER, sector, MAX_SUPPLIED_LAW_NUMBER)
+ sector = clamp(MIN_SUPPLIED_LAW_NUMBER, sector, MAX_SUPPLIED_LAW_NUMBER)
A.add_supplied_law(sector, law_to_add)
to_chat(A, "
Custom law uploaded to sector [sector]: [law_to_add].")
return 1
diff --git a/code/modules/modular_computers/file_system/programs/security/digitalwarrant.dm b/code/modules/modular_computers/file_system/programs/security/digitalwarrant.dm
index 98985e05ba2..875c3b8588b 100644
--- a/code/modules/modular_computers/file_system/programs/security/digitalwarrant.dm
+++ b/code/modules/modular_computers/file_system/programs/security/digitalwarrant.dm
@@ -19,28 +19,31 @@ var/global/list/all_warrants
name = "Warrant Assistant"
var/datum/computer_file/report/warrant/active
-/datum/nano_module/program/proc/get_warrants()
+/datum/nano_module/program/proc/get_warrants(list/accesses, mob/user)
var/datum/computer_network/network = program?.computer?.get_network()
if(network)
- return network.get_all_files_of_type(/datum/computer_file/report/warrant)
+ return network.get_all_files_of_type(/datum/computer_file/report/warrant, accesses, user)
-/datum/nano_module/program/proc/remove_warrant(datum/computer_file/report/warrant/W)
+/datum/nano_module/program/proc/remove_warrant(datum/computer_file/report/warrant/W, list/accesses, mob/user)
var/datum/computer_network/network = program?.computer?.get_network()
if(network)
- return network.remove_file(W)
+ return network.remove_file(W, accesses, user)
-/datum/nano_module/program/proc/save_warrant(datum/computer_file/report/warrant/W)
+/datum/nano_module/program/proc/save_warrant(datum/computer_file/report/warrant/W, list/accesses, mob/user)
var/datum/computer_network/network = program?.computer?.get_network()
if(network)
- return network.store_file(W)
+ return network.store_file(W, OS_DOCUMENTS_DIR, TRUE, accesses = accesses, user = user)
/datum/nano_module/program/digitalwarrant/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state)
var/list/data = host.initial_data()
+ var/list/accesses = get_access(user)
+
if(active)
- data["details"] = active.generate_nano_data(get_access(user), user)
+ data["details"] = active.generate_nano_data(accesses, user)
else
- for(var/datum/computer_file/report/warrant/W in global.all_warrants)
+ var/list/avail_warrants = get_warrants(accesses, user)
+ for(var/datum/computer_file/report/warrant/W in avail_warrants)
LAZYADD(data[W.get_category()], W.get_nano_summary())
ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
@@ -53,24 +56,26 @@ var/global/list/all_warrants
/datum/nano_module/program/digitalwarrant/Topic(href, href_list)
if(..())
return 1
+ var/list/accesses = get_access(usr)
+ var/list/avail_warrants = get_warrants(accesses, usr)
if(href_list["editwarrant"])
. = 1
- for(var/datum/computer_file/report/warrant/W in global.all_warrants)
+ for(var/datum/computer_file/report/warrant/W in avail_warrants)
if(W.uid == text2num(href_list["editwarrant"]))
active = W
break
if(href_list["sendtoarchive"])
. = 1
- for(var/datum/computer_file/report/warrant/W in global.all_warrants)
+ for(var/datum/computer_file/report/warrant/W in avail_warrants)
if(W.uid == text2num(href_list["sendtoarchive"]))
W.archived = TRUE
break
if(href_list["restore"])
. = 1
- for(var/datum/computer_file/report/warrant/W in global.all_warrants)
+ for(var/datum/computer_file/report/warrant/W in avail_warrants)
if(W.uid == text2num(href_list["restore"]))
W.archived = FALSE
break
@@ -89,17 +94,23 @@ var/global/list/all_warrants
if(!active)
return
broadcast_security_hud_message("[active.get_broadcast_summary()] has been [(active in global.all_warrants) ? "edited" : "uploaded"].", nano_host())
- LAZYDISTINCTADD(global.all_warrants, active)
+
+ var/success = save_warrant(active, accesses, usr)
+ if(success != OS_FILE_SUCCESS)
+ to_chat(usr, SPAN_WARNING("Could not save warrant. You may lack access to the file servers."))
+ return
active = null
if(href_list["deletewarrant"])
. = 1
if(!active)
- for(var/datum/computer_file/report/warrant/W in global.all_warrants)
+ for(var/datum/computer_file/report/warrant/W in avail_warrants)
if(W.uid == text2num(href_list["deletewarrant"]))
active = W
break
- LAZYREMOVE(global.all_warrants, active)
+ var/success = remove_warrant(active, accesses, usr)
+ if(success != OS_FILE_SUCCESS)
+ to_chat(usr, SPAN_WARNING("Could not remove warrant. You may lack access to the file servers."))
active = null
if(href_list["back"])
@@ -112,7 +123,7 @@ var/global/list/all_warrants
var/datum/report_field/F = active.field_from_ID(text2num(href_list["edit_field"]))
if(!F)
return
- if(!(F.get_perms(get_access(usr), usr) & OS_WRITE_ACCESS))
+ if(!(F.get_perms(accesses, usr) & OS_WRITE_ACCESS))
to_chat(usr, SPAN_WARNING("\The [nano_host()] flashes an \"Access Denied\" warning."))
return
F.ask_value(usr)
diff --git a/code/modules/modular_computers/file_system/reports/crew_record.dm b/code/modules/modular_computers/file_system/reports/crew_record.dm
index 9e6fd8e1874..83063cf559c 100644
--- a/code/modules/modular_computers/file_system/reports/crew_record.dm
+++ b/code/modules/modular_computers/file_system/reports/crew_record.dm
@@ -9,6 +9,8 @@ var/global/arrest_security_status = "Arrest"
/datum/computer_file/report/crew_record
filetype = "CDB"
size = 2
+ write_access = list(list(access_bridge))
+
var/icon/photo_front = null
var/icon/photo_side = null
@@ -165,7 +167,7 @@ var/global/arrest_security_status = "Arrest"
CR.load_from_mob(H)
var/datum/computer_network/network = get_local_network_at(get_turf(H))
if(network)
- network.store_file(CR, MF_ROLE_CREW_RECORDS)
+ network.store_file(CR, OS_RECORDS_DIR, TRUE, mainframe_role = MF_ROLE_CREW_RECORDS)
return CR
// Gets crew records filtered by set of positions
@@ -260,7 +262,7 @@ FIELD_SHORT("Faction", faction, access_bridge, access_bridge, FALSE, TRUE)
FIELD_LONG("Qualifications", skillset, access_bridge, access_bridge, TRUE)
// ANTAG RECORDS
-FIELD_LONG("Exploitable Information", antag_record, access_syndicate, access_syndicate, FALSE)
+FIELD_LONG("Exploitable Information", antag_record, access_hacked, access_hacked, FALSE)
//Options builderes
/datum/report_field/options/crew_record/rank/proc/record_ranks()
diff --git a/code/modules/modular_computers/file_system/reports/people.dm b/code/modules/modular_computers/file_system/reports/people.dm
index 617fab0cc21..20b1597b1ff 100644
--- a/code/modules/modular_computers/file_system/reports/people.dm
+++ b/code/modules/modular_computers/file_system/reports/people.dm
@@ -24,9 +24,9 @@
//Helper procs.
/datum/report_field/people/proc/get_used_network()
- var/obj/item/stock_parts/computer/hard_drive/holder = owner.holder
- if(holder)
- var/datum/extension/interactive/os/os = get_extension(holder.loc, /datum/extension/interactive/os)
+ var/obj/item/stock_parts/computer/hard_drive/hard_drive = owner.holder?.resolve()
+ if(!isnull(hard_drive) && isloc(hard_drive.loc))
+ var/datum/extension/interactive/os/os = get_extension(hard_drive.loc, /datum/extension/interactive/os)
return os?.get_network()
/datum/report_field/people/proc/perform_send(subject, body, attach_report)
@@ -111,7 +111,7 @@
if(in_as_list(entry, new_value))
continue //ignore repeats
new_value += list(entry)
- value = new_value
+ value = new_value
/datum/report_field/people/list_from_manifest/ask_value(mob/user)
var/alert = alert(user, "Would you like to add or remove a name?", "Form Input", "Add", "Remove")
diff --git a/code/modules/modular_computers/file_system/reports/report.dm b/code/modules/modular_computers/file_system/reports/report.dm
index 14e4a2277df..efb782c507f 100644
--- a/code/modules/modular_computers/file_system/reports/report.dm
+++ b/code/modules/modular_computers/file_system/reports/report.dm
@@ -11,7 +11,7 @@
var/list/datum/report_field/fields = list() //A list of fields the report comes with, in order that they should be displayed.
var/available_on_network = 0 //Whether this report type should show up for download.
var/logo //Can be set to a pencode logo for use with some display methods.
- var/list/searchable_fields = list() //The names of fields in the report which are searchable.
+ var/list/searchable_fields = list() //The names of fields in the report which are searchable.
/datum/computer_file/report/New()
..()
@@ -98,16 +98,16 @@ The recursive option resets access to all fields in the report as well.
fields += field
return field
-/datum/computer_file/report/clone()
- var/datum/computer_file/report/temp = ..()
- temp.title = title
- temp.form_name = form_name
- temp.creator = creator
- temp.file_time = file_time
+/datum/computer_file/report/PopulateClone(datum/computer_file/report/clone)
+ clone = ..()
+ clone.title = title
+ clone.form_name = form_name
+ clone.creator = creator
+ clone.file_time = file_time
for(var/i = 1, i <= length(fields), i++)
- var/datum/report_field/new_field = temp.fields[i]
+ var/datum/report_field/new_field = clone.fields[i]
new_field.copy_value(fields[i])
- return temp
+ return clone
/datum/computer_file/report/proc/display_name()
return "Form [form_name]: [title]"
@@ -166,8 +166,8 @@ Overriden so that read access is required to have write access
*/
/datum/computer_file/report/get_file_perms(list/accesses, mob/user)
var/perms = ..()
- if(!(perms & OS_WRITE_ACCESS))
- perms &= ~OS_READ_ACCESS
+ if(!(perms & OS_READ_ACCESS))
+ perms &= ~OS_WRITE_ACCESS
return perms
// Manually changing the permissions of a report will change *all* contained fields to match.
diff --git a/code/modules/modular_computers/file_system/reports/warrant.dm b/code/modules/modular_computers/file_system/reports/warrant.dm
index c32a22435b2..e486810042f 100644
--- a/code/modules/modular_computers/file_system/reports/warrant.dm
+++ b/code/modules/modular_computers/file_system/reports/warrant.dm
@@ -1,6 +1,9 @@
/datum/computer_file/report/warrant
title = "Warrant"
form_name = "W-104"
+
+ write_access = list(list(access_security), list(access_bridge))
+ read_access = list(list(access_security), list(access_bridge))
var/archived = FALSE
/datum/computer_file/report/warrant/New()
diff --git a/code/modules/modular_computers/hardware/_hardware.dm b/code/modules/modular_computers/hardware/_hardware.dm
index 2734bc57ce7..c830be8b70e 100644
--- a/code/modules/modular_computers/hardware/_hardware.dm
+++ b/code/modules/modular_computers/hardware/_hardware.dm
@@ -19,9 +19,17 @@
return 1
return ..()
+/obj/item/stock_parts/computer/on_install(obj/machinery/machine)
+ . = ..()
+ do_after_install(machine, TRUE)
+
+/obj/item/stock_parts/computer/on_uninstall(obj/machinery/machine, temporary)
+ do_before_uninstall(machine, TRUE)
+ . = ..()
+
// Called on multitool click, prints diagnostic information to the user.
/obj/item/stock_parts/computer/proc/diagnostics()
- return list("Hardware Integrity Test... (Corruption: [max_health ? round((max_health - health)/max_health * 100) : 0]%)")
+ return list("Hardware Integrity Test... (Corruption: [get_percent_damage()]%)")
/obj/item/stock_parts/computer/Initialize()
. = ..()
@@ -49,3 +57,7 @@
var/datum/extension/interactive/os/os = get_extension(loc, /datum/extension/interactive/os)
if(os)
os.recalc_power_usage()
+
+/obj/item/stock_parts/computer/proc/do_after_install(atom/device, loud)
+
+/obj/item/stock_parts/computer/proc/do_before_uninstall(atom/device, loud)
\ No newline at end of file
diff --git a/code/modules/modular_computers/hardware/card_slot.dm b/code/modules/modular_computers/hardware/card_slot.dm
index a9c62002de4..f4204c032b4 100644
--- a/code/modules/modular_computers/hardware/card_slot.dm
+++ b/code/modules/modular_computers/hardware/card_slot.dm
@@ -10,7 +10,7 @@
external_slot = TRUE
material = /decl/material/solid/metal/steel
- var/can_write = TRUE
+ // TODO: reimplment RFID card write access and can_write
var/can_broadcast = FALSE
var/obj/item/card/id/stored_card = null
@@ -40,7 +40,7 @@
if(check_functionality()) // Read the access, or show "RD_ERR"
var/datum/access/access_information = get_access_by_id(access_id)
var/access_type = access_information.access_type
- if(access_type == ACCESS_TYPE_NONE || access_type == ACCESS_TYPE_SYNDICATE || access_type == ACCESS_TYPE_CENTCOM) // Don't elaborate on these access types.
+ if(access_type == ACCESS_TYPE_NONE || access_type == ACCESS_TYPE_ANTAG || access_type == ACCESS_TYPE_CENTCOM) // Don't elaborate on these access types.
list_of_accesses += "UNKNOWN" // "UNKNOWN"
else
list_of_accesses += uppertext(access_information.desc)
@@ -111,7 +111,6 @@
/obj/item/stock_parts/computer/card_slot/broadcaster // read only
name = "RFID card broadcaster"
desc = "Reads and broadcasts the RFID signal of an inserted card."
- can_write = FALSE
can_broadcast = TRUE
usage_flags = PROGRAM_PDA
diff --git a/code/modules/modular_computers/hardware/charge_stick_slot.dm b/code/modules/modular_computers/hardware/charge_stick_slot.dm
index 0498b3a1d64..75016619647 100644
--- a/code/modules/modular_computers/hardware/charge_stick_slot.dm
+++ b/code/modules/modular_computers/hardware/charge_stick_slot.dm
@@ -10,8 +10,8 @@
external_slot = TRUE
material = /decl/material/solid/metal/steel
- var/can_write = TRUE
var/can_broadcast = FALSE
+ // TODO: reimplment charge stick write access and can_write
var/obj/item/charge_stick/stored_stick = null
/obj/item/stock_parts/computer/charge_stick_slot/proc/get_currency_name()
@@ -91,7 +91,6 @@
/obj/item/stock_parts/computer/charge_stick_slot/broadcaster // read only
name = "NFC charge-stick broadcaster"
desc = "Reads and broadcasts the NFC signal of an inserted charge-stick."
- can_write = FALSE
can_broadcast = TRUE
usage_flags = PROGRAM_PDA
diff --git a/code/modules/modular_computers/hardware/drive_slot.dm b/code/modules/modular_computers/hardware/drive_slot.dm
index a6d5cf7c5fa..da7c5d72987 100644
--- a/code/modules/modular_computers/hardware/drive_slot.dm
+++ b/code/modules/modular_computers/hardware/drive_slot.dm
@@ -11,6 +11,7 @@
material = /decl/material/solid/metal/steel
var/obj/item/stock_parts/computer/hard_drive/portable/stored_drive = null
+ var/mount_name
/obj/item/stock_parts/computer/drive_slot/diagnostics()
. = ..()
@@ -67,6 +68,29 @@
loc.verbs |= /obj/item/stock_parts/computer/drive_slot/proc/verb_eject_drive
return TRUE
+/obj/item/stock_parts/computer/drive_slot/do_after_install(atom/device, loud)
+ var/datum/extension/interactive/os/os = get_extension(device, /datum/extension/interactive/os)
+ if(!os)
+ return FALSE
+
+ var/datum/file_storage/new_storage = os.mount_storage(/datum/file_storage/disk/removable, "media", FALSE)
+ if(new_storage)
+ mount_name = new_storage.root_name
+ if(loud)
+ device.visible_message(SPAN_NOTICE("\The [device] pings: Mounted removable hard drive as file system with root directory '[new_storage.root_name]'."))
+ return TRUE
+ else
+ if(loud)
+ device.visible_message(SPAN_WARNING("\The [device] flashes an error: Failed to mount removable harddrive. Hard drive may be non-functional."))
+ return FALSE
+
+/obj/item/stock_parts/computer/drive_slot/do_before_uninstall(atom/device, loud)
+ var/datum/extension/interactive/os/os = get_extension(device, /datum/extension/interactive/os)
+ if(!os)
+ return FALSE
+
+ os.unmount_storage(mount_name)
+
/obj/item/stock_parts/computer/drive_slot/attackby(obj/item/stock_parts/computer/hard_drive/portable/I, mob/user)
if(!istype(I))
return
diff --git a/code/modules/modular_computers/hardware/hard_drive.dm b/code/modules/modular_computers/hardware/hard_drive.dm
index 1084a08c91e..331a1f609d7 100644
--- a/code/modules/modular_computers/hardware/hard_drive.dm
+++ b/code/modules/modular_computers/hardware/hard_drive.dm
@@ -9,7 +9,7 @@
matter = list(/decl/material/solid/fiberglass = MATTER_AMOUNT_REINFORCEMENT)
var/max_capacity = 128
var/used_capacity = 0
- var/list/stored_files = list() // List of stored files on this drive. DO NOT MODIFY DIRECTLY!
+ var/list/stored_files = list() // Dictionary of stored files on this drive, with file->weakref of directory. DO NOT MODIFY DIRECTLY!
/obj/item/stock_parts/computer/hard_drive/advanced
name = "advanced hard drive"
@@ -73,42 +73,147 @@
. += "NFS File Table Status: [stored_files.len]/999"
. += "Storage capacity: [used_capacity]/[max_capacity]GQ"
-// Use this proc to add file to the drive. Returns 1 on success and 0 on failure. Contains necessary sanity checks.
-/obj/item/stock_parts/computer/hard_drive/proc/store_file(var/datum/computer_file/F)
- if(!try_store_file(F))
- return 0
- F.holder = src
- stored_files.Add(F)
- recalculate_size()
- return 1
+/obj/item/stock_parts/computer/hard_drive/Initialize()
+ . = ..()
+ install_default_programs()
+
+/obj/item/stock_parts/computer/hard_drive/Destroy()
+ stored_files = null
+ return ..()
// Add programs that the disk will spawn with
/obj/item/stock_parts/computer/hard_drive/proc/install_default_programs()
- store_file(new/datum/computer_file/program/computerconfig(src)) // Computer configuration utility, allows hardware control and displays more info than status bar
- store_file(new/datum/computer_file/program/appdownloader(src)) // Downloader Utility, allows users to download more software from loca repositories
- store_file(new/datum/computer_file/program/filemanager(src)) // File manager, allows text editor functions and basic file manipulation.
+ var/datum/computer_file/directory/program_directory = add_directory(OS_PROGRAMS_DIR)
+ program_directory.unrenamable = TRUE // Protect the program directory from renaming/deletion.
+ program_directory.undeletable = TRUE
+ store_file(new/datum/computer_file/program/computerconfig(src), program_directory) // Computer configuration utility, allows hardware control and displays more info than status bar
+ store_file(new/datum/computer_file/program/appdownloader(src), program_directory) // Downloader Utility, allows users to download more software from loca repositories
+ store_file(new/datum/computer_file/program/filemanager(src), program_directory) // File manager, allows text editor functions and basic file manipulation.
+ return program_directory
+
+// Use this proc to add file to the drive. Returns OS_FILE_SUCCESS on success and error codes on failure. Contains necessary sanity checks.
+/obj/item/stock_parts/computer/hard_drive/proc/store_file(var/datum/computer_file/F, var/directory, var/create_directories = FALSE, var/list/accesses, var/mob/user, var/overwrite = TRUE)
+ var/datum/computer_file/directory/target = parse_directory(directory, create_directories)
+ if(!istype(target) && directory) // The directory could not be parsed or created
+ return target
+ var/store_file = try_store_file(F, target)
+ if(store_file != OS_FILE_SUCCESS)
+ if(store_file == OS_FILE_EXISTS)
+ if(!overwrite)
+ return store_file
+ // Remove the duplicate file.
+ var/datum/computer_file/old = find_file_by_name(F.filename, target)
+ if(!istype(old)) // Something went wrong since we already found this file earlier.
+ return OS_HARDDRIVE_ERROR
+ var/removed = remove_file(old, accesses, user)
+ if(removed != OS_FILE_SUCCESS)
+ return removed // Return the error code from removing the file.
-// Use this proc to remove file from the drive. Returns 1 on success and 0 on failure. Contains necessary sanity checks.
-/obj/item/stock_parts/computer/hard_drive/proc/remove_file(var/datum/computer_file/F, list/accesses, mob/user)
- if(!F || !istype(F))
- return 0
+ // If we've reached this point, we are able to store the file, since we successfully removed the old version.
+ // try_store_file() is intentionally written so that existing file checks are returned only if all other checks pass.
+ else
+ return store_file
+ if(istype(F, /datum/computer_file/directory))
+ var/datum/computer_file/directory/dir = F
+ for(var/datum/computer_file/child in dir.get_held_files())
+ stored_files[child] = dir
+ child.holder = weakref(src)
+
+ dir.temp_file_refs.Cut() // No longer need these references to the directory's stored files.
+
+ F.holder = weakref(src)
+ if(target)
+ if(target.inherit_perms) // Add the permissions of the directory to the file's.
+ if(LAZYLEN(target.read_access))
+ LAZYDISTINCTADD(F.read_access, target.read_access)
+ if(LAZYLEN(target.write_access))
+ LAZYDISTINCTADD(F.write_access, target.write_access)
+ if(LAZYLEN(target.mod_access))
+ LAZYDISTINCTADD(F.mod_access, target.mod_access)
+ stored_files[F] = target
+ target.held_files += weakref(F)
+ else
+ stored_files += F
+ recalculate_size()
+ return OS_FILE_SUCCESS
+
+// Use this proc to remove file from the drive. Returns OS_FILE_SUCCESS on success and error codes on failure. Contains necessary sanity checks.
+/obj/item/stock_parts/computer/hard_drive/proc/remove_file(var/datum/computer_file/F, list/accesses, mob/user, forced)
+ if(!istype(F) || !(F in stored_files))
+ return OS_FILE_NOT_FOUND
if(!stored_files)
- return 0
+ return OS_HARDDRIVE_ERROR
- if(!check_functionality())
- return 0
-
- if(!(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
- return 0
- if(F in stored_files)
- stored_files -= F
- F.holder = null
- recalculate_size()
- return 1
+ if(!forced)
+ if(!check_functionality())
+ return OS_HARDDRIVE_ERROR
+
+ if(F.undeletable)
+ return OS_FILE_NO_WRITE
+
+ if(!(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
+ return OS_FILE_NO_WRITE
+
+ var/list/removed_files = list(F)
+
+ if(istype(F, /datum/computer_file/directory))
+ var/datum/computer_file/directory/dir = F
+ var/list/dir_files = dir.get_held_files()
+ if(!forced)
+ if(!(dir.get_held_perms(accesses, user) & OS_WRITE_ACCESS))
+ return OS_FILE_NO_WRITE
+ for(var/datum/computer_file/child in dir_files)
+ if(child.undeletable)
+ return OS_FILE_NO_WRITE
+ removed_files |= dir_files
+
+ // Store references to the removed files temporarily to prevent them being GC'd, in case we're
+ // transferring this directory elsewhere.
+ dir.temp_file_refs += dir_files
+
+ for(var/datum/computer_file/removed in removed_files)
+ var/datum/computer_file/directory/dir = stored_files[removed]
+
+ // File is being removed without the directory, they're going their seperate ways.
+ if(dir && !(dir in removed_files))
+ dir.held_files -= weakref(removed)
+ dir.temp_file_refs -= removed
+ stored_files -= removed
+ removed.holder = null
+ recalculate_size()
+ return OS_FILE_SUCCESS
+
+// Saves a file, either overwriting the data of a previous file or saving a new one.
+/obj/item/stock_parts/computer/hard_drive/proc/save_file(filename, directory, new_data, list/metadata, list/accesses, mob/user, file_type = /datum/computer_file/data)
+ var/datum/computer_file/F = find_file_by_name(filename, directory)
+
+ if(istype(F) && !(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
+ return OS_FILE_NO_WRITE
+ //Try to save file, possibly won't fit size-wise
+ var/datum/computer_file/backup
+ if(istype(F))
+ backup = F.Clone()
+ remove_file(F)
else
- return 0
+ F = new file_type()
+ F.filename = filename
+ if(istype(F, /datum/computer_file/data))
+ var/datum/computer_file/data/D = F
+ D.stored_data = new_data
+ D.calculate_size()
+ F.metadata = metadata && metadata.Copy()
+
+ var/success = store_file(F, directory, FALSE, accesses, user)
+ if(success != OS_FILE_SUCCESS)
+ if(backup)
+ store_file(backup, directory)
+ return success
+
+ if(backup)
+ qdel(backup)
+ return F
// Loops through all stored files and recalculates used_capacity of this drive
/obj/item/stock_parts/computer/hard_drive/proc/recalculate_size()
@@ -123,66 +228,146 @@
// In the unlikely event someone manages to create that many files.
// BYOND is acting weird with numbers above 999 in loops (infinite loop prevention)
if(stored_files.len >= 999)
- return 0
+ return FALSE
if(used_capacity + size > max_capacity)
- return 0
+ return FALSE
else
- return 1
+ return TRUE
// Checks whether we can store the file. We can only store unique files, so this checks whether we wouldn't get a duplicity by adding a file.
-/obj/item/stock_parts/computer/hard_drive/proc/try_store_file(var/datum/computer_file/F)
- if(!F || !istype(F))
- return 0
- if(!can_store_file(F.size))
- return 0
+// Storing a file in a directory requires write access to that directory.
+/obj/item/stock_parts/computer/hard_drive/proc/try_store_file(var/datum/computer_file/F, var/directory, var/list/accesses, var/mob/user)
+ if(!istype(F))
+ return OS_FILE_NOT_FOUND
+
+ var/file_size = F.size
+ if(istype(F, /datum/computer_file/directory))
+ var/datum/computer_file/directory/dir = F
+ file_size += dir.get_held_size()
+
+ if(!can_store_file(file_size))
+ return OS_HARDDRIVE_SPACE
if(!check_functionality())
- return 0
+ return OS_HARDDRIVE_ERROR
if(!stored_files)
- return 0
+ return OS_HARDDRIVE_ERROR
+
+ // Safety check
+ F.filename = sanitize_for_file(F.filename)
+
+ var/datum/computer_file/directory/D = parse_directory(directory)
+ if(directory && !D)
+ return OS_DIR_NOT_FOUND
+ if(D && !(D.get_held_perms(accesses, user) & OS_WRITE_ACCESS))
+ return OS_FILE_NO_WRITE
- var/list/badchars = list("/","\\",":","*","?","\"","<",">","|","#", ".")
- for(var/char in badchars)
- if(findtext(F.filename, char))
- return 0
+ // We intentionally check these two cases last so that if we are overwriting the file, we are sure
+ // to be able to store it once we remove the old version.
// This file is already stored. Don't store it again.
if(F in stored_files)
- return 0
+ return OS_FILE_EXISTS
+ // Fail on finding a file with a duplicate name.
+ if(!isnum(find_file_by_name(F.filename, D, TRUE)))
+ return OS_FILE_EXISTS
+ return OS_FILE_SUCCESS
- var/name = F.filename + "." + F.filetype
- for(var/datum/computer_file/file in stored_files)
- if((file.filename + "." + file.filetype) == name)
- return 0
- return 1
-
-// Tries to find the file by filename. Returns null on failure
-/obj/item/stock_parts/computer/hard_drive/proc/find_file_by_name(var/filename)
- if(!check_functionality())
- return null
+// Tries to find file by filename. Returns error code on failure
+/obj/item/stock_parts/computer/hard_drive/proc/find_file_by_name(var/filename, var/directory, var/forced)
+ if(!forced && !check_functionality())
+ return OS_HARDDRIVE_ERROR
if(!filename)
- return null
+ return OS_FILE_NOT_FOUND
if(!stored_files)
- return null
+ return OS_HARDDRIVE_ERROR
- for(var/datum/computer_file/F in stored_files)
- if(F.filename == filename)
- return F
- return null
+ var/datum/computer_file/directory/target = parse_directory(directory)
-/obj/item/stock_parts/computer/hard_drive/Destroy()
- stored_files = null
- return ..()
+ if(istype(target))
+ var/list/held_files = target.get_held_files()
+ for(var/datum/computer_file/file in held_files)
+ // Filename uniqueness is enforced even if filetype is not the same, so allow
+ // users to access files either by the filename alone or with the filetype.
+ if(file.filename == filename || (file.filename + "." + file.filetype) == filename)
+ return file
+ else
+ if(directory)
+ return target
+ // Check in files not in a directory.
+ for(var/datum/computer_file/file in stored_files)
+ if(stored_files[file] != null) // Ignore files in a directory.
+ continue
+ if(file.filename == filename || (file.filename + "." + file.filetype) == filename)
+ return file
+ return OS_FILE_NOT_FOUND
-/obj/item/stock_parts/computer/hard_drive/Initialize()
- . = ..()
- install_default_programs()
+/obj/item/stock_parts/computer/hard_drive/proc/add_directory(var/directory, var/check_for_existing = TRUE)
+ if(check_for_existing)
+ var/datum/computer_file/directory/existing = parse_directory(directory)
+ if(istype(existing))
+ return existing
+
+ var/list/directories = splittext(directory, "/")
+ if(!length(directories))
+ return OS_DIR_NOT_FOUND
+ var/new_directory = sanitize_for_file(directories[directories.len])
+ if(!length(new_directory))
+ return OS_BAD_NAME
+ directories.Cut(directories.len) // Remove the final directory.
+
+ var/datum/computer_file/directory/new_dir = new()
+ new_dir.filename = new_directory
+ if(!length(directories))
+ var/success = store_file(new_dir)
+ if(success != OS_FILE_SUCCESS)
+ return success
+ return new_dir
+ else // Add directories until the final one is added.
+ var/datum/computer_file/directory/parent_dir = add_directory(jointext(directories, "/"))
+ var/success = store_file(new_dir, parent_dir)
+ if(success != OS_FILE_SUCCESS)
+ return success
+ return new_dir
+
+/obj/item/stock_parts/computer/hard_drive/proc/parse_directory(var/directory, var/create_directories = FALSE)
+ if(istype(directory, /datum/computer_file/directory))
+ if(directory in stored_files)
+ return directory
+ return OS_DIR_NOT_FOUND
+ if(istext(directory))
+ var/list/directories = splittext(directory, "/")
+ var/datum/computer_file/directory/current_dir
+ if(!length(directories))
+ return OS_DIR_NOT_FOUND
+ directory_loop:
+ for(var/directory_name in directories)
+ if(directory_name == "..")
+ directories.Cut(1, 2)
+ if(!current_dir)
+ return OS_DIR_NOT_FOUND
+ current_dir = current_dir.get_directory()
+ continue
+ var/list/file_list = current_dir ? current_dir.get_held_files() : stored_files
+ for(var/datum/computer_file/directory/D in file_list)
+ if(D.filename == directory_name)
+ directories.Cut(1, 2)
+ current_dir = D
+ . = D
+ continue directory_loop
+ // Found a missing directory.
+ if(create_directories)
+ var/final_path = current_dir ? current_dir.get_file_path() + "/" : ""
+ final_path += jointext(directories, "/")
+ return add_directory(final_path)
+ else
+ return OS_DIR_NOT_FOUND
// Preset for borgs and AIs
/obj/item/stock_parts/computer/hard_drive/silicon/install_default_programs()
- ..()
- store_file(new/datum/computer_file/program/records())
- store_file(new/datum/computer_file/program/crew_manifest())
- store_file(new/datum/computer_file/program/email_client())
- store_file(new/datum/computer_file/program/suit_sensors())
\ No newline at end of file
+ var/datum/computer_file/directory/program_directory = ..()
+ store_file(new/datum/computer_file/program/records(), program_directory)
+ store_file(new/datum/computer_file/program/crew_manifest(), program_directory)
+ store_file(new/datum/computer_file/program/email_client(), program_directory)
+ store_file(new/datum/computer_file/program/suit_sensors(), program_directory)
\ No newline at end of file
diff --git a/code/modules/modular_computers/hardware/lan_port.dm b/code/modules/modular_computers/hardware/lan_port.dm
index 646c17d8566..a50d49a0c08 100644
--- a/code/modules/modular_computers/hardware/lan_port.dm
+++ b/code/modules/modular_computers/hardware/lan_port.dm
@@ -15,8 +15,8 @@
set_terminal()
/obj/item/stock_parts/computer/lan_port/Destroy()
- . = ..()
QDEL_NULL(terminal)
+ return ..()
/obj/item/stock_parts/computer/lan_port/on_uninstall(obj/machinery/machine, temporary)
. = ..()
@@ -76,7 +76,7 @@
if(!C.can_use(5))
to_chat(user, SPAN_WARNING("You need five lengths of network cable for \the [parent]."))
return TRUE
-
+
user.visible_message(SPAN_NOTICE("\The [user] adds cables to the \the [parent]."), "You start adding cables to \the [parent] frame...")
if(do_after(user, 20, parent))
if(!terminal && (loc == parent) && parent.components_are_accessible(type) && !check_terminal_block(T) && C.use(5))
@@ -95,4 +95,3 @@
to_chat(user, SPAN_NOTICE("You cut the cables and dismantle the network terminal."))
qdel(terminal)
return TRUE
-
\ No newline at end of file
diff --git a/code/modules/modular_computers/hardware/network_card.dm b/code/modules/modular_computers/hardware/network_card.dm
index 003587e2f6f..a2014e4b061 100644
--- a/code/modules/modular_computers/hardware/network_card.dm
+++ b/code/modules/modular_computers/hardware/network_card.dm
@@ -10,7 +10,7 @@
var/long_range = 0
var/ethernet = 0 // Hard-wired, therefore always on, ignores wireless checks.
- var/proxy_id // If set, uses the value to funnel connections through another network card.
+ // TODO: Reimplement proxy_id
/obj/item/stock_parts/computer/network_card/diagnostics()
. = ..()
diff --git a/code/modules/modular_computers/hardware/scanners/scanner.dm b/code/modules/modular_computers/hardware/scanners/scanner.dm
index 4ee6a6cae25..cdb1222ab75 100644
--- a/code/modules/modular_computers/hardware/scanners/scanner.dm
+++ b/code/modules/modular_computers/hardware/scanners/scanner.dm
@@ -19,28 +19,32 @@
do_before_uninstall()
. = ..()
-/obj/item/stock_parts/computer/scanner/proc/do_after_install(user, atom/device)
+/obj/item/stock_parts/computer/scanner/do_after_install(atom/device, loud)
var/datum/extension/interactive/os/os = get_extension(device, /datum/extension/interactive/os)
if(!driver_type || !device || !os)
return 0
if(!os.has_component(PART_HDD))
- to_chat(user, "Driver installation for \the [src] failed: \the [device] lacks a hard drive.")
+ if(loud)
+ device.visible_message(SPAN_WARNING("\The [device] flashes an error: Driver installation for \the [src] failed. Could not locate hard drive."))
return 0
- var/datum/computer_file/program/scanner/old_driver = os.get_file(initial(driver_type.filename))
+ var/datum/computer_file/program/scanner/old_driver = os.get_file(initial(driver_type.filename), OS_PROGRAMS_DIR)
if(istype(old_driver))
- to_chat(user, "Drivers found on \the [device]; \the [src] has been installed.")
+ if(loud)
+ device.visible_message(SPAN_NOTICE("\The [device] pings: Drivers located for \the [src]. Installation complete."))
old_driver.connect_scanner()
return 1
var/datum/computer_file/program/scanner/driver_file = new driver_type
- if(!os.store_file(driver_file))
- to_chat(user, "Driver installation for \the [src] failed: file could not be written to the hard drive.")
+ if(!os.store_file(driver_file, OS_PROGRAMS_DIR, create_directories = TRUE))
+ if(loud)
+ device.visible_message(SPAN_WARNING("\The [device] flashes an error: Driver installation for \the [src] failed. Could not write to the hard drive."))
return 0
- to_chat(user, "Driver software for \the [src] has been installed on \the [device].")
driver_file.computer = os
driver_file.connect_scanner()
+ if(loud)
+ device.visible_message(SPAN_NOTICE("\The [device] pings: Driver installation for \the [src] complete."))
return 1
-/obj/item/stock_parts/computer/scanner/proc/do_before_uninstall()
+/obj/item/stock_parts/computer/scanner/do_before_uninstall(atom/device, loud)
if(driver)
driver.disconnect_scanner()
if(driver) //In case the driver doesn't find it.
diff --git a/code/modules/modular_computers/networking/accounts/_network_accounts.dm b/code/modules/modular_computers/networking/accounts/_network_accounts.dm
index 25d17ec1833..9ef74773dd8 100644
--- a/code/modules/modular_computers/networking/accounts/_network_accounts.dm
+++ b/code/modules/modular_computers/networking/accounts/_network_accounts.dm
@@ -30,7 +30,7 @@
/datum/computer_network/proc/add_account(datum/computer_file/data/account/acc, accesses)
for(var/datum/extension/network_device/mainframe/M in get_mainframes_by_role(MF_ROLE_ACCOUNT_SERVER, accesses))
- if(M.store_file(acc))
+ if(M.store_file(acc, OS_ACCOUNTS_DIR, TRUE))
return TRUE
/datum/computer_network/proc/find_account_by_login(login, accesses)
diff --git a/code/modules/modular_computers/networking/accounts/account.dm b/code/modules/modular_computers/networking/accounts/account.dm
index e87ce71897d..91aa2dfbe47 100644
--- a/code/modules/modular_computers/networking/accounts/account.dm
+++ b/code/modules/modular_computers/networking/accounts/account.dm
@@ -7,7 +7,7 @@
var/suspended = FALSE // Whether the account is banned by the SA.
var/list/logged_in_os = list() // OS which are currently logged into this account. Used for e-mail notifications, currently.
- var/list/groups = list() // Groups which this account is a member of.
+ var/list/groups = list() // Groups which this account is a member of.
var/list/parent_groups = list() // Parent groups with a child/children which this account is a member of.
var/fullname = "N/A"
@@ -53,7 +53,7 @@
logged_in_os.Cut()
groups.Cut()
parent_groups.Cut()
-
+
QDEL_NULL_LIST(inbox)
QDEL_NULL_LIST(outbox)
QDEL_NULL_LIST(spam)
@@ -68,23 +68,23 @@
/datum/computer_file/data/account/proc/receive_mail(var/datum/computer_file/data/email_message/received_message, var/datum/computer_network/network)
-/datum/computer_file/data/account/clone()
- var/datum/computer_file/data/account/copy = ..(TRUE) // We always rename the file since a copied account is always a backup.
- copy.backup = TRUE
-
- copy.login = login
- copy.password = password
- copy.can_login = can_login
- copy.suspended = suspended
-
- copy.groups = groups.Copy()
- copy.parent_groups = parent_groups.Copy()
+/datum/computer_file/data/account/Clone(rename)
+ . = ..(TRUE) // We always rename the file since a copied account is always a backup.
- copy.fullname = fullname
+/datum/computer_file/data/account/PopulateClone(datum/computer_file/data/account/clone)
+ clone = ..()
+ clone.backup = TRUE
+ clone.login = login
+ clone.password = password
+ clone.can_login = can_login
+ clone.suspended = suspended
+ clone.groups = groups.Copy()
+ clone.parent_groups = parent_groups.Copy()
+ clone.fullname = fullname
// TODO: Don't backup e-mails for now - they are themselves other files which makes this complicated. In the future
// accounts will point to e-mails stored seperately on a server.
- return copy
+ return clone
// Address namespace (@internal-services.net) for email addresses with special purpose only!.
/datum/computer_file/data/account/service
diff --git a/code/modules/modular_computers/networking/device_types/_network_device.dm b/code/modules/modular_computers/networking/device_types/_network_device.dm
index de6066e9339..fcb15bcf959 100644
--- a/code/modules/modular_computers/networking/device_types/_network_device.dm
+++ b/code/modules/modular_computers/networking/device_types/_network_device.dm
@@ -68,7 +68,7 @@
if(!net)
// We should already be queued for reconnect if it went down, so do nothing.
return FALSE
- if(!net.devices_by_tag[network_tag] != src)
+ if(net.devices_by_tag[network_tag] != src)
// The connection has failed but the network is still up, so we try to reconnect.
if(!connect())
return FALSE
@@ -450,6 +450,10 @@
alias = replacetext(alias, " ", "_")
LAZYSET(command_and_write, alias, pub)
+/**Returns the outward facing URI for this network device.*/
+/datum/extension/network_device/proc/get_network_URI()
+ return "[network_tag].[network_id]"
+
//Subtype for passive devices, doesn't init until asked for
/datum/extension/network_device/lazy
base_type = /datum/extension/network_device
diff --git a/code/modules/modular_computers/networking/device_types/mainframe.dm b/code/modules/modular_computers/networking/device_types/mainframe.dm
index 8693ee89dc5..465b001c5dc 100644
--- a/code/modules/modular_computers/networking/device_types/mainframe.dm
+++ b/code/modules/modular_computers/networking/device_types/mainframe.dm
@@ -31,64 +31,55 @@ var/global/list/all_mainframe_roles = list(
if(istype(M))
return M.get_component_of_type(/obj/item/stock_parts/computer/hard_drive)
+// File storage procs
/datum/extension/network_device/mainframe/proc/get_all_files()
var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
if(HDD)
return HDD.stored_files
-/datum/extension/network_device/mainframe/proc/get_file(filename)
+/datum/extension/network_device/mainframe/proc/get_file(filename, directory)
var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
if(HDD)
- return HDD.find_file_by_name(filename)
+ return HDD.find_file_by_name(filename, directory)
-/datum/extension/network_device/mainframe/proc/delete_file(filename, list/accesses, mob/user)
+/datum/extension/network_device/mainframe/proc/delete_file(datum/computer_file/F, list/accesses, mob/user)
var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
if(HDD)
- var/datum/computer_file/data/F = HDD.find_file_by_name(filename)
- if(!F || F.undeletable)
- return FALSE
return HDD.remove_file(F, accesses, user)
-/datum/extension/network_device/mainframe/proc/store_file(datum/computer_file/file)
+/datum/extension/network_device/mainframe/proc/store_file(datum/computer_file/file, directory, create_directories, list/accesses, mob/user, overwrite = TRUE)
var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
if(!HDD)
- return FALSE
- var/datum/computer_file/data/old_version = HDD.find_file_by_name(file.filename)
- if(old_version)
- HDD.remove_file(old_version)
- if(!HDD.store_file(file))
- HDD.store_file(old_version)
- return FALSE
- else
- return TRUE
+ return OS_HARDDRIVE_ERROR
+
+ return HDD.store_file(file, directory, create_directories, accesses, user, overwrite)
-/datum/extension/network_device/mainframe/proc/save_file(newname, new_data)
+/datum/extension/network_device/mainframe/proc/try_store_file(datum/computer_file/file, directory, list/accesses, mob/user)
var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
if(!HDD)
- return FALSE
+ return OS_HARDDRIVE_ERROR
+
+ return HDD.try_store_file(file, directory, accesses, user)
- var/datum/computer_file/data/F = HDD.find_file_by_name(newname)
- //Try to save file, possibly won't fit size-wise
- var/datum/computer_file/data/backup
- if(F)
- backup = F.clone()
- HDD.remove_file(F)
- else
- F = new()
- F.stored_data = new_data
- F.calculate_size()
- if(!HDD.store_file(F))
- if(backup)
- HDD.store_file(backup)
- return FALSE
- return TRUE
+/datum/extension/network_device/mainframe/proc/save_file(newname, directory, new_data, list/metadata, list/accesses, mob/user)
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ return HDD.save_file(newname, directory, new_data, metadata, accesses, user)
-/datum/extension/network_device/mainframe/proc/append_to_file(filename, data)
+/datum/extension/network_device/mainframe/proc/parse_directory(directory_path, create_directories)
var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
if(!HDD)
- return FALSE
- var/datum/computer_file/data/logfile/F = get_file(filename)
- if(F)
+ return OS_HARDDRIVE_ERROR
+
+ return HDD.parse_directory(directory_path, create_directories)
+
+/datum/extension/network_device/mainframe/proc/append_to_file(filename, directory, data)
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ var/datum/computer_file/data/logfile/F = get_file(filename, directory)
+ if(istype(F))
var/list/logs = splittext(F.stored_data, "\[br\]")
logs.Add(data)
if(length(logs) > max_log_count)
@@ -99,8 +90,7 @@ var/global/list/all_mainframe_roles = list(
F = new()
F.filename = filename
F.stored_data = data
- store_file(F)
- return TRUE
+ return store_file(F, directory)
/datum/extension/network_device/mainframe/proc/get_capacity()
var/obj/item/stock_parts/computer/hard_drive/HDD = get_storage()
@@ -121,9 +111,9 @@ var/global/list/all_mainframe_roles = list(
for(var/F in subtypesof(/datum/computer_file/report))
var/datum/computer_file/report/type = F
if(initial(type.available_on_network))
- store_file(new type)
+ store_file(new type, "reports", TRUE)
for(var/F in subtypesof(/datum/computer_file/program))
var/datum/computer_file/program/type = F
if(initial(type.available_on_network))
- store_file(new type)
\ No newline at end of file
+ store_file(new type, OS_PROGRAMS_DIR, TRUE)
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/device_types/stock_part.dm b/code/modules/modular_computers/networking/device_types/stock_part.dm
index 452b359ef78..189560b6556 100644
--- a/code/modules/modular_computers/networking/device_types/stock_part.dm
+++ b/code/modules/modular_computers/networking/device_types/stock_part.dm
@@ -1,18 +1,7 @@
// Attached to a stock part for issuing commands to machinery via networks.
/datum/extension/network_device/stock_part
has_commands = TRUE
- internet_allowed = TRUE // GOOSE devices, network locks, etc. can all connect over PLEXUS
-
-// Network receivers check the access of the parent machine, and do not check for network administrator access.
-/datum/extension/network_device/stock_part/has_access(mob/user)
- var/datum/computer_network/network = get_network()
- if(!network)
- return TRUE // If not on network, always TRUE for access, as there isn't anything to access.
- if(!user)
- return FALSE
- var/atom/H = holder
- var/obj/M = H.loc
- return M.check_access(user)
+ internet_allowed = TRUE // GOOSE devices, network locks, etc. can all connect over PLEXUS
/datum/extension/network_device/stock_part/get_wired_connection()
var/atom/H = holder
diff --git a/code/modules/modular_computers/networking/emails/_email.dm b/code/modules/modular_computers/networking/emails/_email.dm
index a0f6ef82822..87872ea6cae 100644
--- a/code/modules/modular_computers/networking/emails/_email.dm
+++ b/code/modules/modular_computers/networking/emails/_email.dm
@@ -25,7 +25,7 @@
if(!(recipient in get_accounts_unsorted()))
return FALSE
- var/datum/computer_file/data/email_message/received_copy = received.clone()
+ var/datum/computer_file/data/email_message/received_copy = received.Clone()
received_copy.set_timestamp()
recipient.inbox.Add(received_copy)
@@ -40,6 +40,6 @@
for(var/datum/computer_file/data/account/email_account in get_accounts_unsorted())
if(email_account.broadcaster)
continue
- var/datum/computer_file/data/email_message/new_message = received.clone()
+ var/datum/computer_file/data/email_message/new_message = received.Clone()
send_email(recipient, "[email_account.login]@[network_id]", new_message)
return TRUE
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/emails/email_message.dm b/code/modules/modular_computers/networking/emails/email_message.dm
index 6901695b145..ef3ee45d41e 100644
--- a/code/modules/modular_computers/networking/emails/email_message.dm
+++ b/code/modules/modular_computers/networking/emails/email_message.dm
@@ -11,15 +11,14 @@
. = ..()
QDEL_NULL(attachment)
-/datum/computer_file/data/email_message/clone()
- var/datum/computer_file/data/email_message/temp = ..()
- temp.title = title
- temp.source = source
- temp.spam = spam
- temp.timestamp = timestamp
- if(attachment)
- temp.attachment = attachment.clone()
- return temp
+/datum/computer_file/data/email_message/PopulateClone(datum/computer_file/data/email_message/clone)
+ clone = ..()
+ clone.title = title
+ clone.source = source
+ clone.spam = spam
+ clone.timestamp = timestamp
+ clone.attachment = attachment?.Clone()
+ return clone
// Turns /email_message/ file into regular /data/ file.
/datum/computer_file/data/email_message/proc/export()
diff --git a/code/modules/modular_computers/networking/machinery/_network_machine.dm b/code/modules/modular_computers/networking/machinery/_network_machine.dm
index 84dd6841270..c1035851060 100644
--- a/code/modules/modular_computers/networking/machinery/_network_machine.dm
+++ b/code/modules/modular_computers/networking/machinery/_network_machine.dm
@@ -17,8 +17,18 @@
var/heat_threshold = 90 CELSIUS // At what temperature the machine will lock up.
var/overheated = FALSE
+ var/tmp/tag_network_tag //The name of this device on the network set by mapper
+
+/obj/machinery/network/modify_mapped_vars(map_hash)
+ ..()
+ ADJUST_TAG_VAR(initial_network_id, map_hash)
+ ADJUST_TAG_VAR(initial_network_key, map_hash)
+ ADJUST_TAG_VAR(tag_network_tag, map_hash)
+
/obj/machinery/network/Initialize()
- set_extension(src, network_device_type, initial_network_id, initial_network_key, RECEIVER_STRONG_WIRELESS)
+ var/datum/extension/network_device/ND = get_or_create_extension(src, network_device_type, initial_network_id, initial_network_key, RECEIVER_STRONG_WIRELESS)
+ if(length(tag_network_tag))
+ ND.set_network_tag(tag_network_tag)
. = ..()
/obj/machinery/network/populate_parts(full_populate)
diff --git a/code/modules/modular_computers/networking/machinery/wall_router.dm b/code/modules/modular_computers/networking/machinery/wall_router.dm
index 29fac836387..390d830bab7 100644
--- a/code/modules/modular_computers/networking/machinery/wall_router.dm
+++ b/code/modules/modular_computers/networking/machinery/wall_router.dm
@@ -7,24 +7,8 @@
density = 0
obj_flags = OBJ_FLAG_MOVES_UNSUPPORTED
base_type = /obj/machinery/network/router/wall_mounted
+ directional_offset = "{'NORTH':{'y':-21}, 'SOUTH':{'y':21}, 'EAST':{'x':-21}, 'WEST':{'x':21}}"
/obj/machinery/network/router/wall_mounted/Initialize()
. = ..()
queue_icon_update()
-
-/obj/machinery/network/router/wall_mounted/on_update_icon()
- . = ..()
- // Set pixel offsets
- pixel_x = 0
- pixel_y = 0
- var/turf/T = get_step(get_turf(src), turn(dir, 180))
- if(istype(T) && T.density)
- if(dir == NORTH)
- default_pixel_y = -21
- else if(dir == SOUTH)
- default_pixel_y = 21
- else if(dir == WEST)
- default_pixel_x = 21
- else if(dir == EAST)
- default_pixel_x = -21
- reset_offsets(0)
\ No newline at end of file
diff --git a/code/modules/modular_computers/networking/network_files.dm b/code/modules/modular_computers/networking/network_files.dm
index d712e344d8e..66d6e21fc12 100644
--- a/code/modules/modular_computers/networking/network_files.dm
+++ b/code/modules/modular_computers/networking/network_files.dm
@@ -1,6 +1,6 @@
-/datum/computer_network/proc/find_file_by_name(filename, mainframe_role = MF_ROLE_FILESERVER, list/accesses)
+/datum/computer_network/proc/find_file_by_name(filename, directory, mainframe_role = MF_ROLE_FILESERVER, list/accesses)
for(var/datum/extension/network_device/mainframe/M in get_mainframes_by_role(mainframe_role, accesses))
- var/datum/computer_file/F = M.get_file(filename)
+ var/datum/computer_file/F = M.get_file(filename, directory)
if(F)
return F
@@ -15,20 +15,20 @@
. |= F
found_filenames |= F.filename
-/datum/computer_network/proc/store_file(datum/computer_file/F, mainframe_role = MF_ROLE_FILESERVER, list/accesses)
+/datum/computer_network/proc/store_file(datum/computer_file/file, directory, create_directories, list/accesses, mob/user, overwrite = TRUE, mainframe_role = MF_ROLE_FILESERVER)
for(var/datum/extension/network_device/mainframe/M in get_mainframes_by_role(mainframe_role, accesses))
- if(M.store_file(F))
+ if(M.store_file(file, directory, create_directories, accesses, user, overwrite))
return TRUE
-/datum/computer_network/proc/remove_file(datum/computer_file/F, mainframe_role = MF_ROLE_FILESERVER, list/accesses)
+// We don't pass the directory since this is generally used in conjunction with get_all_files_of_type
+/datum/computer_network/proc/remove_file(datum/computer_file/F, list/accesses, mob/user, mainframe_role = MF_ROLE_FILESERVER)
for(var/datum/extension/network_device/mainframe/M in get_mainframes_by_role(mainframe_role, accesses))
- if(M.delete_file(F))
+ if(M.delete_file(F, accesses, user))
return TRUE
-/datum/computer_network/proc/find_file_location(filename, mainframe_role = MF_ROLE_FILESERVER, list/accesses)
+/datum/computer_network/proc/find_file_location(datum/computer_file/F, list/accesses, mainframe_role = MF_ROLE_FILESERVER)
for(var/datum/extension/network_device/mainframe/M in get_mainframes_by_role(mainframe_role, accesses))
- var/datum/computer_file/F = M.get_file(filename)
- if(F)
+ if(F in M.get_all_files())
return M.network_tag
// Reports
@@ -60,13 +60,13 @@
var/entry = "[stationtime2text()] - [source ? source : "*SYSTEM*" ] - "
entry += data
for(var/datum/extension/network_device/mainframe/M in mainframes_by_role[MF_ROLE_LOG_SERVER])
- if(M.append_to_file("network_log", entry))
+ if(M.append_to_file("network_log", OS_LOGS_DIR, entry))
return TRUE
/datum/computer_network/proc/get_log_files()
. = list()
for(var/datum/extension/network_device/mainframe/M in mainframes_by_role[MF_ROLE_LOG_SERVER])
- var/logfile = M.get_file("network_log")
+ var/logfile = M.get_file("network_log", OS_LOGS_DIR)
if(logfile)
. += logfile
diff --git a/code/modules/modular_computers/networking/network_helper.dm b/code/modules/modular_computers/networking/network_helper.dm
new file mode 100644
index 00000000000..b37de813198
--- /dev/null
+++ b/code/modules/modular_computers/networking/network_helper.dm
@@ -0,0 +1,5 @@
+/proc/parse_network_uri(var/uri)
+ var/found = findtext(uri, ".")
+ if(!found)
+ return
+ return list("network_tag" = copytext(uri, 1, found), "network_id" = copytext(uri, found + 1))
diff --git a/code/modules/modular_computers/os/os.dm b/code/modules/modular_computers/os/_os.dm
similarity index 95%
rename from code/modules/modular_computers/os/os.dm
rename to code/modules/modular_computers/os/_os.dm
index 2367b5cc2f8..c4ad2b1c037 100644
--- a/code/modules/modular_computers/os/os.dm
+++ b/code/modules/modular_computers/os/_os.dm
@@ -14,6 +14,7 @@
var/screen_icon_file // dmi where the screen overlays are kept, defaults to holder's icon if unset
var/menu_icon = "menu" // Icon state overlay when the computer is turned on, but no program is loaded that would override the screen.
var/screensaver_icon = "standby"
+ var/default_icon = "generic" //Overlay icon for programs that have a screen overlay the host doesn't have.
// Used for deciding if various tray icons need to be updated
var/last_battery_percent
@@ -169,7 +170,7 @@
/datum/extension/interactive/os/proc/system_boot()
on = TRUE
- var/datum/computer_file/data/autorun = get_file("autorun")
+ var/datum/computer_file/data/autorun = get_file("autorun", "local")
if(istype(autorun))
run_program(autorun.stored_data)
var/obj/item/stock_parts/computer/network_card/network_card = get_component(PART_NETWORK)
@@ -190,11 +191,11 @@
update_host_icon()
/datum/extension/interactive/os/proc/run_program(filename)
- var/datum/computer_file/program/P = get_file(filename)
+ var/datum/computer_file/program/P = get_file(filename, programs_dir)
var/mob/user = usr
if(!P || !istype(P)) // Program not found or it's not executable program.
- show_error(user, "I/O ERROR - Unable to run [filename]")
+ show_error(user, " - Unable to run [filename]")
return
if(!P.is_supported_by_hardware(get_hardware_flag(), user, TRUE))
@@ -227,17 +228,19 @@
return
active_program.program_state = PROGRAM_STATE_BACKGROUND // Should close any existing UIs
SSnano.close_uis(active_program.NM ? active_program.NM : active_program)
+ if(active_program.browser_module)
+ SSnano.close_uis(active_program.browser_module)
active_program = null
update_host_icon()
if(istype(user))
ui_interact(user) // Re-open the UI on this computer. It should show the main screen now.
/datum/extension/interactive/os/proc/set_autorun(program)
- var/datum/computer_file/data/autorun = get_file("autorun")
+ var/datum/computer_file/data/autorun = get_file("autorun", "local")
if(istype(autorun))
autorun.stored_data = "[program]"
else
- create_file("autorun", "[program]")
+ create_file("autorun", "local", "[program]") // Autorun file is created in the root directory.
/datum/extension/interactive/os/proc/add_log(var/text)
var/datum/extension/network_device/D = get_extension(get_component(PART_NETWORK), /datum/extension/network_device)
diff --git a/code/modules/modular_computers/os/components.dm b/code/modules/modular_computers/os/components.dm
index 6370a6f64e9..5a50bd135e1 100644
--- a/code/modules/modular_computers/os/components.dm
+++ b/code/modules/modular_computers/os/components.dm
@@ -31,7 +31,7 @@
if(network_card)
var/signal_power_level = NETWORK_SPEED_BASE * network_card.get_signal(specific_action)
if(signal_power_level > 0)
- signal_power_level = round(Clamp(signal_power_level, 1, 3))
+ signal_power_level = round(clamp(signal_power_level, 1, 3))
return signal_power_level
else
return 0
diff --git a/code/modules/modular_computers/os/files.dm b/code/modules/modular_computers/os/files.dm
index e0162132790..20c3dc26d23 100644
--- a/code/modules/modular_computers/os/files.dm
+++ b/code/modules/modular_computers/os/files.dm
@@ -1,112 +1,204 @@
-/datum/extension/interactive/os/proc/get_all_files(var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD))
- . = list()
- if(disk)
- return disk.stored_files
+/datum/extension/interactive/os
+ var/list/mounted_storage = list() // Dictionary of root name -> /datum/file_storage
+ var/programs_dir // For easy reference. Use only with OS filesystem procs.
-/datum/extension/interactive/os/proc/get_file(filename, var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD))
- if(disk)
- return disk.find_file_by_name(filename)
+/datum/extension/interactive/os/system_boot()
+ . = ..()
+ var/obj/item/stock_parts/computer/hard_drive = get_component(PART_HDD)
+ if(hard_drive)
+ mounted_storage["local"] = new /datum/file_storage/disk(src, "local")
+ programs_dir = "local" + "/" + OS_PROGRAMS_DIR
+ var/obj/item/stock_parts/computer/drive_slot/drive_slot = get_component(PART_D_SLOT)
+ if(drive_slot)
+ mounted_storage["media"] = new /datum/file_storage/disk/removable(src, "media")
+
+/datum/extension/interactive/os/system_shutdown()
+ QDEL_LIST_ASSOC_VAL(mounted_storage)
+ . = ..()
-/datum/extension/interactive/os/proc/create_file(var/newname, var/data, var/file_type = /datum/computer_file/data, var/list/metadata, var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD))
- if(!newname)
- return
- if(!disk)
- return
- if(get_file(newname))
+// Mounts a new file storage by a given root_name.
+/datum/extension/interactive/os/proc/mount_storage(storage_type, root_name, hidden)
+ if(!ispath(storage_type, /datum/file_storage) || !length(root_name))
return
- var/datum/computer_file/data/F = new file_type(md = metadata)
- F.filename = newname
- F.stored_data = data
- F.calculate_size()
- if(disk.store_file(F))
- return F
+ var/mount_name = root_name
+ var/i = 0
+ while(mounted_storage[mount_name])
+ i++
+ mount_name = root_name + "_[i]"
-/datum/extension/interactive/os/proc/store_file(var/datum/computer_file/file, var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD))
- if(!disk)
- return FALSE
- var/datum/computer_file/data/old_version = disk.find_file_by_name(file.filename)
- if(old_version)
- disk.remove_file(old_version)
- if(!disk.store_file(file))
- disk.store_file(old_version)
- return FALSE
- else
- return TRUE
+ var/datum/file_storage/new_storage = new storage_type(src, mount_name, hidden)
+ mounted_storage[mount_name] = new_storage
+ return new_storage
-/datum/extension/interactive/os/proc/try_store_file(var/datum/computer_file/file, var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD))
- if(!disk)
+/datum/extension/interactive/os/proc/unmount_storage(root_name)
+ var/datum/file_storage/removed = mounted_storage[root_name]
+ if(!removed)
return FALSE
- return disk.try_store_file(file)
-/datum/extension/interactive/os/proc/save_file(var/newname, var/data, var/file_type = /datum/computer_file/data, var/list/metadata, var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD), var/list/accesses, var/mob/user)
- if(!disk)
- return FALSE
- var/datum/computer_file/data/F = disk.find_file_by_name(newname)
- if(!F) //try to make one if it doesn't exist
- return !!create_file(newname, data, file_type, metadata, disk)
- if(!(F.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
- return FALSE
- //Try to save file, possibly won't fit size-wise
- var/datum/computer_file/data/backup = F.clone()
- disk.remove_file(F)
- F.stored_data = data
- F.metadata = metadata && metadata.Copy()
- F.calculate_size()
- if(!disk.store_file(F))
- disk.store_file(backup)
- return FALSE
+ // Tell programs to clean up any lingering references.
+ for(var/datum/computer_file/program/P in running_programs)
+ P.on_file_storage_removal(removed)
+
+ mounted_storage[root_name] = null
+ mounted_storage -= root_name
+
+ qdel(removed)
return TRUE
-/datum/extension/interactive/os/proc/delete_file(var/filename, var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD), var/list/accesses, var/mob/user)
- if(!disk)
- return FALSE
+/datum/extension/interactive/os/proc/mount_mainframe(root_name, mainframe_tag)
+ var/datum/computer_network/network = get_network()
+ if(!network)
+ return "NETWORK ERROR: Cannot connect to network."
+ var/datum/extension/network_device/mainframe/mainframe = network.get_device_by_tag(mainframe_tag)
+ if(!istype(mainframe))
+ return "NETWORK ERROR: No mainframe with network tag '[mainframe_tag]' found."
+
+ var/datum/file_storage/network/created_storage = mount_storage(/datum/file_storage/network, root_name, FALSE)
+ if(!created_storage)
+ return "I/O ERROR: Unable to mount mainframe as file system with root directory '[root_name]'."
+ created_storage.server = mainframe_tag
+ return "Successfully mounted mainframe with network tag '[mainframe_tag]' as file system with root directory '[root_name]'."
+
+// Rundown of the filesystem hierarchy:
+// The OS creates instances of /datum/file_storage as local or network disks, referenced in its mounted_storage list.
+// mounted_storage is keyed by the name of the root directory of the disk.
+// Filesystem procs on the OS extension itself expects directory paths with root directories e.g. /local/programs or /network/logs
+// Filesystem procs on the file_storage datums and harddrives do not, eg. /programs or /logs
+
+// Returns list(/datum/file_storage, /datum/computer_file/directory) on success, error code on failure. Only supports absolute paths.
+// This should not be done every tick for UIs etc. Cache a reference to the directory/file and only re-parse the directory when necessary.
+/datum/extension/interactive/os/proc/parse_directory(directory_path, create_directories = FALSE)
+ var/list/directories = splittext(directory_path, "/")
+
+ // Cut out any extraneous spaces which may have came from splitting the path
+ if(!length(directories[1]))
+ directories.Cut(1, 2)
+ if(!length(directories[directories.len]))
+ directories.Cut(directories.len)
+
+ var/datum/file_storage/storage = mounted_storage[directories[1]]
+ if(!storage)
+ return OS_DIR_NOT_FOUND
+ if(length(directories) == 1) // Root directory of the storage
+ return list(storage, null)
+ var/datum/computer_file/directory/dir = storage.parse_directory(jointext(directories, "/", 2), create_directories)
+ if(!istype(dir)) // Error!
+ return dir
+ return list(storage, dir)
+
+// Returns list(/datum/file_storage, /datum/computer_file/directory, /datum/computer_file) from the passed path. Only supports absolute paths.
+/datum/extension/interactive/os/proc/parse_file(file_path)
+ var/list/paths = splittext(file_path, "/")
+ if(!length(paths))
+ return OS_DIR_NOT_FOUND
+ if(!length(paths[1]))
+ paths.Cut(1, 2)
+
+ var/list/file_loc = parse_directory(jointext(paths, "/", paths.len))
+ if(!islist(file_loc))
+ return file_loc
+ var/datum/file_storage/storage = file_loc[1]
+ var/datum/computer_file/F = storage.get_file(paths[paths.len], file_loc[2])
+ if(!istype(F))
+ return F
+ return list(storage, file_loc[2], F)
- var/datum/computer_file/F = disk.find_file_by_name(filename)
- if(!F || F.undeletable)
- return FALSE
+/datum/extension/interactive/os/proc/get_all_files(obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD))
+ . = list()
+ if(disk)
+ return disk.stored_files
- return disk.remove_file(F, accesses, user)
+// Returns the file with the given name in the given directory path. Returns error code on failure.
+/datum/extension/interactive/os/proc/get_file(filename, dir_path, list/accesses, mob/user)
+ var/list/file_loc = parse_directory(dir_path)
+ if(!islist(file_loc))
+ return file_loc
+ var/datum/file_storage/storage = file_loc[1]
+ return storage.get_file(filename, file_loc[2])
-/datum/extension/interactive/os/proc/clone_file(var/filename, var/obj/item/stock_parts/computer/hard_drive/disk = get_component(PART_HDD), var/list/accesses, var/mob/user)
- if(!disk)
- return FALSE
+// Stores the passed file in the given directory path. Returns OS_FILE_SUCCESS on success, error code on failure.
+/datum/extension/interactive/os/proc/store_file(datum/computer_file/file, dir_path, create_directories = FALSE, list/accesses, mob/user, overwrite = TRUE)
+ var/list/file_loc = parse_directory(dir_path, create_directories)
+ if(!islist(file_loc))
+ return file_loc
- var/datum/computer_file/F = disk.find_file_by_name(filename)
- if(!F)
- return FALSE
-
- if(!(F.get_file_perms(accesses, user) & OS_READ_ACCESS))
- return FALSE
+ var/datum/file_storage/storage = file_loc[1]
+ return storage.store_file(file, file_loc[2], create_directories, accesses, user, overwrite)
- var/datum/computer_file/C = F.clone(1)
+// Checks if the passed file can be stored in the given directory path without actually storing it. Returns OS_FILE_SUCCESS on success, error code on failure.
+/datum/extension/interactive/os/proc/try_store_file(datum/computer_file/file, dir_path, list/accesses, mob/user)
+ var/list/file_loc = parse_directory(dir_path)
+ if(!islist(file_loc))
+ return file_loc
- return disk.store_file(C)
+ var/datum/file_storage/storage = file_loc[1]
+ return storage.try_store_file(file, file_loc[2], accesses, user)
-/datum/extension/interactive/os/proc/copy_between_disks(var/filename, var/obj/item/stock_parts/computer/hard_drive/disk_from, var/obj/item/stock_parts/computer/hard_drive/disk_to, var/list/accesses, var/mob/user)
- if(!istype(disk_from) || !istype(disk_to))
- return FALSE
+// Helper for creating a file. Returns file on success, error code on failure.
+/datum/extension/interactive/os/proc/create_file(filename, dir_path, data, file_type = /datum/computer_file/data/text, list/metadata, list/accesses, mob/user)
+ filename = sanitize_for_file(filename)
+ if(!length(filename))
+ return OS_BAD_NAME
- var/datum/computer_file/F = disk_from.find_file_by_name(filename)
- if(!istype(F))
- return FALSE
- if(!(F.get_file_perms(accesses, user) & OS_READ_ACCESS))
- return FALSE
- var/datum/computer_file/C = F.clone(0)
- return disk_to.store_file(C)
+ var/list/file_loc = parse_directory(dir_path)
+ if(!islist(file_loc))
+ return file_loc
+
+ var/datum/file_storage/storage = file_loc[1]
+
+ return storage.create_file(filename, file_loc[2], data, file_type, metadata, accesses, user)
+
+// Saves or creates the file with the given name in the passed directory. Returns file on success, error code on failure.
+/datum/extension/interactive/os/proc/save_file(filename, dir_path, new_data, file_type = /datum/computer_file/data/text, list/metadata, list/accesses, mob/user)
+ filename = sanitize_for_file(filename)
+ if(!length(filename))
+ return OS_BAD_NAME
+
+ var/list/file_loc = parse_directory(dir_path)
+ if(!islist(file_loc))
+ return file_loc
+
+ var/datum/file_storage/storage = file_loc[1]
+ return storage.save_file(filename, file_loc[2], new_data, metadata, accesses, user, file_type)
+
+// Deletes the file with the given filepath. Returns OS_FILE_SUCCESS on success, error code on failure.
+/datum/extension/interactive/os/proc/delete_file(filepath, list/accesses, mob/user)
+ var/list/file_loc = parse_file(filepath)
+ if(!islist(file_loc))
+ return file_loc
+
+ var/datum/file_storage/storage = file_loc[1]
+ return storage.delete_file(file_loc[3], file_loc[2], accesses, user)
+
+/datum/extension/interactive/os/proc/clone_file(filename, dir_path, list/accesses, mob/user)
+ var/list/file_loc = parse_directory(dir_path)
+ if(!islist(file_loc))
+ return file_loc
+
+ var/datum/file_storage/storage = file_loc[1]
+ return storage.clone_file(filename, file_loc[2], accesses, user)
// Generic file storage interface
/datum/file_storage
- var/name = "Generic File Interface"
+ var/desc = "Generic File Interface"
+ var/root_name // Display name of the root directory which doesn't exist as an actual directory file.
var/datum/extension/interactive/os/os
+ var/hidden = FALSE // Whether this file storage interface is for internal use.
-/datum/file_storage/New(ntos)
+/datum/file_storage/New(ntos, name, is_hidden)
os = ntos
+ root_name = name
+ hidden = is_hidden
/datum/file_storage/Destroy(force)
os = null
. = ..()
+// Additional check for access on top of file system permissions.
+/datum/file_storage/proc/check_access(list/accesses)
+ return TRUE
+
/datum/file_storage/proc/check_errors()
if(!istype(os))
return "No GOOSE compatible device found."
@@ -116,36 +208,96 @@
/datum/file_storage/proc/get_all_files()
-/datum/file_storage/proc/get_file(filename)
+/datum/file_storage/proc/get_dir_files(datum/computer_file/directory/dir)
+ . = list()
+ var/list/all_files = get_all_files()
+ if(dir && (dir in all_files))
+ . = dir.get_held_files()
+ else
+ // No current directory, get all files which are not held by a directory
+ if(!all_files)
+ return
+ for(var/file in all_files)
+ if(!all_files[file]) // No directory associated with the file.
+ . += file
+
+ return sortTim(., /proc/cmp_files_sort)
+
+// The following procs should return OS_FILE_SUCCESS on success (or the target file), or error codes on failure.
+/datum/file_storage/proc/get_file(filename, directory)
-/datum/file_storage/proc/store_file(datum/computer_file/F, copied)
+/datum/file_storage/proc/store_file(datum/computer_file/F, directory, create_directories, list/accesses, mob/user, overwrite = TRUE)
-/datum/file_storage/proc/save_file(filename, new_data)
+/datum/file_storage/proc/try_store_file(datum/computer_file/F, directory, list/accesses, mob/user)
-/datum/file_storage/proc/delete_file(filename)
+/datum/file_storage/proc/save_file(filename, directory, new_data, list/metadata, list/accesses, mob/user, file_type = /datum/computer_file/data)
-/datum/file_storage/proc/create_file(newname, var/file_type = /datum/computer_file/data/text)
+/datum/file_storage/proc/delete_file(datum/computer_file/F, list/accesses, mob/user)
+
+/datum/file_storage/proc/create_file(filename, directory, data, file_type = /datum/computer_file/data, list/metadata, list/accesses, mob/user)
if(check_errors())
- return FALSE
- var/datum/computer_file/F = new file_type
- F.filename = newname
- var/datum/computer_file/data/FD = F
+ return OS_HARDDRIVE_ERROR
+
+ filename = sanitize_for_file(filename)
+ if(!length(filename))
+ return OS_BAD_NAME
+ var/datum/computer_file/F = new file_type(md = metadata)
+ F.filename = filename
if(istype(F, /datum/computer_file/data))
+ var/datum/computer_file/data/FD = F
FD.calculate_size()
- return store_file(F)
+ var/success = store_file(F, directory, FALSE, accesses, user)
+ if(success == OS_FILE_SUCCESS)
+ return F
+ qdel(F)
+ return success
-/datum/file_storage/proc/clone_file(filename)
+/datum/file_storage/proc/create_directory(filename, directory, list/accesses, mob/user)
+ return create_file(filename, directory, null, /datum/computer_file/directory, null, accesses, user)
+
+/datum/file_storage/proc/clone_file(filename, directory, list/accesses, mob/user)
if(check_errors())
- return FALSE
- var/datum/computer_file/F = get_file(filename)
- if(F)
- store_file(F.clone(1))
+ return OS_HARDDRIVE_ERROR
+ var/datum/computer_file/F = get_file(filename, directory)
+ if(!istype(F))
+ return F
+ if(!(F.get_file_perms(accesses, user) & OS_READ_ACCESS))
+ return OS_FILE_NO_READ
+
+ var/datum/computer_file/cloned_file = F.Clone(TRUE)
+ if(!istype(cloned_file))
+ return OS_FILE_NO_READ
+
+ var/success = store_file(cloned_file, directory, accesses, user)
+ if(success != OS_FILE_SUCCESS)
+ qdel(cloned_file) // Clean up after ourselves
+ return success
+
+/datum/file_storage/proc/get_dir_path(datum/computer_file/directory/current_directory, full)
+ if(current_directory)
+ if(full)
+ return "[root_name]/" + current_directory.get_file_path()
+ return current_directory.filename
+ return root_name
+
+/datum/file_storage/proc/parse_directory(directory_path, create_directories = FALSE)
// Storing stuff on a server in computer network
/datum/file_storage/network
- name = "Remote File Server"
+ desc = "Remote File Server"
var/server = "NONE" //network tag of the file server
+/datum/file_storage/network/New(ntos, name, hidden, server_tag)
+ . = ..()
+ if(server_tag)
+ server = server_tag
+
+/datum/file_storage/network/check_access(list/accesses)
+ var/datum/extension/network_device/mainframe/M = get_mainframe()
+ if(!M)
+ return
+ return M.has_access(accesses)
+
/datum/file_storage/network/check_errors()
. = ..()
if(.)
@@ -161,6 +313,9 @@
if(!istype(M))
return "NETWORK ERROR: Invalid server '[server]', no file sharing capabilities detected"
+/datum/file_storage/network/proc/set_server(new_server)
+ server = new_server
+
/datum/file_storage/network/proc/get_mainframe()
if(check_errors())
return FALSE
@@ -171,22 +326,41 @@
var/datum/extension/network_device/mainframe/M = get_mainframe()
return M && M.get_all_files()
-/datum/file_storage/network/get_file(filename)
+/datum/file_storage/network/get_file(filename, directory)
var/datum/extension/network_device/mainframe/M = get_mainframe()
- return M && M.get_file(filename)
+ if(!M)
+ return OS_NETWORK_ERROR
+ return M.get_file(filename, directory)
-/datum/file_storage/network/store_file(datum/computer_file/F, copied)
- var/datum/computer_file/stored = copied ? F.clone() : F
+/datum/file_storage/network/store_file(datum/computer_file/F, directory, create_directories, list/accesses, mob/user, overwrite = TRUE)
var/datum/extension/network_device/mainframe/M = get_mainframe()
- return M && M.store_file(stored)
+ if(!M)
+ return OS_NETWORK_ERROR
+ return M.store_file(F, directory, create_directories, accesses, user, overwrite)
-/datum/file_storage/network/delete_file(filename, list/accesses, mob/user)
+/datum/file_storage/network/try_store_file(datum/computer_file/F, directory, list/accesses, mob/user)
var/datum/extension/network_device/mainframe/M = get_mainframe()
- return M && M.delete_file(filename, accesses, user)
+ if(!M)
+ return OS_NETWORK_ERROR
+ return M.try_store_file(F, directory, accesses, user)
-/datum/file_storage/network/save_file(filename, new_data)
+/datum/file_storage/network/delete_file(datum/computer_file/F, list/accesses, mob/user)
var/datum/extension/network_device/mainframe/M = get_mainframe()
- return M && M.save_file(filename, new_data)
+ if(!M)
+ return OS_NETWORK_ERROR
+ return M.delete_file(F, accesses, user)
+
+/datum/file_storage/network/save_file(filename, directory, new_data, list/metadata, list/accesses, mob/user, file_type = /datum/computer_file/data)
+ var/datum/extension/network_device/mainframe/M = get_mainframe()
+ if(!M)
+ return OS_NETWORK_ERROR
+ return M.save_file(filename, directory, new_data, metadata, accesses, user, file_type)
+
+/datum/file_storage/network/parse_directory(directory_path, create_directories)
+ var/datum/extension/network_device/mainframe/M = get_mainframe()
+ if(!M)
+ return OS_NETWORK_ERROR
+ return M.parse_directory(directory_path, create_directories)
/datum/file_storage/network/get_transfer_speed()
if(check_errors())
@@ -229,11 +403,11 @@
// Storing stuff locally on some kinda disk
/datum/file_storage/disk
- name = "Local Storage"
+ desc = "Local Storage"
var/disk_type = PART_HDD
/datum/file_storage/disk/proc/get_disk()
- return os.get_component(PART_HDD)
+ return os.get_component(disk_type)
/datum/file_storage/disk/check_errors()
. = ..()
@@ -248,37 +422,66 @@
/datum/file_storage/disk/get_all_files()
if(check_errors())
return FALSE
- return os.get_all_files(get_disk())
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_disk()
+ return HDD.stored_files
-/datum/file_storage/disk/get_file(filename)
+/datum/file_storage/disk/get_file(filename, directory)
if(check_errors())
- return FALSE
- return os.get_file(filename, get_disk())
+ return OS_HARDDRIVE_ERROR
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_disk()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ return HDD.find_file_by_name(filename, directory)
-/datum/file_storage/disk/store_file(datum/computer_file/F, copied)
- var/datum/computer_file/stored = copied ? F.clone() : F
+/datum/file_storage/disk/store_file(datum/computer_file/F, directory, create_directories, list/accesses, mob/user, overwrite = TRUE)
if(check_errors())
- return FALSE
- return os.store_file(stored, get_disk())
+ return OS_HARDDRIVE_ERROR
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_disk()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ return HDD.store_file(F, directory, create_directories, accesses, user, overwrite)
-/datum/file_storage/disk/save_file(filename, new_data, list/accesses, mob/user)
+/datum/file_storage/disk/try_store_file(datum/computer_file/F, directory, list/accesses, mob/user)
if(check_errors())
- return FALSE
- return os.save_file(filename, new_data, get_disk(), accesses, user)
+ return OS_HARDDRIVE_ERROR
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_disk()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ return HDD.try_store_file(F, directory, accesses, user)
-/datum/file_storage/disk/delete_file(filename, list/accesses, mob/user)
+/datum/file_storage/disk/save_file(filename, directory, new_data, metadata, accesses, user, file_type = /datum/computer_file/data)
if(check_errors())
- return FALSE
- return os.delete_file(filename, get_disk(), accesses, user)
+ return OS_HARDDRIVE_ERROR
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_disk()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ return HDD.save_file(filename, directory, new_data, metadata, accesses, user, file_type)
+
+/datum/file_storage/disk/delete_file(datum/computer_file/F, list/accesses, mob/user)
+ if(check_errors())
+ return OS_HARDDRIVE_ERROR
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_disk()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ return HDD.remove_file(F, accesses, user)
/datum/file_storage/disk/get_transfer_speed()
if(check_errors())
return 0
return NETWORK_SPEED_DISK
+/datum/file_storage/disk/parse_directory(directory_path, create_directories)
+ if(check_errors())
+ return OS_HARDDRIVE_ERROR
+ var/obj/item/stock_parts/computer/hard_drive/HDD = get_disk()
+ if(!HDD)
+ return OS_HARDDRIVE_ERROR
+ return HDD.parse_directory(directory_path, create_directories)
+
// Storing files on a removable disk.
/datum/file_storage/disk/removable
- name = "Disk Drive"
+ desc = "Disk Drive"
+ root_name = "media"
/datum/file_storage/disk/removable/get_disk()
var/obj/item/stock_parts/computer/drive_slot/drive_slot = os.get_component(PART_D_SLOT)
@@ -296,50 +499,63 @@
if(!istype(drive_slot.stored_drive))
return "HARDWARE ERROR: No portable drive inserted."
-
// Datum tracking progress between of file transfer between two file streams
/datum/file_transfer
var/datum/file_storage/transfer_from
var/datum/file_storage/transfer_to
+
+ var/datum/computer_file/directory/directory_to
+ var/datum/computer_file/directory/directory_from
+
var/datum/computer_file/transferring
var/left_to_transfer
var/copying = FALSE // Whether or not this file transfer is copying, rather than transferring.
-/datum/file_transfer/New(datum/file_storage/source, datum/file_storage/destination, datum/computer_file/file, copy)
+/datum/file_transfer/New(datum/file_storage/source, datum/file_storage/destination, datum/computer_file/directory/dest_directory, datum/computer_file/file, copy)
transfer_from = source
transfer_to = destination
transferring = file
- left_to_transfer = file.size
+
+ directory_to = dest_directory
+ directory_from = file.get_directory()
+
+ if(istype(file, /datum/computer_file/directory))
+ var/datum/computer_file/directory/dir = file
+ left_to_transfer = dir.get_held_size()
+ else
+ left_to_transfer = file.size
copying = copy
/datum/file_transfer/Destroy()
transfer_from = null
transfer_to = null
+
+ directory_to = null
transferring = null
. = ..()
/datum/file_transfer/proc/check_self()
if(QDELETED(transfer_from) || QDELETED(transfer_from) || QDELETED(transferring))
qdel(src)
- return FALSE
- return TRUE
+ return OS_FILE_NOT_FOUND
+ return OS_FILE_SUCCESS
-//Returns FALSE if something went wrong, TRUE if progress was made and or we are done
+//Returns OS_FILE_SUCESS if progress was made and or we are done. Returns error code otherwise.
/datum/file_transfer/proc/update_progress()
. = check_self()
- if(!.)
+ if(. != OS_FILE_SUCCESS)
return
left_to_transfer = max(0, left_to_transfer - get_transfer_speed())
if(!left_to_transfer)
if(copying)
- return transfer_to.store_file(transferring, TRUE)
+ return transfer_to.store_file(transferring, directory_to, TRUE)
else
- . = transfer_from.delete_file(transferring.filename) // Check if we can delete the file.
- if(.)
- . = transfer_to.store_file(transferring, FALSE)
+ . = transfer_from.delete_file(transferring) // Check if we can delete the file.
+ if(. == OS_FILE_SUCCESS)
+ . = transfer_to.store_file(transferring, directory_to, FALSE)
// If we failed to store the file, restore it to its former location.
- if(!.)
- transfer_from.store_file(transferring, FALSE)
+ if(. != OS_FILE_SUCCESS)
+ transfer_from.store_file(transferring, directory_from, FALSE)
/datum/file_transfer/proc/get_transfer_speed()
if(!check_self())
@@ -355,9 +571,38 @@
if(!check_self())
return
var/list/data = list()
- data["transfer_from"] = transfer_from.name
- data["transfer_to"] = transfer_to.name
+
+ data["transfer_from"] = transfer_from.get_dir_path(directory_from, TRUE)
+ data["transfer_to"] = transfer_to.get_dir_path(directory_to, TRUE)
data["transfer_file"] = transferring.filename
data["transfer_progress"] = transferring.size - left_to_transfer
data["transfer_total"] = transferring.size
- return data
\ No newline at end of file
+ return data
+
+// Checks permissions for transferring files. Returns error string on failure, null on success.
+/proc/check_file_transfer(datum/computer_file/directory/destination, datum/computer_file/file, copy, list/accesses, mob/user)
+ if(!file)
+ return "Could not locate file"
+
+ if(destination)
+ if(!(destination.get_file_perms(accesses, user) & OS_WRITE_ACCESS))
+ return "You lack access to the directory [destination.filename]"
+
+ if(file)
+ var/req_access = copy ? OS_READ_ACCESS : OS_WRITE_ACCESS
+ var/move_string = copy ? "copy" : "transfer"
+
+ if(istype(file, /datum/computer_file/directory))
+ var/datum/computer_file/directory/dir = file
+ if(!copy)
+ if(dir.undeletable)
+ return "You lack permission to [move_string] the directory [dir.filename]"
+ for(var/datum/computer_file/child in dir.get_held_files())
+ if(child.undeletable)
+ return "You lack permission to [move_string] the directory [dir.filename]"
+
+ if(!(dir.get_held_perms(accesses, user) & req_access))
+ return "You lack permission to [move_string] the directory [dir.filename]"
+ else
+ if((!copy && file.undeletable) || !(file.get_file_perms(accesses, user) & req_access))
+ return "You lack permission to [move_string] the file [file.filename]"
\ No newline at end of file
diff --git a/code/modules/modular_computers/os/ui.dm b/code/modules/modular_computers/os/ui.dm
index cd4e01ca1b1..26a6980500a 100644
--- a/code/modules/modular_computers/os/ui.dm
+++ b/code/modules/modular_computers/os/ui.dm
@@ -19,12 +19,13 @@
show_error(user, "DISK ERROR")
return // No HDD, No HDD files list or no stored files. Something is very broken.
- var/datum/computer_file/data/autorun = get_file("autorun")
+ var/datum/computer_file/data/autorun = get_file("autorun", "local")
var/list/data = get_header_data()
var/list/programs = list()
- for(var/datum/computer_file/program/P in hard_drive.stored_files)
+ var/datum/computer_file/directory/program_files = hard_drive.parse_directory(OS_PROGRAMS_DIR, TRUE)
+ for(var/datum/computer_file/program/P in program_files.get_held_files())
var/list/program = list()
program["name"] = P.filename
program["desc"] = P.filedesc
@@ -97,7 +98,7 @@
return TOPIC_HANDLED
if( href_list["PC_killprogram"] )
- var/datum/computer_file/program/P = get_file(href_list["PC_killprogram"])
+ var/datum/computer_file/program/P = get_file(href_list["PC_killprogram"], programs_dir)
if(!istype(P) || P.program_state == PROGRAM_STATE_KILLED)
return TOPIC_HANDLED
@@ -169,7 +170,7 @@
SSnano.update_uis(active_program.NM)
// Function used by NanoUI's to obtain data for header. All relevant entries begin with "PC_"
-/datum/extension/interactive/os/proc/get_header_data()
+/datum/extension/interactive/os/proc/get_header_data(file_browser = FALSE)
var/list/data = list()
var/obj/item/stock_parts/computer/battery_module/battery_module = get_component(PART_BATTERY)
if(battery_module)
@@ -223,6 +224,7 @@
data["PC_stationtime"] = stationtime2text()
data["PC_hasheader"] = !updating
data["PC_showexitprogram"] = active_program ? 1 : 0 // Hides "Exit Program" button on mainscreen
+ data["PC_showshutdown"] = !file_browser // Don't show shutdown/program closing options from file browser window
var/datum/computer_file/data/account/cur_account = get_account_nocheck()
data["PC_loggedin"] = cur_account?.login
diff --git a/code/modules/modular_computers/os/visuals.dm b/code/modules/modular_computers/os/visuals.dm
index 08c4d9e3af3..7d03f0e75d5 100644
--- a/code/modules/modular_computers/os/visuals.dm
+++ b/code/modules/modular_computers/os/visuals.dm
@@ -20,7 +20,10 @@
if(screen_icon_file)
var/image/I
if(active_program)
- I = image(screen_icon_file, active_program.program_icon_state)
+ if(active_program.program_icon_state in icon_states(screen_icon_file))
+ I = image(screen_icon_file, active_program.program_icon_state)
+ else
+ I = image(screen_icon_file, default_icon) //Fallback icon
else
I = image(screen_icon_file, menu_icon)
I.appearance_flags |= RESET_COLOR
diff --git a/code/modules/modular_computers/terminal/terminal.dm b/code/modules/modular_computers/terminal/terminal.dm
index 0066c0de5e6..994bbc2f774 100644
--- a/code/modules/modular_computers/terminal/terminal.dm
+++ b/code/modules/modular_computers/terminal/terminal.dm
@@ -8,15 +8,13 @@
var/network_target // Network tag of whatever device is being targeted on the network by commands.
// Terminal can act as a file transfer utility.
- var/list/disks = list(
- /datum/file_storage/disk,
- /datum/file_storage/disk/removable,
- /datum/file_storage/network
- )
var/datum/file_storage/current_disk
+ var/datum/computer_file/directory/current_directory
+
var/datum/file_transfer/current_move
var/datum/extension/interactive/os/computer
+ var/list/disks = list()
/datum/terminal/New(mob/user, datum/extension/interactive/os/computer)
..()
@@ -55,7 +53,7 @@
/datum/terminal/Process()
if(current_move)
var/result = current_move.update_progress()
- if(!result)
+ if(result != OS_FILE_SUCCESS)
if(QDELETED(current_move))
append_to_history("File Move Cancelled: Unknown error.")
else
@@ -66,7 +64,7 @@
var/completion = round(1 - (current_move.left_to_transfer / current_move.transferring.size), 0.01) * 100
append_to_history("File Move: [completion]% complete.")
else
- append_to_history("File Move: Successfully copied file '[current_move.transferring.filename]' to [current_move.transfer_to].")
+ append_to_history("File Move: Successfully [current_move.copying ? "copied" : "moved"] file '[current_move.transferring.filename]' to '[current_move.transfer_to.get_dir_path(current_move.directory_to, TRUE)]'.")
QDEL_NULL(current_move)
if(!can_use(get_user()))
@@ -100,7 +98,7 @@
account_name = "LOCAL"
else
account_name = "GUEST"
- content += "
"
+ content += "
"
content += "
type `man` for a list of available commands."
panel.set_content("
[jointext(content, "
")]")
@@ -120,9 +118,10 @@
return 1
/datum/terminal/proc/append_to_history(var/text)
- history += text
- if(length(history) > history_max_length)
- history.Cut(1, length(history) - history_max_length + 1)
+ if(length(text))
+ history += text
+ if(length(history) > history_max_length)
+ history.Cut(1, length(history) - history_max_length + 1)
update_content()
panel.update()
@@ -154,4 +153,117 @@
/datum/terminal/proc/get_access(mob/user)
var/datum/extension/interactive/os/account_computer = get_account_computer()
- return(account_computer.get_access(user))
\ No newline at end of file
+ return(account_computer.get_access(user))
+
+// Returns list(/datum/file_storage, directory) on success. Returns error code on failure.
+/datum/terminal/proc/parse_directory(directory_path, create_directories = FALSE)
+ var/datum/file_storage/target_disk = current_disk
+ var/datum/computer_file/directory/root_dir = current_directory
+
+ if(!length(directory_path))
+ return list(target_disk, root_dir)
+
+ if(directory_path[1] == "/") // This is an absolute path, so we can pass it directly to the OS proc to be processed.
+ return computer.parse_directory(directory_path, create_directories)
+
+ // Otherwise, we append the working directory path to the passed path.
+ var/list/directories = splittext(directory_path, "/")
+
+ // When splitting the text, there could be blank strings at either end, so remove them. If there's any in the body of the path, there was a
+ // missed input, so leave them.
+ if(!length(directories[1]))
+ directories.Cut(1, 2)
+ if(!length(directories[directories.len]))
+ directories.Cut(directories.len)
+
+ for(var/dir in directories)
+ if(dir == "..") // Up a directory.
+ if(root_dir)
+ root_dir = root_dir.get_directory()
+ directories.Cut(1, 2)
+ continue
+ if(target_disk)
+ target_disk = null
+ directories.Cut(1, 2)
+ continue
+ // We're trying to move up past the mounting points, return failure.
+ return OS_DIR_NOT_FOUND
+
+ if(!target_disk)
+ target_disk = computer.mounted_storage[dir]
+ if(!target_disk) // Invalid disk entered.
+ return OS_DIR_NOT_FOUND
+ directories.Cut(1, 2)
+
+ break // Any further use of ../ is handled by the hard drive.
+
+ // If we were only pathing to the parent of a directory or to a disk, we can return early.
+ if(!length(directories))
+ return list(target_disk, root_dir)
+
+ // Assemble the final path from whatever root directory we're in, and the remaining entered paths.
+ // The hard drive handles the rest.
+ var/final_path = root_dir ? root_dir.get_file_path() + "/" : ""
+ final_path += jointext(directories, "/")
+ var/datum/computer_file/directory/target_directory = target_disk.parse_directory(final_path, create_directories)
+ if(!istype(target_directory))
+ return OS_DIR_NOT_FOUND
+
+ return list(target_disk, target_directory)
+
+// Returns list(/datum/file_storage, /datum/computer_file/directory, /datum/computer_file) on success. Returns error code on failure.
+/datum/terminal/proc/parse_file(file_path)
+ if(!length(file_path))
+ return OS_FILE_NOT_FOUND
+ if(file_path[1] == "/") // As above, this is an absolute path, which the OS can handle directly.
+ return computer.parse_file(file_path)
+
+ var/list/dirs_and_file = splittext(file_path, "/")
+ if(!length(dirs_and_file))
+ return OS_DIR_NOT_FOUND
+
+ // Join together everything but the filename into a path.
+ var/list/file_loc = parse_directory(jointext(dirs_and_file, "/", 1, dirs_and_file.len))
+ if(!islist(file_loc)) // Errored!
+ return file_loc
+
+ var/datum/file_storage/target_disk = file_loc[1]
+ var/datum/computer_file/directory/target_dir = file_loc[2]
+ if(!istype(target_disk))
+ return OS_DIR_NOT_FOUND
+
+ var/filename = dirs_and_file[dirs_and_file.len]
+ var/datum/computer_file/target_file = target_disk.get_file(filename, target_dir)
+ if(!istype(target_file))
+ return OS_FILE_NOT_FOUND
+
+ return list(target_disk, target_dir, target_file)
+
+/proc/get_terminal_error(path, error_code)
+ var/list/dirs_and_file = splittext(path, "/")
+ var/dir_path = jointext(dirs_and_file, "/", 1, dirs_and_file.len)
+ if(!length(dirs_and_file))
+ return "Unable to parse passed path."
+ var/filename = dirs_and_file[dirs_and_file.len]
+
+ switch(error_code)
+ if(OS_FILE_NOT_FOUND)
+ return "Unable to locate the file[length(filename) ? "'[filename]'" : ""]"
+ if(OS_DIR_NOT_FOUND)
+ return "Unable to locate the directory[length(dir_path) ? "'[dir_path]'" : ""]"
+ if(OS_FILE_EXISTS)
+ return "A file with name '[filename]' already exists"
+ if(OS_BAD_NAME)
+ return "The file name '[filename]' is invalid"
+ if(OS_FILE_NO_READ)
+ return "You do not have permission to read the file[length(filename) ? "'[filename]'" : ""]"
+ if(OS_FILE_NO_WRITE)
+ return "You do not have permission to modify the file[length(filename) ? "'[filename]'" : ""]"
+ if(OS_HARDDRIVE_SPACE)
+ return "Insufficient harddrive space"
+ if(OS_HARDDRIVE_ERROR)
+ return "I/O error, Harddrive may be non-functional"
+ if(OS_NETWORK_ERROR)
+ return "Unable to connect to the network"
+
+ return "An unspecified error occured."
\ No newline at end of file
diff --git a/code/modules/modular_computers/terminal/terminal_commands.dm b/code/modules/modular_computers/terminal/terminal_commands.dm
index 823b16809d4..1b316db0208 100644
--- a/code/modules/modular_computers/terminal/terminal_commands.dm
+++ b/code/modules/modular_computers/terminal/terminal_commands.dm
@@ -34,7 +34,28 @@ var/global/list/terminal_commands
/datum/terminal_command/proc/get_arguments(text)
var/argtext = copytext(text, length(pattern) + 1)
- return splittext(argtext, " ")
+
+ var/cur_string = ""
+ var/list/arguments = list()
+
+ var/last_was_escape = FALSE
+ for(var/i in 1 to length(argtext)) // Allow players to escape spaces by using '\'.
+ var/char = argtext[i]
+ if(char == "\\")
+ last_was_escape = TRUE
+ continue
+ last_was_escape = FALSE
+ if(char == " ")
+ if(!last_was_escape) // Space wasn't escaped.
+ if(length(cur_string))
+ arguments += cur_string
+ cur_string = ""
+ continue
+ cur_string += char
+
+ if(length(cur_string))
+ arguments += cur_string
+ return arguments
// null return: continue. "" return will break and show a blank line. Return list() to break and not show anything.
/datum/terminal_command/proc/parse(text, mob/user, datum/terminal/terminal)
@@ -62,7 +83,7 @@ var/global/list/terminal_commands
var/pg = clamp(selected_page, 1, max_pages)
var/start_index = (pg - 1)*pg_length + 1
- var/end_index = min(length(data), (pg)*pg_length)+1
+ var/end_index = min(length(data), pg*pg_length) + 1
. += data.Copy(start_index, end_index)
. += "[length(data)] [value_name]\s. Page [pg] / [max_pages]."
@@ -251,7 +272,7 @@ Subtypes
/datum/terminal_command/cd
name = "cd"
- man_entry = list("Format: cd \[target\] \[network tag\]", "Changes the current disk to the target.", "LOCAL, REMOVABLE, and NETWORK are supported.")
+ man_entry = list("Format: cd \[path\]", "Changes the current working directory.", "Both relative and absolute paths are supported.")
pattern = @"^cd"
/datum/terminal_command/cd/proper_input_entered(text, mob/user, datum/terminal/terminal)
@@ -260,94 +281,64 @@ Subtypes
var/list/cd_args = get_arguments(text)
- var/target = uppertext(cd_args[1])
- if(target == "LOCAL")
- terminal.current_disk = terminal.disks[/datum/file_storage/disk]
- if(!terminal.current_disk)
- return "cd: Could not locate disk."
- var/error = terminal.current_disk.check_errors()
- if(error)
- terminal.current_disk = null
- return "cd: [error]"
- return "cd: Changed to local disk."
- else if(target == "REMOVABLE")
- terminal.current_disk = terminal.disks[/datum/file_storage/disk/removable]
- if(!terminal.current_disk)
- return "cd: Could not locate removable disk."
- var/error = terminal.current_disk.check_errors()
- if(error)
- terminal.current_disk = null
- return "cd: [error]"
- return "cd: Changed to removable disk"
-
- else if(target == "NETWORK")
- var/datum/extension/interactive/os/origin = terminal.computer
- if(!origin || !origin.get_network_status())
- return "cd: Check network connectivity."
- var/datum/computer_network/network = terminal.computer.get_network()
- // Get the network tag input into the command, or the current network_target otherwise.
- var/network_tag = (length(cd_args) >= 2) ? cd_args[2] : terminal.network_target
- var/datum/extension/network_device/mainframe/M = network.get_device_by_tag(network_tag)
- if(!istype(M))
- return "cd: Could not locate file server with tag [network_tag]."
- if(!M.has_access(terminal.get_access(user)))
- return "cd: Access denied to file server with tag [network_tag]"
- terminal.current_disk = terminal.disks[/datum/file_storage/network]
- if(!terminal.current_disk)
- return "cd: Could not locate remote file server."
-
- var/datum/file_storage/network/N = terminal.current_disk
- N.server = network_tag
- var/error = terminal.current_disk.check_errors()
- if(error)
- terminal.current_disk = null
- return "cd: [error]"
- return "cd: Changed to remote file server with tag [network_tag]."
- else
- return "cd: Target disk not recognized. LOCAL, REMOVABLE, and NETWORK are supported."
+ var/target_directory = cd_args[1]
+
+ var/list/cd_targets = terminal.parse_directory(target_directory)
+ if(!islist(cd_targets))
+ return "cd: [get_terminal_error(target_directory, cd_targets)]."
+
+ terminal.current_disk = cd_targets[1]
+ terminal.current_directory = cd_targets[2]
+ return ""
/datum/terminal_command/ls
name = "ls"
- man_entry = list("Format: ls \[pg number\]", "Lists the files in the current disk, starting from the page number.")
+ man_entry = list("Format: ls \[pg number\]", "Lists the files in the working directory, starting from the page number.")
pattern = @"^ls"
/datum/terminal_command/ls/proper_input_entered(text, mob/user, datum/terminal/terminal)
- if(!terminal.current_disk || ispath(terminal.current_disk))
- return "ls: No disk selected."
-
var/list/ls_args = get_arguments(text)
var/selected_page = (length(ls_args)) ? text2num(ls_args[1]) : 1
if(!isnum(selected_page))
return "ls: Improper syntax, use format ls \[page number\]."
- var/list/files = terminal.current_disk.get_all_files()
+ if(!terminal.current_disk)
+ return print_as_page(terminal.computer.mounted_storage, "disk", selected_page, terminal.history_max_length - 1)
+ var/list/files = terminal.current_disk.get_dir_files(terminal.current_directory)
var/list/file_data = list()
for(var/datum/computer_file/F in files)
- file_data += "[F.filename].[F.filetype] - [F.size] GQ"
+ if(istype(F, /datum/computer_file/directory))
+ file_data += "[F.filename] - DIR"
+ else
+ file_data += "[F.filename].[F.filetype] - [F.size] GQ"
return print_as_page(file_data, "file", selected_page, terminal.history_max_length - 1)
/datum/terminal_command/remove
name = "rm"
- man_entry = list("Format: rm \[file name\]", "Removes the file given in the current disk.")
+ man_entry = list("Format: rm \[file path\]", "Removes the file with the given path.")
pattern = @"^rm\b"
/datum/terminal_command/remove/proper_input_entered(text, mob/user, datum/terminal/terminal)
- if(!terminal.current_disk)
- return "rm: No disk selected."
-
- var/file_name = copytext(text, 4)
-
- var/deleted = terminal.current_disk.delete_file(file_name, terminal.get_access(user), user)
- if(deleted)
- return "rm: Removed file [file_name]."
- else
- return "rm: Failed to remove file [file_name]. Additional access may be required."
-
+ var/file_path = copytext(text, 4)
+ var/list/file_loc = terminal.parse_file(file_path)
+ // Errored!
+ if(!islist(file_loc))
+ return "rm: [get_terminal_error(file_path, file_loc)]."
+
+ var/datum/file_storage/disk = file_loc[1]
+ var/datum/computer_file/file = file_loc[3]
+ var/deleted = disk.delete_file(file, terminal.get_access(user), user)
+ if(deleted == OS_FILE_SUCCESS)
+ return "rm: Removed file '[file.filename]'."
+ if(deleted == OS_FILE_NO_WRITE)
+ return "rm: You do not have permission to remove file '[file.filename]'."
+ // Other error. Most likely, the hard drive is non-functional.
+ return "rm: Failed to delete file '[file.filename]'. Hard drive may be non-functional."
/datum/terminal_command/move
name = "mv"
- man_entry = list("Format: mv \[file name\] \[destination\] \[copying (0/1) \]", "Moves a file in the current disk to another.")
+ man_entry = list("Format: mv \[file path\] \[destination\] \[copying (0/1) \]", "Moves a file to another directory.")
pattern = @"^mv"
/datum/terminal_command/move/proper_input_entered(text, mob/user, datum/terminal/terminal)
@@ -356,63 +347,32 @@ Subtypes
var/list/mv_args = get_arguments(text)
if(length(mv_args) < 2)
- return "mv: Improper syntax, use mv \[file name\] \[destination\] \[copying (0/1) \]."
- var/datum/computer_file/F = terminal.current_disk.get_file(mv_args[1])
- if(!F)
- return "mv: Could not find file with name [mv_args[1]]."
- var/copying = length(mv_args > 2) ? text2num(mv_args[3]) : FALSE
- if(copying == TRUE)
- if(!(F.get_file_perms(terminal.get_access(user), user) & OS_READ_ACCESS))
- return "mv: You do not have read access to this file."
- else
- if(!(F.get_file_perms(terminal.get_access(user), user) & OS_WRITE_ACCESS))
- return "mv: You do not have write access to this file. Write access is required when not copying files with mv"
+ return "mv: Improper syntax, use mv \[file path\] \[destination\] \[copying (0/1) \]."
+ var/source_path = mv_args[1]
+ var/list/file_loc = terminal.parse_file(source_path)
+ if(!islist(file_loc)) // Errored!
+ return "mv: [get_terminal_error(source_path, file_loc)]."
+
+ var/datum/computer_file/F = file_loc[3]
+ var/copying = length(mv_args) > 2 ? text2num(mv_args[3]) : FALSE
// Find the destination.
- var/datum/file_storage/dest
- var/target = uppertext(mv_args[2])
- if(target == "LOCAL")
- dest = terminal.disks[/datum/file_storage/disk]
- if(!dest)
- return "mv: Could not locate disk."
- var/error = dest.check_errors()
- if(error)
- return "mv: [error]"
- else if(target == "REMOVABLE")
- dest = terminal.disks[/datum/file_storage/disk/removable]
- if(!dest)
- return "mv: Could not locate removable disk."
- var/error = dest.check_errors()
- if(error)
- return "mv: [error]"
- else
- var/datum/extension/interactive/os/origin = terminal.computer
- if(!origin || !origin.get_network_status())
- return "mv: Check network connectivity."
- var/datum/computer_network/network = terminal.computer.get_network()
- var/datum/extension/network_device/mainframe/M = network.get_device_by_tag(target)
- if(!istype(M))
- return "mv: Could not locate destination with tag [target]."
- if(!M.has_access(terminal.get_access(user)))
- return "mv: Access denied to destination with tag [target]"
- dest = terminal.disks[/datum/file_storage/network]
- if(!dest)
- return "mv: Could not locate remote file server."
-
- var/datum/file_storage/network/N = dest
- N.server = target
- var/error = dest.check_errors()
- if(error)
- return "mv: [error]"
-
- if(!dest)
- return "mv: Could not locate file destination."
-
- terminal.current_move = new(terminal.current_disk, dest, F, copying)
+ var/target_path = mv_args[2]
+
+ var/list/destination = terminal.parse_directory(target_path)
+ if(!islist(destination))
+ return "mv: [get_terminal_error(target_path, file_loc)]."
+
+ // Check file permisisons.
+ var/error = check_file_transfer(destination[2], F, copying, terminal.get_access(user), user)
+ if(error)
+ return "mv: [error]."
+
+ terminal.current_move = new(file_loc[1], destination[1], destination[2], F, copying)
return "mv: Beginning file move..."
/datum/terminal_command/copy
name = "cp"
- man_entry = list("Format: cp \[file name\]", "Copies a file in the current disk.")
+ man_entry = list("Format: cp \[file path\]", "Copies the file with the given path.")
pattern = @"^cp"
/datum/terminal_command/copy/proper_input_entered(text, mob/user, datum/terminal/terminal)
@@ -420,43 +380,70 @@ Subtypes
return "cp: No disk selected."
if(length(text) < 4)
- return "cp: Improper syntax, use copy \[file name\]."
-
- var/target_name = copytext(text, 4)
- var/datum/computer_file/F = terminal.current_disk.get_file(target_name)
- if(!F)
- return "cp: Could not find file with name [target_name]."
+ return "cp: Improper syntax, use copy \[file path\]."
+
+ var/file_path = copytext(text, 4)
+ var/list/file_loc = terminal.parse_file(file_path)
+ // Errored!
+ if(!islist(file_loc))
+ var/list/dirs_and_file = splittext(file_path, "/")
+ var/dir_path = jointext(dirs_and_file, "/", 1, dirs_and_file.len)
+ var/filename = dirs_and_file[dirs_and_file.len]
+ return "cp: [get_terminal_error(filename, dir_path, file_loc)]."
+
+ var/datum/file_storage/disk = file_loc[1]
+ var/datum/computer_file/file = file_loc[3]
+
+ var/datum/computer_file/copy = file.Clone(TRUE)
+ if(!istype(copy))
+ return
+ var/success = disk.store_file(copy, file_loc[2], FALSE, terminal.get_access(user), user)
+ if(success == OS_FILE_SUCCESS)
+ return "cp: Successfully copied file [file.filename]."
- var/copied_file = F.clone(TRUE)
- if(terminal.current_disk.store_file(copied_file))
- return "cp: Successfully copied file [F.filename]."
- else
- return "cp: Could not copy file!"
+ return "cp: [get_terminal_error(file_path, success)]."
/datum/terminal_command/rename
name = "rename"
- man_entry = list("Format: rename \[file name\] \[new name\]", "Renames a file in the current disk.")
+ man_entry = list("Format: rename \[file path\] \[new name\]", "Renames a file with the given path.")
pattern = @"^rename"
/datum/terminal_command/rename/proper_input_entered(text, mob/user, datum/terminal/terminal)
- if(!terminal.current_disk)
- return "rename: No disk selected."
-
var/list/rename_args = get_arguments(text)
-
if(length(rename_args) < 2)
return "rename: Improper syntax, use rename \[file name\] \[new name\]."
- var/datum/computer_file/F = terminal.current_disk.get_file(rename_args[1])
- if(!F)
- return "rename: Could not find file with name [rename_args[1]]."
+ var/file_path = rename_args[1]
+ var/list/file_loc = terminal.parse_file(file_path)
+ // Errored!
+ if(!islist(file_loc))
+ return "rename: [get_terminal_error(file_path, file_loc)]."
- var/new_name = sanitize(rename_args[2])
+ var/datum/computer_file/F = file_loc[3]
+ var/new_name = sanitize_for_file(rename_args[2])
if(length(new_name))
+ if(F.unrenamable || !(F.get_file_perms(terminal.get_access(user), user) & OS_WRITE_ACCESS))
+ return "rename: You lack permission to rename [F.filename]."
F.filename = new_name
- return "rename: File renamed to [new_name]."
+ return "rename: File renamed to '[new_name]'."
else
- return "rename: Could not rename file."
+ return "rename: Invalid file name."
+
+/datum/terminal_command/mkdir
+ name = "mkdir"
+ man_entry = list("Format: mkdir \[dir path\]", "Creates a directory with the given path.")
+ pattern = @"^mkdir"
+
+/datum/terminal_command/mkdir/proper_input_entered(text, mob/user, datum/terminal/terminal)
+ var/list/mkdir_args = get_arguments(text)
+ if(!length(mkdir_args))
+ return "mv: Improper syntax, use mkdir \[dir path\]."
+ var/list/file_loc = terminal.parse_directory(mkdir_args[1], TRUE)
+ if(!islist(file_loc) || (length(file_loc) > 1 && file_loc[2] == null)) // Don't return the error directly since we're attempting to create a directory, not just parse one.
+ return "mkdir: Unable to create directory '[mkdir_args[1]]'."
+
+ var/datum/computer_file/directory/created_dir = file_loc[2]
+ return "mkdir: Successfully created directory '[created_dir.get_file_path()]'."
/datum/terminal_command/target
name = "target"
@@ -503,7 +490,7 @@ Subtypes
/datum/terminal_command/permmod
name = "permmod"
- man_entry = list("Format: permmod \[file name\] \[access key\] \[permission mod flags\]", "Modifies or lists the permissions of the given file. Do not pass an access key to list permissions.",
+ man_entry = list("Format: permmod \[file path\] \[access key\] \[permission mod flags\]", "Modifies or lists the permissions of the given file. Do not pass an access key to list permissions.",
"Supported flags are as follows:",
"'+/-' - Add or Remove access requirement",
"'r/w/m' - Target read/write/modification access",
@@ -520,11 +507,14 @@ Subtypes
var/list/permmod_args = get_arguments(text)
if(!length(permmod_args))
- return "permmod: Improper syntax, use permmod \[file name\] \[access key\] \[permission mod flags\]."
+ return "permmod: Improper syntax, use permmod \[file path\] \[access key\] \[permission mod flags\]."
+
+ var/file_path = permmod_args[1]
+ var/list/file_loc = terminal.parse_file(file_path)
+ if(!islist(file_loc))
+ return "permmod: [get_terminal_error(file_path, file_loc)]."
- var/datum/computer_file/F = terminal.current_disk.get_file(permmod_args[1])
- if(!F)
- return "permmod: Could not find file with name [permmod_args[1]]."
+ var/datum/computer_file/F = file_loc[3]
if(length(permmod_args) < 3)
return F.get_perms_readable()
diff --git a/code/modules/multiz/disabled.dm b/code/modules/multiz/disabled.dm
deleted file mode 100644
index 2214bc0a206..00000000000
--- a/code/modules/multiz/disabled.dm
+++ /dev/null
@@ -1,11 +0,0 @@
-var/global/const/HIGHEST_CONNECTABLE_ZLEVEL_INDEX = 0
-
-proc/HasAbove(var/z)
- return 0
-proc/HasBelow(var/z)
- return 0
-// These give either the turf or null.
-proc/GetAbove(var/turf/turf)
- return null
-proc/GetBelow(var/turf/turf)
- return null
\ No newline at end of file
diff --git a/code/modules/multiz/ladder.dm b/code/modules/multiz/ladder.dm
index 8422de3bc85..50f086ab03e 100644
--- a/code/modules/multiz/ladder.dm
+++ b/code/modules/multiz/ladder.dm
@@ -37,10 +37,14 @@
if(maploading)
for(var/obj/structure/ladder/ladder in loc)
if(ladder != src)
+ log_warning("Deleting duplicate ladder at ([x], [y], [z])!")
qdel(ladder)
- if(HasBelow(z) && (locate(/obj/structure/ladder) in GetBelow(src)))
- var/turf/T = get_turf(src)
+ var/turf/T = get_turf(src)
+ if((locate(/obj/structure/ladder) in GetBelow(src)) && (!(locate(/obj/structure/lattice) in loc) || !T.is_open()))
+ var/old_turf_type = T.type
T.ReplaceWithLattice()
+ //Gonna keep logging those, since it's not clear if it's always a desired behavior. Since mappers would probably not want to rely on this.
+ log_debug("Ladder replaced turf type '[old_turf_type]' at ([x], [y], [z]) with a lattice and open turf '[loc]' of type '[loc.type]'.")
find_connections()
set_extension(src, /datum/extension/turf_hand)
@@ -141,8 +145,10 @@
if(istype(AIeye))
instant_climb(AIeye)
-/obj/structure/ladder/attack_robot(var/mob/M)
- climb(M)
+/obj/structure/ladder/attack_robot(var/mob/user)
+ if(CanPhysicallyInteract(user))
+ climb(user)
+ return TRUE
/obj/structure/ladder/proc/instant_climb(var/mob/M)
var/atom/target_ladder = getTargetLadder(M)
diff --git a/code/modules/multiz/map_data.dm b/code/modules/multiz/map_data.dm
index 6076474f065..8d84bc76b5f 100644
--- a/code/modules/multiz/map_data.dm
+++ b/code/modules/multiz/map_data.dm
@@ -1,13 +1,21 @@
/obj/abstract/map_data
name = "Map Data"
desc = "An unknown location."
- invisibility = 101
var/height = 1 ///< The number of Z-Levels in the map.
var/turf/edge_type ///< What the map edge should be formed with. (null = world.turf)
VAR_PROTECTED/UT_turf_exceptions_by_door_type // An associate list of door types/list of allowed turfs
+#ifdef UNIT_TEST
+// Do not use this in production; for unit tests ONLY.
+/obj/abstract/map_data/proc/get_UT_turf_exceptions_by_door_type()
+ return UT_turf_exceptions_by_door_type
+#else
+/obj/abstract/map_data/proc/get_UT_turf_exceptions_by_door_type()
+ CRASH("map_data.get_UT_turf_exceptions_by_door_type() called in production code!")
+#endif
+
// If the height is more than 1, we mark all contained levels as connected.
// This is in New because it is an auxiliary effect specifically needed pre-init.
/obj/abstract/map_data/New(turf/loc, _height)
diff --git a/code/modules/multiz/movement.dm b/code/modules/multiz/movement.dm
index 66a26ae6a89..c22e982f9c3 100644
--- a/code/modules/multiz/movement.dm
+++ b/code/modules/multiz/movement.dm
@@ -192,7 +192,7 @@
if(locate(/obj/structure/stairs) in landing)
return 1
if(landing.get_fluid_depth() >= FLUID_DEEP)
- var/primary_fluid = landing.reagents.get_primary_reagent_name()
+ var/primary_fluid = landing.get_fluid_name()
if(previous.get_fluid_depth() >= FLUID_DEEP) //We're sinking further
visible_message(SPAN_NOTICE("\The [src] sinks deeper down into \the [primary_fluid]!"), SPAN_NOTICE("\The [primary_fluid] rushes around you as you sink!"))
playsound(previous, pick(SSfluids.gurgles), 50, 1)
diff --git a/code/modules/multiz/pipes.dm b/code/modules/multiz/pipes.dm
index d255b548445..519a945d468 100644
--- a/code/modules/multiz/pipes.dm
+++ b/code/modules/multiz/pipes.dm
@@ -13,9 +13,6 @@
dir = SOUTH
initialize_directions = SOUTH
- var/minimum_temperature_difference = 300
- var/thermal_conductivity = 0 //WALL_HEAT_TRANSFER_COEFFICIENT No
-
level = 1
/obj/machinery/atmospherics/pipe/zpipe/check_pressure(pressure)
@@ -131,7 +128,6 @@
color = PIPE_COLOR_ORANGE
maximum_pressure = 420*ONE_ATMOSPHERE
fatigue_pressure = 350*ONE_ATMOSPHERE
- alert_pressure = 350*ONE_ATMOSPHERE
connect_types = CONNECT_TYPE_FUEL
/obj/machinery/atmospherics/pipe/zpipe/down/fuel
@@ -139,5 +135,4 @@
color = PIPE_COLOR_ORANGE
maximum_pressure = 420*ONE_ATMOSPHERE
fatigue_pressure = 350*ONE_ATMOSPHERE
- alert_pressure = 350*ONE_ATMOSPHERE
connect_types = CONNECT_TYPE_FUEL
diff --git a/code/modules/multiz/zmimic/mimic_docs.dm b/code/modules/multiz/zmimic/mimic_docs.dm
index 8c23059016f..e3d794acafb 100644
--- a/code/modules/multiz/zmimic/mimic_docs.dm
+++ b/code/modules/multiz/zmimic/mimic_docs.dm
@@ -1,5 +1,5 @@
/*
-Types:
+Types (also see terminology section):
openspace/multiplier -> shadows the below level, also copies lighting
openspace/mimic -> copies below movables
openspace/turf_proxy -> holds the appearance of the below turf for non-OVERWRITE Z-turfs
@@ -45,7 +45,57 @@ Public API:
- ZM_MIMIC_NO_AO: normal turf AO should be skipped, only do openspace AO (if your turf is not solid, you probably want this)
- ZM_NO_OCCLUDE: don't block clicking on below atoms if not OVERWRITE
- - movable/no_z_overlay
- - bool
- - Set this to TRUE if you want Z-Mimic to ignore this atom. Atoms with INVISIBLITY_ABSTRACT are automatically ignored; other invisibility values are inherited.
+ - atom/movable/z_flags
+ - bitfield
+ - ZMM_IGNORE: Do not copy this atom. Atoms with INVISIBILITY_ABSTRACT are automatically not copied.
+ - ZMM_MANGLE_PLANES: Scan this atom's overlays and monkeypatch explicit plane sets. Fixes emissive overlays shining through floors, but expensive -- use only if necessary.
+
+Implementation details:
+ Z-Mimic makes some assumptions. While it may continue to work if these are violated, don't be surprised if it behaves strangely, renders things in the incorrect order, or outright breaks.
+
+ Assumptions:
+ - Z-Stacks will not be taller than OPENTURF_MAX_DEPTH.
+ - If violated: Warning emitted on boot, layering may break for items near the bottom of the z-stack.
+ - Atoms will render correctly if copied to another plane.
+ - Atoms will layer correctly if copied to the same plane as other arbitrary in-world atoms.
+ - Atoms without ZMM_MANGLE_PLANES do not have any overlays that have explicit plane sets.
+ - If violated: Atoms on the below floor may be partially visible on the current floor.
+ - Z-Stacks are 1:1 across the entire x/y plane.
+ - If violated: Z-turfs may form nonsensical connections.
+ - Z-Stacks are contiguous and linear -- get_step(UP) corresponds to moving up a z-level (within a z-stack) in all cases.
+ - If violated: layering becomes nonsensical.
+ - Z-Stacks will not be changed (note: adding new Z-stacks is OK) after an openturf has been initialized on that z-stack.
+ - If violated: Z-Turfs may act as if they are still connected even though they are not.
+ - /turf/space is never above another turf type in the Z-Stack.
+ - Turfs that are setting ZM_MIMIC_OVERWRITE do not care about their appearance.
+ - If violated: Appearance of turf is lost.
+ - Multiturf movable atoms are symmetric, and centered on their visual center.
+ - If violated: Multitile atoms may not render in cases where they should.
+ - SHADOWER_DARKENING_FACTOR and SHADOWER_DARKENING_COLOR represent the same shade of grey.
+ - If violated: unlit and lit z-turfs may look inconsistent with each other.
+ - Lighting will mimic correctly without being associated with a plane.
+ - If violated: depending on implementation, lighting may be inverted, or not render at all.
+ - This can usually be addressed by changing /atom/movable/openspace/multiplier/proc/copy_lighting().
+
+ Known Limitations:
+ - Multiturf movable atoms are not rendered if they are not centered on a z-turf, but overlap one.
+ - vis_contents is ignored -- mimics will not copy it.
+
+ Terminology (of varying obscurity):
+ - Z-Stack
+ - A set of z-connected turfs with the same x/y coordinates.
+ - Z-Depth
+ - How many Z-levels this atom is *from the top of a Z-Stack* (absolute layering), regardless of z-turf presence
+ - Shadower / Multiplier
+ - An abstract object used to darken lower levels, copy lighting, and host Z-AO overlays.
+ - Mimic / Openspace Object
+ - An abstract object that holds appearances of atoms and proxies clicks.
+ - Turf Proxy / Turf Object
+ - An abstract object that holds Z-Copy turf appearances for non-OVERWRITE turfs.
+ - Turf Mimic
+ - An abstract object that holds appearances of non-OVERWRITE z-turfs below this z-turf.
+ - Foreign Turf
+ - A turf below this z-turf that is contributing to our appearance.
+ - Mimic Underlay
+ - A turf appearance holder specifically for fake space below a z-turf at the bottom of a z-stack.
*/
diff --git a/code/modules/multiz/zmimic/mimic_movable.dm b/code/modules/multiz/zmimic/mimic_movable.dm
index 71ccd0c163f..4b3c63117b7 100644
--- a/code/modules/multiz/zmimic/mimic_movable.dm
+++ b/code/modules/multiz/zmimic/mimic_movable.dm
@@ -54,6 +54,7 @@
simulated = FALSE
anchored = TRUE
mouse_opacity = FALSE
+ abstract_type = /atom/movable/openspace // unsure if this is valid, check with Lohi
/atom/movable/openspace/can_fall()
return FALSE
@@ -79,17 +80,19 @@
name = "openspace multiplier"
desc = "You shouldn't see this."
icon = 'icons/effects/lighting_overlay.dmi'
- icon_state = "dark"
+ icon_state = "blank"
plane = OPENTURF_MAX_PLANE
layer = MIMICED_LIGHTING_LAYER
blend_mode = BLEND_MULTIPLY
color = SHADOWER_DARKENING_COLOR
-/atom/movable/openspace/multiplier/Destroy()
+/atom/movable/openspace/multiplier/Destroy(force)
+ if(!force)
+ PRINT_STACK_TRACE("Turf shadower improperly qdel'd.")
+ return QDEL_HINT_LETMELIVE
var/turf/myturf = loc
if (istype(myturf))
myturf.shadower = null
-
return ..()
/atom/movable/openspace/multiplier/proc/copy_lighting(atom/movable/lighting_overlay/LO)
@@ -136,6 +139,7 @@
var/mimiced_type
var/original_z
var/override_depth
+ var/have_performed_fixup = FALSE
/atom/movable/openspace/mimic/New()
atom_flags |= ATOM_FLAG_INITIALIZED
diff --git a/code/modules/multiz/zmimic/mimic_turf.dm b/code/modules/multiz/zmimic/mimic_turf.dm
index a456e090ee7..505dbda9fc2 100644
--- a/code/modules/multiz/zmimic/mimic_turf.dm
+++ b/code/modules/multiz/zmimic/mimic_turf.dm
@@ -20,22 +20,15 @@
var/tmp/z_depth
var/tmp/z_generation = 0
-/turf/Entered(atom/movable/thing, turf/oldLoc)
- . = ..()
- if (thing.bound_overlay || (thing.z_flags & ZMM_IGNORE) || !TURF_IS_MIMICKING(above))
- return
- above.update_mimic()
-
/turf/update_above()
if (TURF_IS_MIMICKING(above))
above.update_mimic()
/turf/proc/update_mimic()
- if (!(z_flags & ZM_MIMIC_BELOW))
- return
-
- z_queued += 1
- SSzcopy.queued_turfs += src
+ if(z_flags & ZM_MIMIC_BELOW)
+ z_queued += 1
+ // This adds duplicates for a reason. Do not change this unless you understand how ZM queues work.
+ SSzcopy.queued_turfs += src
/// Enables Z-mimic for a turf that didn't already have it enabled.
/turf/proc/enable_zmimic(additional_flags = 0)
@@ -77,7 +70,10 @@
// Don't remove ourselves from the queue, the subsystem will explode. We'll naturally fall out of the queue.
z_queued = 0
- QDEL_NULL(shadower)
+ // can't use QDEL_NULL as we need to supply force to qdel
+ if(shadower)
+ qdel(shadower, TRUE)
+ shadower = null
QDEL_NULL(mimic_above_copy)
QDEL_NULL(mimic_underlay)
diff --git a/code/modules/nano/modules/law_manager.dm b/code/modules/nano/modules/law_manager.dm
index 523be390d2b..f336559475f 100644
--- a/code/modules/nano/modules/law_manager.dm
+++ b/code/modules/nano/modules/law_manager.dm
@@ -94,7 +94,7 @@
if(href_list["change_supplied_law_position"])
var/new_position = input(usr, "Enter new supplied law position between 1 and [MAX_SUPPLIED_LAW_NUMBER], inclusive. Inherent laws at the same index as a supplied law will not be stated.", "Law Position", supplied_law_position) as num|null
if(isnum(new_position) && can_still_topic())
- supplied_law_position = Clamp(new_position, 1, MAX_SUPPLIED_LAW_NUMBER)
+ supplied_law_position = clamp(new_position, 1, MAX_SUPPLIED_LAW_NUMBER)
return 1
if(href_list["edit_law"])
diff --git a/code/modules/organs/ailments/ailments_medical.dm b/code/modules/organs/ailments/ailments_medical.dm
index 8df018ca3c7..69c53e08a54 100644
--- a/code/modules/organs/ailments/ailments_medical.dm
+++ b/code/modules/organs/ailments/ailments_medical.dm
@@ -91,8 +91,7 @@
return FALSE
/datum/ailment/coughing/on_ailment_event()
- if(organ.owner.usable_emotes["cough"])
- organ.owner.emote("cough")
+ organ.owner.cough()
organ.owner.setClickCooldown(3)
/datum/ailment/sore_joint
diff --git a/code/modules/organs/blood.dm b/code/modules/organs/blood.dm
index e7e5e7f9bc4..2e633ff211e 100644
--- a/code/modules/organs/blood.dm
+++ b/code/modules/organs/blood.dm
@@ -343,7 +343,7 @@
else
blood_volume = 100
- var/blood_volume_mod = max(0, 1 - getOxyLoss()/(species.total_health/2))
+ var/blood_volume_mod = max(0, 1 - getOxyLossPercent()/(species.total_health/2))
var/oxygenated_mult = 0
switch(GET_CHEMICAL_EFFECT(src, CE_OXYGENATED))
if(1)
diff --git a/code/modules/organs/external/_external.dm b/code/modules/organs/external/_external.dm
index e39cf2efd56..d1967950625 100644
--- a/code/modules/organs/external/_external.dm
+++ b/code/modules/organs/external/_external.dm
@@ -452,6 +452,13 @@
// we can't use implanted() here since it's often interactive
imp_device.imp_in = owner
imp_device.implanted = TRUE
+
+ //Since limbs attached during surgery have their internal organs detached, we want to re-attach them if we're doing the proper install of the parent limb
+ else if(istype(implant, /obj/item/organ) && !detached)
+ var/obj/item/organ/O = implant
+ if(O.parent_organ == organ_tag)
+ //The add_organ chain will automatically handle properly removing the detached flag, and moving it to the proper lists
+ owner.add_organ(O, src, in_place, update_icon, detached)
else
//Handle installing into a stand-alone parent limb to keep dropped limbs in some kind of coherent state
if(!affected)
@@ -676,19 +683,28 @@ This function completely restores a damaged organ to perfect condition.
//Determines if we even need to process this organ.
/obj/item/organ/external/proc/need_process()
- if(get_pain())
- return 1
+
if(length(ailments))
- return 1
- if(status & (ORGAN_CUT_AWAY|ORGAN_BLEEDING|ORGAN_BROKEN|ORGAN_DEAD|ORGAN_MUTATED))
- return 1
+ return TRUE
+
+ if(status & (ORGAN_CUT_AWAY|ORGAN_BLEEDING|ORGAN_BROKEN|ORGAN_DEAD|ORGAN_MUTATED|ORGAN_DISLOCATED))
+ return TRUE
+
if((brute_dam || burn_dam) && !BP_IS_PROSTHETIC(src)) //Robot limbs don't autoheal and thus don't need to process when damaged
- return 1
+ return TRUE
+
+ if(get_genetic_damage())
+ return TRUE
+
+ for(var/obj/item/organ/internal/I in internal_organs)
+ if(I.getToxLoss())
+ return TRUE
+
if(last_dam != brute_dam + burn_dam) // Process when we are fully healed up.
last_dam = brute_dam + burn_dam
- return 1
- else
- last_dam = brute_dam + burn_dam
+ return TRUE
+
+ last_dam = brute_dam + burn_dam
if(germ_level)
return 1
return 0
@@ -1241,7 +1257,11 @@ Note that amputating the affected organ does in fact remove the infection from t
if(species)
return species.get_manual_dexterity(owner)
-/obj/item/organ/external/robotize(var/company, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
+//Completely override, so we can slap in the model
+/obj/item/organ/external/setup_as_prosthetic()
+ . = ..(model ? model : /decl/prosthetics_manufacturer/basic_human)
+
+/obj/item/organ/external/robotize(var/company = /decl/prosthetics_manufacturer/basic_human, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
. = ..()
slowdown = 0
@@ -1259,16 +1279,25 @@ Note that amputating the affected organ does in fact remove the infection from t
//Handling for paths
if(!ispath(company))
PRINT_STACK_TRACE("Limb [type] robotize() was supplied a null or non-decl manufacturer: '[company]'")
- company = /decl/prosthetics_manufacturer
+ company = /decl/prosthetics_manufacturer/basic_human
R = GET_DECL(company)
- //If can't install fallback to default
- check_bodytype = (check_bodytype || owner?.get_bodytype_category() || global.using_map.default_bodytype)
- check_species = (check_species || owner?.get_species_name() || global.using_map.default_species)
+ if(!check_species)
+ check_species = owner?.get_species_name() || global.using_map.default_species
+ if(!check_bodytype)
+ if(owner)
+ check_bodytype = owner.get_bodytype_category()
+ else
+ var/decl/species/species_data = get_species_by_key(check_species)
+ if(species_data)
+ check_bodytype = species_data.default_bodytype.bodytype_category
+ else
+ check_bodytype = global.using_map.default_bodytype
+ //If can't install fallback to defaults.
if(!R.check_can_install(organ_tag, check_bodytype, check_species))
- company = /decl/prosthetics_manufacturer
- R = GET_DECL(/decl/prosthetics_manufacturer)
+ company = /decl/prosthetics_manufacturer/basic_human
+ R = GET_DECL(company)
model = company
name = "[R ? R.modifier_string : "robotic"] [initial(name)]"
@@ -1293,7 +1322,7 @@ Note that amputating the affected organ does in fact remove the infection from t
if(!keep_organs)
for(var/obj/item/organ/thing in internal_organs)
- if(!thing.vital && !BP_IS_PROSTHETIC(thing))
+ if(!thing.is_vital_to_owner() && !BP_IS_PROSTHETIC(thing))
qdel(thing)
owner.refresh_modular_limb_verbs()
@@ -1311,11 +1340,17 @@ Note that amputating the affected organ does in fact remove the infection from t
/obj/item/organ/external/is_usable()
. = ..()
- . = . && !is_malfunctioning()
- . = . && (!is_broken() || splinted)
- . = . && !(status & ORGAN_TENDON_CUT)
- . = . && (!can_feel_pain() || get_pain() < pain_disability_threshold)
- . = . && brute_ratio < 1 && burn_ratio < 1
+ if(.)
+ if(is_malfunctioning())
+ return FALSE
+ if(is_broken() && !splinted)
+ return FALSE
+ if(status & ORGAN_TENDON_CUT)
+ return FALSE
+ if(brute_ratio >= 1 || burn_ratio >= 1)
+ return FALSE
+ if(get_pain() >= pain_disability_threshold)
+ return FALSE
/obj/item/organ/external/proc/is_malfunctioning()
return (is_robotic() && (brute_dam + burn_dam) >= 10 && prob(brute_dam + burn_dam))
@@ -1512,7 +1547,7 @@ Note that amputating the affected organ does in fact remove the infection from t
to_chat(owner, "
You feel extreme pain!")
var/max_halloss = round(owner.species.total_health * 0.8 * ((100 - armor) / 100)) //up to 80% of passing out, further reduced by armour
- add_pain(Clamp(0, max_halloss - owner.getHalLoss(), 30))
+ add_pain(clamp(0, max_halloss - owner.getHalLoss(), 30))
//Adds autopsy data for used_weapon.
/obj/item/organ/external/proc/add_autopsy_data(var/used_weapon, var/damage)
@@ -1574,3 +1609,23 @@ Note that amputating the affected organ does in fact remove the infection from t
/obj/item/organ/external/is_internal()
return FALSE
+
+// This likely seems excessive, but refer to organ explosion_act() to see how it should be handled before reaching this point.
+/obj/item/organ/external/physically_destroyed(skip_qdel)
+ if(owner)
+ if(limb_flags & ORGAN_FLAG_CAN_AMPUTATE)
+ dismember(FALSE, DISMEMBER_METHOD_BLUNT)
+ else
+ owner.gib()
+ else
+ return ..()
+
+/obj/item/organ/external/is_vital_to_owner()
+ if(isnull(vital_to_owner))
+ . = ..()
+ if(!.)
+ for(var/obj/item/organ/O in children)
+ if(O.is_vital_to_owner())
+ vital_to_owner = TRUE
+ break
+ return vital_to_owner
diff --git a/code/modules/organs/external/_external_damage.dm b/code/modules/organs/external/_external_damage.dm
index 803dd11a169..096acd2f761 100644
--- a/code/modules/organs/external/_external_damage.dm
+++ b/code/modules/organs/external/_external_damage.dm
@@ -206,7 +206,11 @@
// Geneloss/cloneloss.
/obj/item/organ/external/proc/get_genetic_damage()
- return ((species && (species.species_flags & SPECIES_FLAG_NO_SCAN)) || BP_IS_PROSTHETIC(src)) ? 0 : genetic_degradation
+ if(species?.species_flags & SPECIES_FLAG_NO_SCAN)
+ return 0
+ if(BP_IS_PROSTHETIC(src))
+ return 0
+ return genetic_degradation
/obj/item/organ/external/proc/remove_genetic_damage(var/amount)
if((species.species_flags & SPECIES_FLAG_NO_SCAN) || BP_IS_PROSTHETIC(src))
@@ -247,7 +251,7 @@
// Pain/halloss
/obj/item/organ/external/proc/get_pain()
- if(!can_feel_pain() || BP_IS_PROSTHETIC(src))
+ if(!can_feel_pain())
return 0
var/lasting_pain = 0
if(is_broken())
diff --git a/code/modules/organs/external/_external_icons.dm b/code/modules/organs/external/_external_icons.dm
index 0ee7d296bf9..2f46814770e 100644
--- a/code/modules/organs/external/_external_icons.dm
+++ b/code/modules/organs/external/_external_icons.dm
@@ -4,13 +4,14 @@ var/global/list/limb_icon_cache = list()
return ..(SOUTH)
/obj/item/organ/external/proc/compile_icon()
- overlays.Cut()
+ //#FIXME: We REALLY shouldn't be messing with overlays outside on_update_icon. And on_update_icon doesn't call this.
+ cut_overlays()
// This is a kludge, only one icon has more than one generation of children though.
for(var/obj/item/organ/external/organ in contents)
if(organ.children && organ.children.len)
for(var/obj/item/organ/external/child in organ.children)
- overlays += child.mob_icon
- overlays += organ.mob_icon
+ add_overlay(child.mob_icon)
+ add_overlay(organ.mob_icon)
/obj/item/organ/external/proc/sync_colour_to_human(var/mob/living/carbon/human/human)
skin_tone = null
@@ -59,7 +60,8 @@ var/global/list/limb_icon_cache = list()
if (mark_style.draw_target == MARKING_TARGET_SKIN)
var/icon/mark_s = new/icon("icon" = mark_style.icon, "icon_state" = "[mark_style.icon_state]-[organ_tag]")
mark_s.Blend(markings[M], mark_style.blend)
- overlays |= mark_s //So when it's not on your body, it has icons
+ //#TODO: This probably should be added to a list that's applied on update icon, otherwise its gonna act really wonky!
+ add_overlay(mark_s) //So when it's not on your body, it has icons
mob_icon.Blend(mark_s, mark_style.layer_blend) //So when it's on your body, it has icons
icon_cache_key += "[M][markings[M]]"
@@ -78,6 +80,7 @@ var/global/list/limb_icon_cache = list()
icon = bodytype.get_base_icon(owner)
/obj/item/organ/external/on_update_icon(var/regenerate = 0)
+ . = ..()
icon_state = "[icon_name]"
icon_cache_key = "[icon_state]_[species ? species.name : "unknown"][render_alpha]"
if(model)
@@ -92,7 +95,8 @@ var/global/list/limb_icon_cache = list()
if (mark_style.draw_target == MARKING_TARGET_SKIN)
var/icon/mark_s = new/icon("icon" = mark_style.icon, "icon_state" = "[mark_style.icon_state]-[organ_tag]")
mark_s.Blend(markings[M], mark_style.blend)
- overlays |= mark_s //So when it's not on your body, it has icons
+ //#TODO: This probably should be added to a list that's applied on update icon, otherwise its gonna act really wonky!
+ add_overlay(mark_s) //So when it's not on your body, it has icons
mob_icon.Blend(mark_s, mark_style.layer_blend) //So when it's on your body, it has icons
icon_cache_key += "[M][markings[M]]"
@@ -170,7 +174,7 @@ var/global/list/robot_hud_colours = list("#ffffff","#cccccc","#aaaaaa","#888888"
return applying
/obj/item/organ/external/proc/bandage_level()
- if(damage_state_text() == "00")
+ if(damage_state_text() == "00")
return 0
if(!is_bandaged())
return 0
diff --git a/code/modules/organs/external/head.dm b/code/modules/organs/external/head.dm
index 2e1c10a615c..b16cca2dc67 100644
--- a/code/modules/organs/external/head.dm
+++ b/code/modules/organs/external/head.dm
@@ -76,7 +76,7 @@
/obj/item/organ/external/head/get_agony_multiplier()
return (owner && owner.headcheck(organ_tag)) ? 1.50 : 1
-/obj/item/organ/external/head/robotize(var/company, var/skip_prosthetics = 0, var/keep_organs = 1, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
+/obj/item/organ/external/head/robotize(var/company = /decl/prosthetics_manufacturer/basic_human, var/skip_prosthetics = 0, var/keep_organs = 1, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
. = ..()
has_lips = null
if(model)
@@ -107,11 +107,11 @@
// Floating eyes or other effects.
var/image/eye_glow = get_eye_overlay()
- if(eye_glow)
+ if(eye_glow)
overlays |= eye_glow
if(owner.lip_style && !BP_IS_PROSTHETIC(src) && (species && (species.appearance_flags & HAS_LIPS)))
- var/icon/lip_icon = new/icon(bodytype.get_lip_icon(owner) || 'icons/mob/human_races/species/lips.dmi', "lipstick_s")
+ var/icon/lip_icon = new/icon(bodytype.get_lip_icon(owner) || 'icons/mob/human_races/species/lips.dmi', "lipstick_s")
lip_icon.Blend(owner.lip_style, ICON_MULTIPLY)
overlays |= lip_icon
mob_icon.Blend(lip_icon, ICON_OVERLAY)
@@ -138,7 +138,7 @@
if(owner.h_style)
var/decl/sprite_accessory/hair/hair_style = GET_DECL(owner.h_style)
var/obj/item/head = owner.get_equipped_item(slot_head_str)
- if(head && (head.flags_inv & BLOCKHEADHAIR))
+ if(head && (head.flags_inv & BLOCK_HEAD_HAIR))
if(!(hair_style.flags & VERY_SHORT))
hair_style = GET_DECL(/decl/sprite_accessory/hair/short)
if(hair_style)
diff --git a/code/modules/organs/external/standard.dm b/code/modules/organs/external/standard.dm
index 20f743c1d0e..f9cd1b326c7 100644
--- a/code/modules/organs/external/standard.dm
+++ b/code/modules/organs/external/standard.dm
@@ -13,7 +13,6 @@
w_class = ITEM_SIZE_HUGE //Used for dismembering thresholds, in addition to storage. Humans are w_class 6, so it makes sense that chest is w_class 5.
cavity_max_w_class = ITEM_SIZE_NORMAL
body_part = SLOT_UPPER_BODY
- vital = 1
amputation_point = "spine"
joint = "neck"
parent_organ = null
@@ -31,8 +30,8 @@
if( L && L.is_bruised())
. += "Lung ruptured"
-/obj/item/organ/external/chest/die()
- //Special handling for synthetics
+/obj/item/organ/external/chest/die()
+ //Special handling for synthetics
if(BP_IS_PROSTHETIC(src) || BP_IS_CRYSTAL(src))
return
. = ..()
@@ -53,8 +52,8 @@
cavity_name = "abdominal"
limb_flags = ORGAN_FLAG_CAN_AMPUTATE | ORGAN_FLAG_CAN_BREAK
-/obj/item/organ/external/groin/die()
- //Special handling for synthetics
+/obj/item/organ/external/groin/die()
+ //Special handling for synthetics
if(BP_IS_PROSTHETIC(src) || BP_IS_CRYSTAL(src))
return
. = ..()
@@ -174,4 +173,4 @@
amputation_point = "right wrist"
gripper_ui_loc = ui_rhand
overlay_slot_id = BP_R_HAND
- gripper_ui_label = "R"
\ No newline at end of file
+ gripper_ui_label = "R"
diff --git a/code/modules/organs/external/wounds/wound.dm b/code/modules/organs/external/wounds/wound.dm
index 47e239bc8d8..7bb102416b0 100644
--- a/code/modules/organs/external/wounds/wound.dm
+++ b/code/modules/organs/external/wounds/wound.dm
@@ -41,7 +41,7 @@
// Surgical wounds need to be at minimum big enough to be considered open, which is max_bleeding_stage.
if(surgical)
- damage = max(damage, damage_list[Clamp(max_bleeding_stage, 1, length(damage_list))]+1)
+ damage = max(damage, damage_list[clamp(max_bleeding_stage, 1, length(damage_list))]+1)
src.damage = damage
diff --git a/code/modules/organs/internal/_internal.dm b/code/modules/organs/internal/_internal.dm
index 07d11c92655..c18c0c96a15 100644
--- a/code/modules/organs/internal/_internal.dm
+++ b/code/modules/organs/internal/_internal.dm
@@ -3,6 +3,17 @@
****************************************************/
/obj/item/organ/internal
scale_max_damage_to_species_health = TRUE
+
+ // Damage healing vars (moved here from brains)
+ /// Number of gradiations of damage we can recover in. ie. set to 10, we can recover up to the most recent 10% damage. Leave null to disable regen.
+ var/damage_threshold_count = 5
+ /// Max threshold before we stop regenerating without stabilizer.
+ var/max_regeneration_cutoff_threshold = 3
+ /// Min threshold at which we stop regenerating back to 0 damage. Null/0 to always respect thresholds.
+ var/min_regeneration_cutoff_threshold
+ /// Actual amount of health constituting one gradiation.
+ var/damage_threshold_value
+
var/tmp/alive_icon //icon to use when the organ is alive
var/tmp/dead_icon // Icon to use when the organ has died.
var/tmp/prosthetic_icon //Icon to use when the organ is robotic
@@ -16,6 +27,7 @@
if(!alive_icon)
alive_icon = initial(icon_state)
. = ..()
+ set_max_damage(absolute_max_damage)
/obj/item/organ/internal/set_species(species_name)
. = ..()
@@ -28,7 +40,7 @@
if(!affected)
log_warning("'[src]' called obj/item/organ/internal/do_install(), but its expected parent organ is null!")
- //The organ may only update and etc if its being attached, or isn't cut away.
+ //The organ may only update and etc if its being attached, or isn't cut away.
//Calls up the chain should have set the CUT_AWAY flag already
if(status & ORGAN_CUT_AWAY)
LAZYDISTINCTADD(affected.implants, src) //Add us to the detached organs list
@@ -60,7 +72,7 @@
if((status & ORGAN_CUT_AWAY) && detach)
LAZYDISTINCTADD(affected.implants, src)
else
- LAZYREMOVE(affected.implants, src)
+ LAZYREMOVE(affected.implants, src)
//#TODO: Remove rejuv hacks
/obj/item/organ/internal/remove_rejuv()
@@ -70,7 +82,7 @@
/obj/item/organ/internal/is_usable()
return ..() && !is_broken()
-/obj/item/organ/internal/robotize(var/company, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
+/obj/item/organ/internal/robotize(var/company = /decl/prosthetics_manufacturer/basic_human, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
. = ..()
min_bruised_damage += 5
min_broken_damage += 10
@@ -83,9 +95,6 @@
/obj/item/organ/internal/proc/bruise()
damage = max(damage, min_bruised_damage)
-/obj/item/organ/internal/proc/is_damaged()
- return damage > 0
-
/obj/item/organ/internal/proc/is_bruised()
return damage >= min_bruised_damage
@@ -93,15 +102,17 @@
max_damage = FLOOR(ndamage)
min_broken_damage = FLOOR(0.75 * max_damage)
min_bruised_damage = FLOOR(0.25 * max_damage)
+ if(damage_threshold_count > 0)
+ damage_threshold_value = round(max_damage / damage_threshold_count)
/obj/item/organ/internal/take_general_damage(var/amount, var/silent = FALSE)
take_internal_damage(amount, silent)
/obj/item/organ/internal/proc/take_internal_damage(amount, var/silent=0)
if(BP_IS_PROSTHETIC(src))
- damage = between(0, src.damage + (amount * 0.8), max_damage)
+ damage = clamp(0, src.damage + (amount * 0.8), max_damage)
else
- damage = between(0, src.damage + amount, max_damage)
+ damage = clamp(0, src.damage + amount, max_damage)
//only show this if the organ is not robotic
if(owner && can_feel_pain() && parent_organ && (amount > 5 || prob(10)))
@@ -135,14 +146,56 @@
. = "[.][name]"
/obj/item/organ/internal/Process()
+ SHOULD_CALL_PARENT(TRUE)
..()
- handle_regeneration()
+ if(owner && damage && !(status & ORGAN_DEAD))
+ handle_damage_effects()
-/obj/item/organ/internal/proc/handle_regeneration()
- if(!damage || BP_IS_PROSTHETIC(src) || !owner || GET_CHEMICAL_EFFECT(owner, CE_TOXIN) || owner.is_asystole())
- return
- if(damage < 0.1*max_damage)
- heal_damage(0.1)
+/obj/item/organ/internal/proc/handle_damage_effects()
+ SHOULD_CALL_PARENT(TRUE)
+ if(organ_can_heal())
+
+ // Determine the lowest our damage can go with the current state.
+ // If we're under the min regeneration cutoff threshold, we can always heal to zero.
+ // If we don't have one set, we can only heal to the nearest threshold value.
+ var/min_heal_val = 0
+ if(!min_regeneration_cutoff_threshold || past_damage_threshold(min_regeneration_cutoff_threshold))
+ min_heal_val = (get_current_damage_threshold() * damage_threshold_value)
+
+ // We clamp/round here so that we don't accidentally heal past the threshold and
+ // cheat our way into a full second threshold of healing.
+ damage = clamp(damage-get_organ_heal_amount(), min_heal_val, absolute_max_damage)
+
+ // If we're within 1 damage of the nearest threshold (such as 0), round us down.
+ // This should be removed when float-aware modulo comes in in 515, but for now is needed
+ // as modulo only deals with integers, but organ regeneration is <= 0.3 by default.
+ if(!(damage % damage_threshold_value))
+ damage = round(damage)
+
+/obj/item/organ/internal/proc/get_organ_heal_amount()
+ if(damage >= min_broken_damage)
+ return 0.1
+ if(damage >= min_bruised_damage)
+ return 0.2
+ return 0.3
+
+/obj/item/organ/internal/proc/organ_can_heal()
+ // We cannot regenerate, period.
+ if(!damage_threshold_count || !damage_threshold_value || BP_IS_PROSTHETIC(src))
+ return FALSE
+ // Our owner is under stress.
+ if(owner.get_blood_oxygenation() < BLOOD_VOLUME_SAFE || GET_CHEMICAL_EFFECT(owner, CE_TOXIN) || owner.radiation || owner.is_asystole())
+ return FALSE
+ // If we haven't hit the regeneration cutoff point, heal.
+ if(min_regeneration_cutoff_threshold && !past_damage_threshold(min_regeneration_cutoff_threshold))
+ return TRUE
+ // We have room to heal within this threshold, and we either:
+ // - do not have a max cutoff threshold (point at which no further regeneration will occur)
+ // - are not past our max cutoff threshold
+ // - are dosed with stabilizer (ignores max cutoff threshold)
+ if((damage % damage_threshold_value) && (!max_regeneration_cutoff_threshold || !past_damage_threshold(max_regeneration_cutoff_threshold) || GET_CHEMICAL_EFFECT(owner, CE_STABLE)))
+ return TRUE
+ return FALSE
/obj/item/organ/internal/proc/surgical_fix(mob/user)
if(damage > min_broken_damage)
@@ -150,7 +203,7 @@
scarring = 1 - 0.3 * scarring ** 2 // Between ~15 and 30 percent loss
var/new_max_dam = FLOOR(scarring * max_damage)
if(new_max_dam < max_damage)
- to_chat(user, "
Not every part of [src] could be saved, some dead tissue had to be removed, making it more suspectable to damage in the future.")
+ to_chat(user, SPAN_WARNING("Not every part of [src] could be saved; some dead tissue had to be removed, making it more susceptible to damage in the future."))
set_max_damage(new_max_dam)
heal_damage(damage)
@@ -177,9 +230,30 @@
/obj/item/organ/internal/on_update_icon()
. = ..()
if(BP_IS_PROSTHETIC(src) && prosthetic_icon)
- icon_state = ((status & ORGAN_DEAD) && prosthetic_dead_icon)? prosthetic_dead_icon : prosthetic_icon
+ icon_state = ((status & ORGAN_DEAD) && prosthetic_dead_icon) ? prosthetic_dead_icon : prosthetic_icon
else
- icon_state = ((status & ORGAN_DEAD) && dead_icon)? dead_icon : alive_icon
+ icon_state = ((status & ORGAN_DEAD) && dead_icon) ? dead_icon : alive_icon
/obj/item/organ/internal/is_internal()
return TRUE
+
+// Damage recovery procs! Very exciting.
+/obj/item/organ/internal/proc/get_current_damage_threshold()
+ return damage_threshold_value > 0 ? round(damage / damage_threshold_value) : INFINITY
+
+/obj/item/organ/internal/proc/past_damage_threshold(var/threshold)
+ return (get_current_damage_threshold() > threshold)
+
+/obj/item/organ/internal/on_add_effects()
+ . = ..()
+ if(parent_organ && owner)
+ var/obj/item/organ/O = owner.get_organ(parent_organ)
+ if(O)
+ O.vital_to_owner = null
+
+/obj/item/organ/internal/on_remove_effects(mob/living/last_owner)
+ . = ..()
+ if(parent_organ && last_owner)
+ var/obj/item/organ/O = last_owner.get_organ(parent_organ)
+ if(O)
+ O.vital_to_owner = null
diff --git a/code/modules/organs/internal/brain.dm b/code/modules/organs/internal/brain.dm
index ad0b053c0d7..dd2f3a0afe3 100644
--- a/code/modules/organs/internal/brain.dm
+++ b/code/modules/organs/internal/brain.dm
@@ -3,7 +3,6 @@
desc = "A piece of juicy meat found in a person's head."
organ_tag = BP_BRAIN
parent_organ = BP_HEAD
- vital = 1
icon_state = "brain2"
force = 1.0
w_class = ITEM_SIZE_SMALL
@@ -15,12 +14,9 @@
relative_size = 85
damage_reduction = 0
scale_max_damage_to_species_health = FALSE
-
var/can_use_mmi = TRUE
var/mob/living/carbon/brain/brainmob = null
- var/const/damage_threshold_count = 10
- var/damage_threshold_value
- var/healed_threshold = 1
+ var/should_announce_brain_damage = TRUE
var/oxygen_reserve = 6
/obj/item/organ/internal/brain/getToxLoss()
@@ -33,10 +29,6 @@
else
set_max_damage(200)
-/obj/item/organ/internal/brain/set_max_damage(var/ndamage)
- ..()
- damage_threshold_value = round(max_damage / damage_threshold_count)
-
/obj/item/organ/internal/brain/Destroy()
QDEL_NULL(brainmob)
. = ..()
@@ -64,7 +56,7 @@
to_chat(user, "This one seems particularly lifeless. Perhaps it will regain some of its luster later..")
/obj/item/organ/internal/brain/do_install(mob/living/carbon/target, affected, in_place, update_icon, detached)
- if(!(. = ..()))
+ if(!(. = ..()))
return
if(istype(owner))
SetName(initial(name)) //Reset the organ's name to stay coherent if we're putting it back into someone's skull
@@ -93,15 +85,9 @@
/obj/item/organ/internal/brain/can_recover()
return ~status & ORGAN_DEAD
-/obj/item/organ/internal/brain/proc/get_current_damage_threshold()
- return round(damage / damage_threshold_value)
-
-/obj/item/organ/internal/brain/proc/past_damage_threshold(var/threshold)
- return (get_current_damage_threshold() > threshold)
-
-/obj/item/organ/internal/brain/proc/handle_severe_brain_damage()
+/obj/item/organ/internal/brain/proc/handle_severe_damage()
set waitfor = FALSE
- healed_threshold = 0
+ should_announce_brain_damage = FALSE
to_chat(owner, "
Where am I...?")
sleep(5 SECONDS)
if(!owner)
@@ -113,16 +99,19 @@
to_chat(owner, "
What happened...?")
alert(owner, "You have taken massive brain damage! You will not be able to remember the events leading up to your injury.", "Brain Damaged")
+/obj/item/organ/internal/brain/organ_can_heal()
+ return (damage && GET_CHEMICAL_EFFECT(owner, CE_BRAIN_REGEN)) || ..()
+
+/obj/item/organ/internal/brain/get_organ_heal_amount()
+ return 1
+
/obj/item/organ/internal/brain/Process()
if(owner)
- if(damage > max_damage / 2 && healed_threshold)
- handle_severe_brain_damage()
if(damage < (max_damage / 4))
- healed_threshold = 1
+ should_announce_brain_damage = TRUE
handle_disabilities()
- handle_damage_effects()
// Brain damage from low oxygenation or lack of blood.
if(owner.should_have_organ(BP_HEART))
@@ -136,48 +125,44 @@
oxygen_reserve = min(initial(oxygen_reserve), oxygen_reserve+1)
if(!oxygen_reserve) //(hardcrit)
SET_STATUS_MAX(owner, STAT_PARA, 3)
- var/can_heal = damage && damage < max_damage && (damage % damage_threshold_value || GET_CHEMICAL_EFFECT(owner, CE_BRAIN_REGEN) || (!past_damage_threshold(3) && GET_CHEMICAL_EFFECT(owner, CE_STABLE)))
- var/damprob
+
//Effects of bloodloss
- var/stability_effect = GET_CHEMICAL_EFFECT(owner, CE_STABLE)
- switch(blood_volume)
-
- if(BLOOD_VOLUME_SAFE to INFINITY)
- if(can_heal)
- damage = max(damage-1, 0)
- if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE)
- if(prob(1))
- to_chat(owner, "
You feel [pick("dizzy","woozy","faint")]...")
- damprob = stability_effect ? 30 : 60
- if(!past_damage_threshold(2) && prob(damprob))
- take_internal_damage(1)
- if(BLOOD_VOLUME_BAD to BLOOD_VOLUME_OKAY)
- SET_STATUS_MAX(owner, STAT_BLURRY, 6)
- damprob = stability_effect ? 40 : 80
- if(!past_damage_threshold(4) && prob(damprob))
- take_internal_damage(1)
- if(!HAS_STATUS(owner, STAT_PARA) && prob(10))
- SET_STATUS_MAX(owner, STAT_PARA, rand(1,3))
- to_chat(owner, "
You feel extremely [pick("dizzy","woozy","faint")]...")
- if(BLOOD_VOLUME_SURVIVE to BLOOD_VOLUME_BAD)
- SET_STATUS_MAX(owner, STAT_BLURRY, 6)
- damprob = stability_effect ? 60 : 100
- if(!past_damage_threshold(6) && prob(damprob))
- take_internal_damage(1)
- if(!HAS_STATUS(owner, STAT_PARA) && prob(15))
- SET_STATUS_MAX(owner, STAT_PARA, rand(3,5))
- to_chat(owner, "
You feel extremely [pick("dizzy","woozy","faint")]...")
- if(-(INFINITY) to BLOOD_VOLUME_SURVIVE) // Also see heart.dm, being below this point puts you into cardiac arrest.
- SET_STATUS_MAX(owner, STAT_BLURRY, 6)
- damprob = stability_effect ? 80 : 100
- if(prob(damprob))
- take_internal_damage(1)
- if(prob(damprob))
- take_internal_damage(1)
+ if(blood_volume < BLOOD_VOLUME_SAFE)
+ var/damprob
+ var/stability_effect = GET_CHEMICAL_EFFECT(owner, CE_STABLE)
+ switch(blood_volume)
+ if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE)
+ if(prob(1))
+ to_chat(owner, "
You feel [pick("dizzy","woozy","faint")]...")
+ damprob = stability_effect ? 30 : 60
+ if(!past_damage_threshold(2) && prob(damprob))
+ take_internal_damage(1)
+ if(BLOOD_VOLUME_BAD to BLOOD_VOLUME_OKAY)
+ SET_STATUS_MAX(owner, STAT_BLURRY, 6)
+ damprob = stability_effect ? 40 : 80
+ if(!past_damage_threshold(4) && prob(damprob))
+ take_internal_damage(1)
+ if(!HAS_STATUS(owner, STAT_PARA) && prob(10))
+ SET_STATUS_MAX(owner, STAT_PARA, rand(1,3))
+ to_chat(owner, "
You feel extremely [pick("dizzy","woozy","faint")]...")
+ if(BLOOD_VOLUME_SURVIVE to BLOOD_VOLUME_BAD)
+ SET_STATUS_MAX(owner, STAT_BLURRY, 6)
+ damprob = stability_effect ? 60 : 100
+ if(!past_damage_threshold(6) && prob(damprob))
+ take_internal_damage(1)
+ if(!HAS_STATUS(owner, STAT_PARA) && prob(15))
+ SET_STATUS_MAX(owner, STAT_PARA, rand(3,5))
+ to_chat(owner, "
You feel extremely [pick("dizzy","woozy","faint")]...")
+ if(-(INFINITY) to BLOOD_VOLUME_SURVIVE) // Also see heart.dm, being below this point puts you into cardiac arrest.
+ SET_STATUS_MAX(owner, STAT_BLURRY, 6)
+ damprob = stability_effect ? 80 : 100
+ if(prob(damprob))
+ take_internal_damage(1)
+ if(prob(damprob))
+ take_internal_damage(1)
..()
/obj/item/organ/internal/brain/take_internal_damage(var/damage, var/silent)
- set waitfor = 0
..()
if(damage >= 10) //This probably won't be triggered by oxyloss or mercury. Probably.
var/damage_secondary = damage * 0.20
@@ -210,10 +195,14 @@
else if((owner.disabilities & NERVOUS) && prob(10))
SET_STATUS_MAX(owner, STAT_STUTTER, 10)
-/obj/item/organ/internal/brain/proc/handle_damage_effects()
- if(owner.stat)
- return
- if(damage > 0 && prob(1))
+
+/obj/item/organ/internal/brain/handle_damage_effects()
+ ..()
+
+ if(damage >= round(max_damage / 2) && should_announce_brain_damage)
+ handle_severe_damage()
+
+ if(!BP_IS_PROSTHETIC(src) && prob(1))
owner.custom_pain("Your head feels numb and painful.",10)
if(is_bruised() && prob(1) && !HAS_STATUS(owner, STAT_BLURRY))
to_chat(owner, "
It becomes hard to see for some reason.")
diff --git a/code/modules/organs/internal/eyes.dm b/code/modules/organs/internal/eyes.dm
index c4ada03a211..513f71dbbf2 100644
--- a/code/modules/organs/internal/eyes.dm
+++ b/code/modules/organs/internal/eyes.dm
@@ -110,7 +110,7 @@
verbs -= /obj/item/organ/internal/eyes/proc/change_eye_color
verbs -= /obj/item/organ/internal/eyes/proc/toggle_eye_glow
-/obj/item/organ/internal/eyes/robotize(var/company, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
+/obj/item/organ/internal/eyes/robotize(var/company = /decl/prosthetics_manufacturer/basic_human, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
. = ..()
name = "optical sensor"
icon = 'icons/obj/robot_component.dmi'
@@ -131,7 +131,7 @@
if(!owner || !BP_IS_PROSTHETIC(src))
verbs -= /obj/item/organ/internal/eyes/proc/change_eye_color
- return
+ return
if(owner.incapacitated())
return
@@ -152,7 +152,7 @@
if(!owner || !BP_IS_PROSTHETIC(src))
verbs -= /obj/item/organ/internal/eyes/proc/toggle_eye_glow
- return
+ return
if(owner.incapacitated())
return
diff --git a/code/modules/organs/internal/heart.dm b/code/modules/organs/internal/heart.dm
index d58df75f2e1..3dad81f6fd8 100644
--- a/code/modules/organs/internal/heart.dm
+++ b/code/modules/organs/internal/heart.dm
@@ -15,6 +15,10 @@
var/open
var/list/external_pump
+/obj/item/organ/internal/heart/on_holder_death(var/gibbed)
+ pulse = PULSE_NONE
+ update_icon()
+
/obj/item/organ/internal/heart/open
open = 1
@@ -31,20 +35,20 @@
..()
/obj/item/organ/internal/heart/proc/handle_pulse()
- if(BP_IS_PROSTHETIC(src))
+ if(BP_IS_PROSTHETIC(src) || !owner || owner.vital_organ_missing_time)
pulse = PULSE_NONE //that's it, you're dead (or your metal heart is), nothing can influence your pulse
return
// pulse mod starts out as just the chemical effect amount
var/pulse_mod = GET_CHEMICAL_EFFECT(owner, CE_PULSE)
var/is_stable = GET_CHEMICAL_EFFECT(owner, CE_STABLE)
-
+
// If you have enough heart chemicals to be over 2, you're likely to take extra damage.
if(pulse_mod > 2 && !is_stable)
var/damage_chance = (pulse_mod - 2) ** 2
if(prob(damage_chance))
take_internal_damage(0.5)
-
+
// Now pulse mod is impacted by shock stage and other things too
if(owner.shock_stage > 30)
pulse_mod++
@@ -58,7 +62,7 @@
pulse_mod++
if(owner.status_flags & FAKEDEATH || GET_CHEMICAL_EFFECT(owner, CE_NOPULSE))
- pulse = Clamp(PULSE_NONE + pulse_mod, PULSE_NONE, PULSE_2FAST) //pretend that we're dead. unlike actual death, can be inflienced by meds
+ pulse = clamp(PULSE_NONE + pulse_mod, PULSE_NONE, PULSE_2FAST) //pretend that we're dead. unlike actual death, can be inflienced by meds
return
//If heart is stopped, it isn't going to restart itself randomly.
@@ -74,7 +78,7 @@
return
// Pulse normally shouldn't go above PULSE_2FAST
- pulse = Clamp(PULSE_NORM + pulse_mod, PULSE_SLOW, PULSE_2FAST)
+ pulse = clamp(PULSE_NORM + pulse_mod, PULSE_SLOW, PULSE_2FAST)
// If fibrillation, then it can be PULSE_THREADY
var/fibrillation = oxy <= BLOOD_VOLUME_SURVIVE || (prob(30) && owner.shock_stage > 120)
@@ -182,10 +186,7 @@
owner.drip(blood_max)
/obj/item/organ/internal/heart/proc/is_working()
- if(!is_usable())
- return FALSE
-
- return pulse > PULSE_NONE || BP_IS_PROSTHETIC(src) || (owner.status_flags & FAKEDEATH)
+ return is_usable() && (pulse > PULSE_NONE || BP_IS_PROSTHETIC(src) || (owner.status_flags & FAKEDEATH))
/obj/item/organ/internal/heart/listen()
if(BP_IS_PROSTHETIC(src) && is_working())
@@ -220,5 +221,5 @@
. = ..()
if(!BP_IS_PROSTHETIC(src))
pulse = PULSE_NORM
- else
+ else
pulse = PULSE_NONE
\ No newline at end of file
diff --git a/code/modules/organs/internal/liver.dm b/code/modules/organs/internal/liver.dm
index 858144df71c..907cb8627bb 100644
--- a/code/modules/organs/internal/liver.dm
+++ b/code/modules/organs/internal/liver.dm
@@ -10,6 +10,12 @@
min_broken_damage = 45
max_damage = 70
relative_size = 60
+ // Liver recovers a lot better than most meat.
+ min_regeneration_cutoff_threshold = 2
+ max_regeneration_cutoff_threshold = 5
+
+/obj/item/organ/internal/liver/organ_can_heal()
+ return !GET_CHEMICAL_EFFECT(owner, CE_ALCOHOL) && ..()
/obj/item/organ/internal/liver/Process()
@@ -49,13 +55,6 @@
if(alcotox)
take_internal_damage(alcotox, prob(90)) // Chance to warn them
- // Heal a bit if needed and we're not busy. This allows recovery from low amounts of toxloss.
- if(!alco && !GET_CHEMICAL_EFFECT(owner, CE_TOXIN) && !owner.radiation && damage > 0)
- if(damage < min_broken_damage)
- heal_damage(0.2)
- if(damage < min_bruised_damage)
- heal_damage(0.3)
-
//Blood regeneration if there is some space
owner.regenerate_blood(0.1 + GET_CHEMICAL_EFFECT(owner, CE_BLOODRESTORE))
@@ -66,7 +65,3 @@
owner.adjust_nutrition(-10)
else if(owner.nutrition >= 200)
owner.adjust_nutrition(-3)
-
-//We got it covered in Process with more detailed thing
-/obj/item/organ/internal/liver/handle_regeneration()
- return
\ No newline at end of file
diff --git a/code/modules/organs/internal/lungs.dm b/code/modules/organs/internal/lungs.dm
index e862e25dd6d..c25deb0df09 100644
--- a/code/modules/organs/internal/lungs.dm
+++ b/code/modules/organs/internal/lungs.dm
@@ -23,37 +23,52 @@
var/max_pressure_diff = 60
var/oxygen_deprivation = 0
- var/safe_exhaled_max = 6
var/safe_toxins_max = 0.2
- var/SA_para_min = 1
- var/SA_sleep_min = 5
var/breathing = 0
var/last_successful_breath
var/breath_fail_ratio // How badly they failed a breath. Higher is worse.
-/obj/item/organ/internal/lungs/proc/can_drown()
- return (is_broken() || !has_gills)
+ var/datum/reagents/metabolism/inhaled
+
+/obj/item/organ/internal/lungs/Destroy()
+ QDEL_NULL(inhaled)
+ . = ..()
+
+/obj/item/organ/internal/lungs/initialize_reagents(populate)
+ if(!inhaled)
+ inhaled = new/datum/reagents/metabolism(240, (owner || src), CHEM_INHALE)
+ if(!inhaled.my_atom)
+ inhaled.my_atom = src
+ . = ..()
+
+/obj/item/organ/internal/lungs/do_install(mob/living/carbon/human/target, obj/item/organ/external/affected, in_place)
+ if(!(. = ..()))
+ return
+ inhaled.my_atom = owner
+ inhaled.parent = owner
-/obj/item/organ/internal/lungs/proc/remove_oxygen_deprivation(var/amount)
- var/last_suffocation = oxygen_deprivation
- oxygen_deprivation = min(species.total_health,max(0,oxygen_deprivation - amount))
- return -(oxygen_deprivation - last_suffocation)
+/obj/item/organ/internal/lungs/do_uninstall(in_place, detach, ignore_children)
+ . = ..()
+ if(inhaled)
+ inhaled.my_atom = src
+ inhaled.parent = null
-/obj/item/organ/internal/lungs/proc/add_oxygen_deprivation(var/amount)
- var/last_suffocation = oxygen_deprivation
- oxygen_deprivation = min(species.total_health,max(0,oxygen_deprivation + amount))
- return (oxygen_deprivation - last_suffocation)
+/obj/item/organ/internal/lungs/proc/can_drown()
+ return !has_gills || !is_usable()
-// Returns a percentage value for use by GetOxyloss().
-/obj/item/organ/internal/lungs/proc/get_oxygen_deprivation()
- if(status & ORGAN_DEAD)
- return 100
- return round((oxygen_deprivation/species.total_health)*100)
+/obj/item/organ/internal/lungs/proc/adjust_oxygen_deprivation(var/amount)
+ oxygen_deprivation = clamp(oxygen_deprivation + amount, 0, species.total_health)
/obj/item/organ/internal/lungs/set_species(species_name)
. = ..()
sync_breath_types()
+// This call needs to be split out to make sure that all the ingested things are metabolised
+// before the process call is made on any of the other organs
+/obj/item/organ/internal/lungs/proc/metabolize()
+ if(is_usable())
+ inhaled.metabolize()
+
/**
* Set these lungs' breath types based on the lungs' species
*/
@@ -71,14 +86,19 @@
poison_types = list(/decl/material/gas/chlorine = TRUE)
exhale_type = /decl/material/gas/carbon_dioxide
+
/obj/item/organ/internal/lungs/Process()
..()
if(!owner)
return
+ if(owner.vital_organ_missing_time)
+ owner.losebreath = max(10, owner.losebreath)
+ return
+
if (germ_level > INFECTION_LEVEL_ONE && active_breathing)
if(prob(5))
- owner.emote("cough") //respitory tract infection
+ owner.cough() //respitory tract infection
if(is_bruised() && !owner.is_asystole())
if(prob(2))
@@ -172,12 +192,12 @@
owner.emote("gasp")
else if(prob(20))
to_chat(owner, SPAN_WARNING("It's hard to breathe..."))
- breath_fail_ratio = Clamp(0,(1 - inhale_efficiency + breath_fail_ratio)/2,1)
+ breath_fail_ratio = clamp(0,(1 - inhale_efficiency + breath_fail_ratio)/2,1)
failed_inhale = 1
else
if(breath_fail_ratio && prob(20))
to_chat(owner, SPAN_NOTICE("It gets easier to breathe."))
- breath_fail_ratio = Clamp(0,breath_fail_ratio-0.05,1)
+ breath_fail_ratio = clamp(0,breath_fail_ratio-0.05,1)
owner.oxygen_alert = failed_inhale * 2
@@ -204,9 +224,9 @@
continue
// Little bit of sanity so we aren't trying to add 0.0000000001 units of CO2, and so we don't end up with 99999 units of CO2.
var/reagent_amount = breath.gas[gasname] * REAGENT_UNITS_PER_GAS_MOLE * ratio
- if(reagent_amount < 0.05)
+ if(reagent_amount < MINIMUM_CHEMICAL_VOLUME)
continue
- owner.reagents.add_reagent(gasname, reagent_amount)
+ inhaled.add_reagent(gasname, reagent_amount)
breath.adjust_gas(gasname, -breath.gas[gasname], update = 0) //update after
// Moved after reagent injection so we don't instantly poison ourselves with CO2 or whatever.
@@ -256,13 +276,12 @@
if(breath.temperature <= species.cold_level_1)
if(prob(20))
to_chat(owner, "
You feel your face freezing and icicles forming in your lungs!")
- switch(breath.temperature)
- if(species.cold_level_3 to species.cold_level_2)
- damage = COLD_GAS_DAMAGE_LEVEL_3
- if(species.cold_level_2 to species.cold_level_1)
- damage = COLD_GAS_DAMAGE_LEVEL_2
- else
- damage = COLD_GAS_DAMAGE_LEVEL_1
+ if(breath.temperature < species.cold_level_3)
+ damage = COLD_GAS_DAMAGE_LEVEL_3
+ else if(breath.temperature < species.cold_level_2)
+ damage = COLD_GAS_DAMAGE_LEVEL_2
+ else
+ damage = COLD_GAS_DAMAGE_LEVEL_1
if(prob(20))
owner.apply_damage(damage, BURN, BP_HEAD, used_weapon = "Excessive Cold")
@@ -273,13 +292,12 @@
if(prob(20))
to_chat(owner, "
You feel your face burning and a searing heat in your lungs!")
- switch(breath.temperature)
- if(species.heat_level_1 to species.heat_level_2)
- damage = HEAT_GAS_DAMAGE_LEVEL_1
- if(species.heat_level_2 to species.heat_level_3)
- damage = HEAT_GAS_DAMAGE_LEVEL_2
- else
- damage = HEAT_GAS_DAMAGE_LEVEL_3
+ if(breath.temperature < species.heat_level_2)
+ damage = HEAT_GAS_DAMAGE_LEVEL_1
+ else if(breath.temperature < species.heat_level_3)
+ damage = HEAT_GAS_DAMAGE_LEVEL_2
+ else
+ damage = HEAT_GAS_DAMAGE_LEVEL_3
if(prob(20))
owner.apply_damage(damage, BURN, BP_HEAD, used_weapon = "Excessive Heat")
@@ -322,7 +340,7 @@
. += "[pick("wheezing", "gurgling")] sounds"
var/list/breathtype = list()
- if(get_oxygen_deprivation() > 50)
+ if(owner.getOxyLossPercent() > 50)
breathtype += pick("straining","labored")
if(owner.shock_stage > 50)
breathtype += pick("shallow and rapid")
@@ -336,3 +354,25 @@
/obj/item/organ/internal/lungs/gills
name = "lungs and gills"
has_gills = TRUE
+
+/mob/living/carbon/proc/cough(var/deliberate = FALSE)
+ var/obj/item/organ/internal/lungs/lung = get_organ(BP_LUNGS)
+ if(!lung || !lung.active_breathing || isSynthetic() || stat == DEAD || (deliberate && lastcough + 3 SECONDS > world.time))
+ return
+
+ if(lung.breath_fail_ratio > 0.9 && world.time > lung.last_successful_breath + 2 MINUTES)
+ if(deliberate)
+ to_chat(src, SPAN_WARNING("You try to cough, but no air comes out!"))
+ return
+
+ if(deliberate && incapacitated())
+ to_chat(src, SPAN_WARNING("You cannot do that right now."))
+ return
+
+ audible_message("
[src] coughs!", "You cough!", radio_message = "coughs!") // styled like an emote
+
+ lastcough = world.time
+
+ // Coughing clears out 1-2 reagents from the lungs.
+ if(lung.inhaled.total_volume > 0 && loc)
+ lung.inhaled.splash(loc, rand(1, 2))
\ No newline at end of file
diff --git a/code/modules/organs/internal/posibrain.dm b/code/modules/organs/internal/posibrain.dm
index 860e4b03f0b..cc8aa426536 100644
--- a/code/modules/organs/internal/posibrain.dm
+++ b/code/modules/organs/internal/posibrain.dm
@@ -5,7 +5,6 @@
icon_state = "posibrain"
organ_tag = BP_POSIBRAIN
parent_organ = BP_CHEST
- vital = 0
force = 1.0
w_class = ITEM_SIZE_NORMAL
throwforce = 1
@@ -25,14 +24,9 @@
organ_properties = ORGAN_PROP_PROSTHETIC //triggers robotization on init
scale_max_damage_to_species_health = FALSE
- var/mob/living/silicon/sil_brainmob/brainmob = null
+ var/mob/living/carbon/brain/brainmob = null
var/searching = 0
- var/askDelay = 10 * 60 * 1
- var/list/shackled_verbs = list(
- /obj/item/organ/internal/posibrain/proc/show_laws_brain,
- /obj/item/organ/internal/posibrain/proc/brain_checklaws
- )
- var/shackle = 0
+ var/askDelay = 60 SECONDS
/obj/item/organ/internal/posibrain/Initialize()
. = ..()
@@ -48,6 +42,7 @@
brainmob.real_name = H.real_name
brainmob.dna = H.dna.Clone()
brainmob.add_language(/decl/language/human/common)
+ brainmob.add_language(/decl/language/binary)
/obj/item/organ/internal/posibrain/Destroy()
QDEL_NULL(brainmob)
@@ -55,7 +50,6 @@
/obj/item/organ/internal/posibrain/setup_as_prosthetic()
. = ..()
- unshackle()
update_icon()
/obj/item/organ/internal/posibrain/attack_self(mob/user)
@@ -66,9 +60,9 @@
src.searching = 1
var/decl/ghosttrap/G = GET_DECL(/decl/ghosttrap/positronic_brain)
G.request_player(brainmob, "Someone is requesting a personality for a positronic brain.", 60 SECONDS)
- spawn(600) reset_search()
+ addtimer(CALLBACK(src, .proc/reset_search), askDelay)
-/obj/item/organ/internal/posibrain/proc/reset_search() //We give the players sixty seconds to decide, then reset the timer.
+/obj/item/organ/internal/posibrain/proc/reset_search() //We give the players time to decide, then reset the timer.
if(!brainmob?.key)
searching = FALSE
icon_state = "posibrain"
@@ -90,8 +84,6 @@
var/msg = "
*---------*\nThis is [html_icon(src)] \a
[src]!\n[desc]\n"
- if(shackle) msg += "
It is clamped in a set of metal straps with a complex digital lock.\n"
-
msg += "
"
if(src.brainmob && src.brainmob.key)
@@ -124,29 +116,13 @@
src.brainmob.SetName("[pick(list("PBU","HIU","SINA","ARMA","OSI"))]-[random_id(type,100,999)]")
src.brainmob.real_name = src.brainmob.name
-/obj/item/organ/internal/posibrain/proc/shackle(var/given_lawset)
- if(given_lawset)
- brainmob.laws = given_lawset
- shackle = 1
- verbs |= shackled_verbs
- update_icon()
- return 1
-
-/obj/item/organ/internal/posibrain/proc/unshackle()
- shackle = 0
- verbs -= shackled_verbs
- update_icon()
-
/obj/item/organ/internal/posibrain/on_update_icon()
+ . = ..()
if(src.brainmob && src.brainmob.key)
icon_state = "posibrain-occupied"
else
icon_state = "posibrain"
- overlays.Cut()
- if(shackle)
- overlays |= image('icons/obj/assemblies.dmi', "posibrain-shackles")
-
/obj/item/organ/internal/posibrain/proc/transfer_identity(var/mob/living/carbon/H)
if(H && H.mind)
brainmob.set_stat(CONSCIOUS)
@@ -154,7 +130,6 @@
brainmob.SetName(H.real_name)
brainmob.real_name = H.real_name
brainmob.dna = H.dna.Clone()
- brainmob.show_laws(brainmob)
update_icon()
@@ -187,26 +162,6 @@
SetName("\the [owner.real_name]'s [initial(name)]")
return ..()
-/*
- This is for law stuff directly. This is how a human mob will be able to communicate with the posi_brainmob in the
- posibrain organ for laws when the posibrain organ is shackled.
-*/
-/obj/item/organ/internal/posibrain/proc/show_laws_brain()
- set category = "Shackle"
- set name = "Show Laws"
- set src in usr
-
- brainmob.show_laws(owner)
-
-/obj/item/organ/internal/posibrain/proc/brain_checklaws()
- set category = "Shackle"
- set name = "State Laws"
- set src in usr
-
-
- brainmob.open_subsystem(/datum/nano_module/law_manager, usr)
-
-
/obj/item/organ/internal/cell
name = "microbattery"
desc = "A small, powerful cell for use in fully prosthetic bodies."
@@ -214,7 +169,6 @@
dead_icon = "cell_bork"
organ_tag = BP_CELL
parent_organ = BP_CHEST
- vital = 1
organ_properties = ORGAN_PROP_PROSTHETIC //triggers robotization on init
var/open
var/obj/item/cell/cell = /obj/item/cell/hyper
@@ -312,7 +266,6 @@
icon_state = "mmi-empty"
organ_tag = BP_BRAIN
parent_organ = BP_HEAD
- vital = TRUE
organ_properties = ORGAN_PROP_PROSTHETIC //triggers robotization on init
scale_max_damage_to_species_health = FALSE
var/obj/item/mmi/stored_mmi
diff --git a/code/modules/organs/internal/stomach.dm b/code/modules/organs/internal/stomach.dm
index 6c86a35d921..f54d30964d6 100644
--- a/code/modules/organs/internal/stomach.dm
+++ b/code/modules/organs/internal/stomach.dm
@@ -18,13 +18,15 @@
. = ..()
if(species.gluttonous)
verbs |= /obj/item/organ/internal/stomach/proc/throw_up
+ if(species && !stomach_capacity)
+ stomach_capacity = species.stomach_capacity
-/obj/item/organ/internal/stomach/setup_reagents()
- . = ..()
+/obj/item/organ/internal/stomach/initialize_reagents(populate)
if(!ingested)
ingested = new/datum/reagents/metabolism(240, (owner || src), CHEM_INGEST)
if(!ingested.my_atom)
ingested.my_atom = src
+ . = ..()
/obj/item/organ/internal/stomach/do_uninstall(in_place, detach, ignore_children)
. = ..()
@@ -51,7 +53,7 @@
total += I.get_storage_cost()
else
continue
- if(total > species.stomach_capacity)
+ if(total > stomach_capacity)
return TRUE
return FALSE
@@ -86,14 +88,8 @@
/obj/item/organ/internal/stomach/return_air()
return null
-// This call needs to be split out to make sure that all the ingested things are metabolised
-// before the process call is made on any of the other organs
-/obj/item/organ/internal/stomach/proc/metabolize()
- if(is_usable())
- ingested.metabolize()
-
#define STOMACH_VOLUME 65
-
+
/obj/item/organ/internal/stomach/Process()
..()
@@ -121,14 +117,14 @@
owner.custom_pain("Your stomach cramps agonizingly!",1)
var/alcohol_volume = REAGENT_VOLUME(ingested, /decl/material/liquid/ethanol)
-
+
var/alcohol_threshold_met = alcohol_volume > STOMACH_VOLUME / 2
if(alcohol_threshold_met && (owner.disabilities & EPILEPSY) && prob(20))
owner.seizure()
-
+
// Alcohol counts as double volume for the purposes of vomit probability
var/effective_volume = ingested.total_volume + alcohol_volume
-
+
// Just over the limit, the probability will be low. It rises a lot such that at double ingested it's 64% chance.
var/vomit_probability = (effective_volume / STOMACH_VOLUME) ** 6
if(prob(vomit_probability))
diff --git a/code/modules/organs/organ.dm b/code/modules/organs/organ.dm
index a202ebb064b..0772d5730de 100644
--- a/code/modules/organs/organ.dm
+++ b/code/modules/organs/organ.dm
@@ -7,6 +7,7 @@
material = /decl/material/solid/meat
origin_tech = "{'materials':1,'biotech':1}"
throwforce = 2
+ abstract_type = /obj/item/organ
// Strings.
var/organ_tag = "organ" // Unique identifier.
@@ -15,7 +16,7 @@
// Status tracking.
var/status = 0 // Various status flags (such as robotic)
var/organ_properties = 0 // A flag for telling what capabilities this organ has. ORGAN_PROP_PROSTHETIC, ORGAN_PROP_CRYSTAL, etc..
- var/vital // Lose a vital limb, die immediately.
+ var/vital_to_owner // Cache var for vitality to current owner.
// Reference data.
var/mob/living/carbon/human/owner // Current mob owning the organ.
@@ -45,6 +46,9 @@
QDEL_NULL_LIST(ailments)
return ..()
+/obj/item/organ/proc/on_holder_death(var/gibbed)
+ return
+
/obj/item/organ/proc/refresh_action_button()
return action
@@ -71,8 +75,8 @@
if(!given_dna)
if(dna)
given_dna = dna //Use existing if possible
- else if(owner)
- if(owner.dna)
+ else if(owner)
+ if(owner.dna)
given_dna = owner.dna //Grab our owner's dna if we don't have any, and they have
else
//The owner having no DNA can be a valid reason to keep our dna null in some cases
@@ -83,14 +87,14 @@
//If we have NO OWNER and given_dna, just make one up for consistency
given_dna = new/datum/dna()
given_dna.check_integrity() //Defaults everything
-
+
set_dna(given_dna)
- setup_reagents()
+ initialize_reagents()
return TRUE
//Allows specialization of roboticize() calls on initialization meant to be used when loading prosthetics
// NOTE: This wouldn't be necessary if prothetics were a subclass
-/obj/item/organ/proc/setup_as_prosthetic()
+/obj/item/organ/proc/setup_as_prosthetic(var/forced_model = /decl/prosthetics_manufacturer/basic_human)
if(!species)
if(owner?.species)
set_species(owner.species)
@@ -98,16 +102,20 @@
set_species(global.using_map.default_species)
if(istype(material))
- robotize(apply_material = material.type)
- else
- robotize()
+ robotize(forced_model, apply_material = material.type)
+ else
+ robotize(forced_model)
return TRUE
//Called on initialization to add the neccessary reagents
-/obj/item/organ/proc/setup_reagents()
+
+/obj/item/organ/initialize_reagents(populate = TRUE)
if(reagents)
return
create_reagents(5 * (w_class-1)**2)
+ . = ..()
+
+/obj/item/organ/populate_reagents()
reagents.add_reagent(/decl/material/liquid/nutriment/protein, reagents.maximum_volume)
/obj/item/organ/proc/set_dna(var/datum/dna/new_dna)
@@ -120,10 +128,11 @@
set_species(dna.species)
/obj/item/organ/proc/set_species(var/specie_name)
+ vital_to_owner = null // This generally indicates the owner mob is having species set, and this value may be invalidated.
if(istext(specie_name))
species = get_species_by_key(specie_name)
else
- species = specie_name
+ species = specie_name
if(!species)
species = get_species_by_key(global.using_map.default_species)
PRINT_STACK_TRACE("Invalid species. Expected a valid species name as string, was: [log_info_line(specie_name)]")
@@ -155,14 +164,13 @@
STOP_PROCESSING(SSobj, src)
QDEL_NULL_LIST(ailments)
death_time = REALTIMEOFDAY
- if(owner?.species?.is_vital_organ(owner, src))
- owner.death()
update_icon()
/obj/item/organ/Process()
if(loc != owner) //#FIXME: looks like someone was trying to hide a bug :P That probably could break organs placed inside a wrapper though
owner = null
+ vital_to_owner = null
//dead already, no need for more processing
if(status & ORGAN_DEAD)
@@ -204,7 +212,7 @@
/obj/item/organ/proc/handle_ailment(var/datum/ailment/ailment)
if(ailment.treated_by_reagent_type)
- for(var/datum/reagents/source in list(owner.get_injected_reagents(), owner.reagents, owner.get_ingested_reagents()))
+ for(var/datum/reagents/source as anything in owner.get_metabolizing_reagent_holders())
for(var/reagent_type in source.reagent_volumes)
if(ailment.treated_by_medication(source.reagent_volumes[reagent_type]))
ailment.was_treated_by_medication(source, reagent_type)
@@ -239,13 +247,13 @@
//aiming for germ level to go from ambient to INFECTION_LEVEL_TWO in an average of 15 minutes, when immunity is full.
if(antibiotics < 5 && prob(round(germ_level/6 * owner.immunity_weakness() * 0.01)))
if(germ_immunity > 0)
- germ_level += Clamp(round(1/germ_immunity), 1, 10) // Immunity starts at 100. This doubles infection rate at 50% immunity. Rounded to nearest whole.
+ germ_level += clamp(round(1/germ_immunity), 1, 10) // Immunity starts at 100. This doubles infection rate at 50% immunity. Rounded to nearest whole.
else // Will only trigger if immunity has hit zero. Once it does, 10x infection rate.
germ_level += 10
if(germ_level >= INFECTION_LEVEL_ONE)
var/fever_temperature = (owner.species.heat_level_1 - owner.species.body_temperature - 5)* min(germ_level/INFECTION_LEVEL_TWO, 1) + owner.species.body_temperature
- owner.bodytemperature += between(0, (fever_temperature - T20C)/BODYTEMP_COLD_DIVISOR + 1, fever_temperature - owner.bodytemperature)
+ owner.bodytemperature += clamp(0, (fever_temperature - T20C)/BODYTEMP_COLD_DIVISOR + 1, fever_temperature - owner.bodytemperature)
if (germ_level >= INFECTION_LEVEL_TWO)
var/obj/item/organ/external/parent = GET_EXTERNAL_ORGAN(owner, parent_organ)
@@ -325,10 +333,11 @@
CRASH("Not Implemented")
/obj/item/organ/proc/heal_damage(amount)
- if (can_recover())
- damage = between(0, damage - round(amount, 0.1), max_damage)
+ if(can_recover())
+ damage = clamp(0, damage - round(amount, 0.1), max_damage)
-/obj/item/organ/proc/robotize(var/company, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
+/obj/item/organ/proc/robotize(var/company = /decl/prosthetics_manufacturer/basic_human, var/skip_prosthetics = 0, var/keep_organs = 0, var/apply_material = /decl/material/solid/metal/steel, var/check_bodytype, var/check_species)
+ vital_to_owner = null
BP_SET_PROSTHETIC(src)
QDEL_NULL(dna)
reagents?.clear_reagents()
@@ -499,8 +508,9 @@ var/global/list/ailment_reference_cache = list()
/obj/item/organ/proc/do_install(var/mob/living/carbon/human/target, var/obj/item/organ/external/affected, var/in_place = FALSE, var/update_icon = TRUE, var/detached = FALSE)
//Make sure to force the flag accordingly
set_detached(detached)
-
+
owner = target
+ vital_to_owner = null
action_button_name = initial(action_button_name)
if(owner)
forceMove(owner)
@@ -513,10 +523,10 @@ var/global/list/ailment_reference_cache = list()
//Handles uninstalling the organ from its owner and parent limb, without triggering effects or deep updates
//CASES:
-// 1. Before deletion to clear our references.
+// 1. Before deletion to clear our references.
// 2. Called through removal on surgery or dismemberement
// 3. Called when we're changing a mob's species.
-//detach: If detach is true, we're going to set the organ to detached, and add it to the detached organs list, and remove it from processing lists.
+//detach: If detach is true, we're going to set the organ to detached, and add it to the detached organs list, and remove it from processing lists.
// If its false, we just remove the organ from all lists
/obj/item/organ/proc/do_uninstall(var/in_place = FALSE, var/detach = FALSE, var/ignore_children = FALSE, var/update_icon = TRUE)
action_button_name = null
@@ -526,21 +536,24 @@ var/global/list/ailment_reference_cache = list()
if(ailment.timer_id)
deltimer(ailment.timer_id)
ailment.timer_id = null
-
+
//When we detach, we set the ORGAN_CUT_AWAY flag on, depending on whether the organ supports it or not
if(detach)
set_detached(TRUE)
- else
+ else
owner = null
+ vital_to_owner = null
return src
//Events handling for checks and effects that should happen when removing the organ through interactions. Called by the owner mob.
/obj/item/organ/proc/on_remove_effects(var/mob/living/last_owner)
START_PROCESSING(SSobj, src)
+ vital_to_owner = null
//Events handling for checks and effects that should happen when installing the organ through interactions. Called by the owner mob.
/obj/item/organ/proc/on_add_effects()
STOP_PROCESSING(SSobj, src)
+ vital_to_owner = null
//Since some types of organs completely ignore being detached, moved it to an overridable organ proc for external prosthetics
/obj/item/organ/proc/set_detached(var/is_detached)
@@ -552,3 +565,15 @@ var/global/list/ailment_reference_cache = list()
//Some checks to avoid doing type checks for nothing
/obj/item/organ/proc/is_internal()
return FALSE
+
+// If an organ is inside a holder, the holder should be handling damage in their explosion_act() proc.
+/obj/item/organ/explosion_act(severity)
+ return !owner && ..()
+
+/obj/item/organ/proc/is_vital_to_owner()
+ if(isnull(vital_to_owner))
+ if(!owner?.species)
+ vital_to_owner = null
+ return FALSE
+ vital_to_owner = (organ_tag in owner.species.vital_organs)
+ return vital_to_owner
diff --git a/code/modules/organs/prosthetics/_prosthetics.dm b/code/modules/organs/prosthetics/_prosthetics.dm
index bd53e0070ea..9bea6ef346c 100644
--- a/code/modules/organs/prosthetics/_prosthetics.dm
+++ b/code/modules/organs/prosthetics/_prosthetics.dm
@@ -18,7 +18,7 @@
// Checks if a limb could theoretically be removed.
// Note that this does not currently bother checking if a child or internal organ is vital.
/obj/item/organ/external/proc/can_remove_modular_limb(var/mob/living/carbon/human/user)
- if(vital || !(limb_flags & ORGAN_FLAG_CAN_AMPUTATE))
+ if((owner?.species && is_vital_to_owner()) || !(limb_flags & ORGAN_FLAG_CAN_AMPUTATE))
return FALSE
var/bodypart_cat = get_modular_limb_category()
if(bodypart_cat == MODULAR_BODYPART_CYBERNETIC)
@@ -55,7 +55,7 @@
. = damage >= min_broken_damage || (status & ORGAN_BROKEN) // can't use is_broken() as the limb has ORGAN_CUT_AWAY
// Human mob procs:
-// Checks the organ list for limbs meeting a predicate. Way overengineered for such a limited use
+// Checks the organ list for limbs meeting a predicate. Way overengineered for such a limited use
// case but I can see it being expanded in the future if meat limbs or doona limbs use it.
/mob/living/carbon/human/proc/get_modular_limbs(var/return_first_found = FALSE, var/validate_proc)
for(var/bp in get_external_organs())
@@ -63,7 +63,7 @@
if(!validate_proc || call(E, validate_proc)(src) > MODULAR_BODYPART_INVALID)
LAZYADD(., E)
if(return_first_found)
- return
+ return
// Prune children so we can't remove every individual component of an entire prosthetic arm
// piece by piece. Technically a circular dependency here would remove the limb entirely but
// if there's a parent whose child is also its parent, there's something wrong regardless.
diff --git a/code/modules/organs/prosthetics/prosthetics_manufacturer.dm b/code/modules/organs/prosthetics/prosthetics_manufacturer.dm
index 64a7a6b0f41..44180d4c4f9 100644
--- a/code/modules/organs/prosthetics/prosthetics_manufacturer.dm
+++ b/code/modules/organs/prosthetics/prosthetics_manufacturer.dm
@@ -1,8 +1,8 @@
/decl/prosthetics_manufacturer
- var/name = "Unbranded" // Shown when selecting the limb.
+ abstract_type = /decl/prosthetics_manufacturer
+ var/name // Shown when selecting the limb.
var/desc = "A generic unbranded robotic prosthesis." // Seen when examining a limb.
var/icon = 'icons/mob/human_races/cyberlimbs/robotic.dmi' // Icon base to draw from.
- var/unavailable_at_chargen // If set, not available at chargen.
var/can_eat // Determines if heads with this model can ingest food/drink.
var/has_eyes = TRUE // Determines if eyes should render on heads using this model.
var/can_feel_pain // Modifies the return from human can_feel_pain().
@@ -11,7 +11,7 @@
var/list/bodytypes_cannot_use // Blacklists bodytypes from using this limb.
var/list/species_restricted // Determines which species can use this limb.
var/list/applies_to_part // Determines which bodyparts can use this limb.
- var/list/allowed_bodytypes = list(BODYTYPE_HUMANOID) // Determines which bodytypes can apply the limb.
+ var/list/allowed_bodytypes // Determines which bodytypes can apply the limb.
var/modifier_string = "robotic" // Used to alter the name of the limb.
var/hardiness = 1 // Modifies min and max broken damage for the limb.
var/manual_dexterity = DEXTERITY_FULL // For hands, determines the dexterity value passed to get_dexterity().
diff --git a/code/modules/organs/prosthetics/prosthetics_manufacturer_models.dm b/code/modules/organs/prosthetics/prosthetics_manufacturer_models.dm
index 185b8dd9e37..6be162eb64a 100644
--- a/code/modules/organs/prosthetics/prosthetics_manufacturer_models.dm
+++ b/code/modules/organs/prosthetics/prosthetics_manufacturer_models.dm
@@ -1,3 +1,7 @@
+/decl/prosthetics_manufacturer/basic_human
+ name = "Unbranded"
+ allowed_bodytypes = list(BODYTYPE_HUMANOID)
+
/decl/prosthetics_manufacturer/wooden
name = "crude wooden"
desc = "A crude wooden prosthetic."
@@ -8,5 +12,6 @@
movement_slowdown = 1
is_robotic = FALSE
modular_prosthetic_tier = MODULAR_BODYPART_PROSTHETIC
+ allowed_bodytypes = list(BODYTYPE_HUMANOID)
DEFINE_ROBOLIMB_MODEL_ASPECTS(/decl/prosthetics_manufacturer/wooden, pirate, 0)
diff --git a/code/modules/overmap/_overmap.dm b/code/modules/overmap/_overmap.dm
index 0af4681f70e..db782b45375 100644
--- a/code/modules/overmap/_overmap.dm
+++ b/code/modules/overmap/_overmap.dm
@@ -12,7 +12,6 @@
var/map_area_type = /area/overmap
var/list/valid_event_types
- var/empty_z_level_turf
/datum/overmap/New(var/_name)
@@ -144,7 +143,4 @@
res.forceMove(locate(x, y, assigned_z))
return res
- return new /obj/effect/overmap/visitable/sector/temporary(null, x, y, create_empty_z_level())
-
-/datum/overmap/proc/create_empty_z_level()
- . = get_empty_zlevel(map_turf_type)
+ return new /obj/effect/overmap/visitable/sector/temporary(null, x, y, get_empty_zlevel(map_turf_type))
diff --git a/code/modules/overmap/contacts/_contacts.dm b/code/modules/overmap/contacts/_contacts.dm
index eecd7f18214..6caa410e957 100644
--- a/code/modules/overmap/contacts/_contacts.dm
+++ b/code/modules/overmap/contacts/_contacts.dm
@@ -21,27 +21,35 @@
owner.contact_datums[effect] = src
marker = new(loc = effect)
- marker.appearance = effect
- marker.alpha = 0 // Marker fades in on detection.
+ update_marker_icon()
+ marker.alpha = 0 // Marker fades in on detection.
marker.appearance_flags |= RESET_TRANSFORM
images += marker
-
+
radar = image(loc = effect, icon = 'icons/obj/overmap.dmi', icon_state = "sensor_range")
+ radar.color = source.color
radar.tag = "radar"
- radar.filters = filter(type="blur", size = 1)
+ radar.add_filter("blur", 1, list("blur", size = 1))
+
+/datum/overmap_contact/proc/update_marker_icon()
+
+ marker.appearance = effect
+ marker.appearance_flags |= RESET_TRANSFORM
+ // Pixel offsets are included in appearance but since this marker's loc
+ // is the effect, it's already offset and we don't want to double it.
+ marker.pixel_x = 0
+ marker.pixel_y = 0
-/datum/overmap_contact/proc/update_marker_icon(var/range = 0)
- marker.icon_state = effect.icon_state
- marker.dir = effect.dir
marker.transform = effect.transform
marker.overlays.Cut()
-
if(check_effect_shield())
var/image/shield_image = image(icon = 'icons/obj/overmap.dmi', icon_state = "shield")
shield_image.pixel_x = 8
marker.overlays += shield_image
+/datum/overmap_contact/proc/ping_radar(var/range = 0)
+
radar.transform = null
radar.alpha = 255
@@ -69,7 +77,7 @@
if(!visitable_effect || !istype(visitable_effect))
return FALSE
for(var/thing in visitable_effect.get_linked_machines_of_type(/obj/machinery/shield_generator))
- var/obj/machinery/shield_generator/S = thing
+ var/obj/machinery/shield_generator/S = thing
if(S.running == SHIELD_RUNNING)
return TRUE
return FALSE
diff --git a/code/modules/overmap/contacts/contact_sensors.dm b/code/modules/overmap/contacts/contact_sensors.dm
index 929e701b6d7..c0e181b8112 100644
--- a/code/modules/overmap/contacts/contact_sensors.dm
+++ b/code/modules/overmap/contacts/contact_sensors.dm
@@ -48,7 +48,8 @@
if(sensors?.use_power)
sensor_range = round(sensors.range,1)
var/datum/overmap_contact/self_record = contact_datums[linked]
- self_record.update_marker_icon(sensor_range)
+ self_record.update_marker_icon()
+ self_record.ping_radar(sensor_range)
self_record.show()
// Update our 'sensor range' (ie. overmap lighting)
diff --git a/code/modules/overmap/exoplanets/_exoplanet.dm b/code/modules/overmap/exoplanets/_exoplanet.dm
index cf45737a78e..5e1fe842c73 100644
--- a/code/modules/overmap/exoplanets/_exoplanet.dm
+++ b/code/modules/overmap/exoplanets/_exoplanet.dm
@@ -4,6 +4,7 @@
icon_state = "globe"
sector_flags = OVERMAP_SECTOR_KNOWN
free_landing = TRUE
+
var/area/planetary_area
var/lightlevel = 0 //This default makes turfs not generate light. Adjust to have exoplanents be lit.
@@ -21,7 +22,6 @@
var/x_size
var/y_size
- var/landmark_type = /obj/effect/shuttle_landmark/automatic
var/shuttle_size = 20 //'diameter' of expected shuttle in turfs
var/landing_points_to_place // number of landing points to place, calculated dynamically based on planet size
@@ -155,6 +155,7 @@
return engravings
/obj/effect/overmap/visitable/sector/exoplanet/Process(wait, tick)
+
if(animals.len < 0.5*max_animal_count && !repopulating)
repopulating = 1
max_animal_count = round(max_animal_count * 0.5)
@@ -163,10 +164,11 @@
handle_repopulation()
if(daycycle)
+ wait = max(1, wait)
if(tick % round(daycycle / wait) == 0)
night = !night
daycolumn = 1
- if(daycolumn && tick % round(daycycle_column_delay / wait) == 0)
+ if(daycolumn && (tick % round(daycycle_column_delay / wait)) == 0)
update_daynight()
/obj/effect/overmap/visitable/sector/exoplanet/proc/update_daynight()
@@ -296,5 +298,4 @@
ambience = list('sound/effects/wind/wind_2_1.ogg','sound/effects/wind/wind_2_2.ogg','sound/effects/wind/wind_3_1.ogg','sound/effects/wind/wind_4_1.ogg','sound/effects/wind/wind_4_2.ogg','sound/effects/wind/wind_5_1.ogg')
always_unpowered = 1
area_flags = AREA_FLAG_IS_BACKGROUND | AREA_FLAG_EXTERNAL
- show_starlight = TRUE
is_outside = OUTSIDE_YES
diff --git a/code/modules/overmap/exoplanets/exoplanet_atmosphere.dm b/code/modules/overmap/exoplanets/exoplanet_atmosphere.dm
index 26a263a566d..17e75d2b48c 100644
--- a/code/modules/overmap/exoplanets/exoplanet_atmosphere.dm
+++ b/code/modules/overmap/exoplanets/exoplanet_atmosphere.dm
@@ -5,7 +5,7 @@
//Make sure temperature can't damage people on casual planets
if(habitability_class <= HABITABILITY_OKAY)
var/decl/species/S = get_species_by_key(global.using_map.default_species)
- target_temp = Clamp(target_temp, S.cold_level_1 + rand(1,5), S.heat_level_1 - rand(1,5))
+ target_temp = clamp(target_temp, S.cold_level_1 + rand(1,5), S.heat_level_1 - rand(1,5))
atmosphere.temperature = target_temp
@@ -15,9 +15,9 @@
atmosphere.adjust_gas(/decl/material/gas/nitrogen, MOLES_N2STANDARD)
atmosphere.check_tile_graphic()
return
-
+
var/total_moles = MOLES_CELLSTANDARD
-
+
//Add the non-negotiable gasses
var/badflag = 0
var/gas_list = get_mandatory_gasses()
@@ -37,8 +37,6 @@
var/list/all_materials = decls_repository.get_decls_of_subtype(/decl/material)
for(var/mat_type in all_materials)
var/decl/material/mat = all_materials[mat_type]
- if(mat.is_abstract())
- continue
if(mat.exoplanet_rarity == MAT_RARITY_NOWHERE)
continue
if(isnull(mat.boiling_point) || mat.boiling_point > target_temp)
diff --git a/code/modules/overmap/exoplanets/exoplanet_fauna.dm b/code/modules/overmap/exoplanets/exoplanet_fauna.dm
index a9397ce7d6c..553b80473ac 100644
--- a/code/modules/overmap/exoplanets/exoplanet_fauna.dm
+++ b/code/modules/overmap/exoplanets/exoplanet_fauna.dm
@@ -31,7 +31,7 @@
A.verbs |= /mob/living/simple_animal/proc/name_species
if(atmosphere)
//Set up gases for living things
- var/list/all_gasses = subtypesof(/decl/material/gas)
+ var/list/all_gasses = decls_repository.get_decl_paths_of_subtype(/decl/material/gas)
if(!LAZYLEN(breathgas))
var/list/goodgases = all_gasses.Copy()
var/gasnum = min(rand(1,3), goodgases.len)
diff --git a/code/modules/overmap/exoplanets/exoplanet_flora.dm b/code/modules/overmap/exoplanets/exoplanet_flora.dm
index b183986ec9b..7d07bd6dfb0 100644
--- a/code/modules/overmap/exoplanets/exoplanet_flora.dm
+++ b/code/modules/overmap/exoplanets/exoplanet_flora.dm
@@ -65,11 +65,11 @@
/obj/abstract/landmark/exoplanet_spawn/plant/do_spawn(var/obj/effect/overmap/visitable/sector/exoplanet/planet)
if(LAZYLEN(planet.small_flora_types))
- new /obj/machinery/portable_atmospherics/hydroponics/soil/invisible(get_turf(src), pick(planet.small_flora_types), 1)
+ new /obj/structure/flora/plant(get_turf(src), null, null, pick(planet.small_flora_types))
/obj/abstract/landmark/exoplanet_spawn/large_plant
name = "spawn exoplanet large plant"
/obj/abstract/landmark/exoplanet_spawn/large_plant/do_spawn(var/obj/effect/overmap/visitable/sector/exoplanet/planet)
if(LAZYLEN(planet.big_flora_types))
- new /obj/machinery/portable_atmospherics/hydroponics/soil/invisible(get_turf(src), pick(planet.big_flora_types), 1)
\ No newline at end of file
+ new /obj/structure/flora/plant(get_turf(src), null, null, pick(planet.big_flora_types))
\ No newline at end of file
diff --git a/code/modules/overmap/exoplanets/exoplanet_ruins.dm b/code/modules/overmap/exoplanets/exoplanet_ruins.dm
index f1238d1ebda..7f2fa999526 100644
--- a/code/modules/overmap/exoplanets/exoplanet_ruins.dm
+++ b/code/modules/overmap/exoplanets/exoplanet_ruins.dm
@@ -3,13 +3,13 @@ var/global/list/banned_ruin_names = list()
/obj/effect/overmap/visitable/sector/exoplanet/proc/seed_ruins(list/z_levels = null, budget = 0, whitelist = /area/space, list/potentialRuins, var/maxx = world.maxx, var/maxy = world.maxy)
if(!z_levels || !z_levels.len)
- UNLINT(WARNING("No Z levels provided - Not generating ruins"))
+ WARNING("No Z levels provided - Not generating ruins")
return
for(var/zl in z_levels)
var/turf/T = locate(1, 1, zl)
if(!T)
- UNLINT(WARNING("Z level [zl] does not exist - Not generating ruins"))
+ WARNING("Z level [zl] does not exist - Not generating ruins")
return
var/list/ruins = potentialRuins.Copy()
diff --git a/code/modules/overmap/exoplanets/planet_types/meat.dm b/code/modules/overmap/exoplanets/planet_types/meat.dm
index 8b5e09a8fd7..b9c19313ef6 100644
--- a/code/modules/overmap/exoplanets/planet_types/meat.dm
+++ b/code/modules/overmap/exoplanets/planet_types/meat.dm
@@ -66,7 +66,7 @@
desc = "It's disgustingly soft to the touch. And warm. Too warm."
dirt_color = "#c40031"
footstep_type = /decl/footsteps/mud
-
+
/turf/exterior/water/stomach
name = "juices"
desc = "Half-digested chunks of vines are floating in the puddle of some liquid."
@@ -74,4 +74,5 @@
icon = 'icons/turf/exterior/water_still.dmi'
reagent_type = /decl/material/liquid/acid/stomach
color = "#c7c27c"
+ base_color = "#c7c27c"
dirt_color = "#c40031"
diff --git a/code/modules/overmap/exoplanets/theme.dm b/code/modules/overmap/exoplanets/theme.dm
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/code/modules/overmap/ftl_shunt/computer.dm b/code/modules/overmap/ftl_shunt/computer.dm
index 94d4a35dfaa..38810f48a89 100644
--- a/code/modules/overmap/ftl_shunt/computer.dm
+++ b/code/modules/overmap/ftl_shunt/computer.dm
@@ -5,7 +5,6 @@
light_color = "#77fff8"
var/obj/machinery/ftl_shunt/core/linked_core
- var/cost = 0
var/plotting_jump = FALSE
var/jump_plot_timer
var/jump_plotted = FALSE
@@ -67,13 +66,12 @@
var/jump_dist = get_dist(linked, locate(x, y, overmap.assigned_z))
var/plot_delay_mult
var/delay
- switch(jump_dist)
- if(1 to linked_core.safe_jump_distance)
- plot_delay_mult = 1
- if(linked_core.safe_jump_distance to linked_core.moderate_jump_distance)
- plot_delay_mult = 1.5
- if(linked_core.moderate_jump_distance to INFINITY)
- plot_delay_mult = 2
+ if(jump_dist < linked_core.safe_jump_distance)
+ plot_delay_mult = 1
+ else if(jump_dist < linked_core.moderate_jump_distance)
+ plot_delay_mult = 1.5
+ else
+ plot_delay_mult = 2
delay = clamp(((jump_dist * BASE_PLOT_TIME_PER_TILE) * plot_delay_mult),1, INFINITY)
jump_plot_timer = addtimer(CALLBACK(src, .proc/finish_plot, x, y), delay, TIMER_STOPPABLE)
@@ -116,7 +114,6 @@
if(!linked_core)
to_chat(user, SPAN_WARNING("Unable to establish connection to superluminal shunt."))
return
- recalc_cost()
data["ftlstatus"] = linked_core.get_status()
data["shunt_x"] = linked_core.shunt_x
@@ -169,12 +166,12 @@
if(href_list["set_shunt_x"])
input_x = input(user, "Enter Destination X Coordinates", "FTL Computer", to_plot_x) as num|null
input_x += fumble
- input_x = Clamp(input_x, 1, overmap.map_size_x - 1)
+ input_x = clamp(input_x, 1, overmap.map_size_x - 1)
if(href_list["set_shunt_y"])
input_y = input(user, "Enter Destination Y Coordinates", "FTL Computer", to_plot_y) as num|null
input_y += fumble
- input_y = Clamp(input_y, 1, overmap.map_size_y - 1)
+ input_y = clamp(input_y, 1, overmap.map_size_y - 1)
if(!CanInteract(user, state))
return TOPIC_NOACTION
@@ -193,7 +190,7 @@
if(linked_core.get_status() != FTL_STATUS_GOOD)
to_chat(user, SPAN_WARNING("Superluminal shunt inoperable. Please try again later."))
return TOPIC_REFRESH
-
+
var/datum/overmap/overmap = global.overmaps_by_name[overmap_id]
var/dist = get_dist(locate(linked_core.shunt_x, linked_core.shunt_y, overmap.assigned_z), get_turf(linked))
if(is_jump_unsafe()) //We are above the safe jump distance, give them a warning.
diff --git a/code/modules/overmap/ftl_shunt/core.dm b/code/modules/overmap/ftl_shunt/core.dm
index 4c7e8a8a4be..5517c44361f 100644
--- a/code/modules/overmap/ftl_shunt/core.dm
+++ b/code/modules/overmap/ftl_shunt/core.dm
@@ -227,9 +227,9 @@
continue
if(H.skill_check(SKILL_ENGINES, SKILL_EXPERT))
to_chat(H, SPAN_DANGER("The deck vibrates with a harmonic that sets your teeth on edge and fills you with dread."))
-
+
var/announcetxt = replacetext(shunt_start_text, "%%TIME%%", "[round(jump_delay/600)] minutes.")
-
+
ftl_announcement.Announce(announcetxt, "FTL Shunt Management System", new_sound = sound('sound/misc/notice2.ogg'))
update_icon()
@@ -304,13 +304,12 @@
//Handles all the effects of the jump.
/obj/machinery/ftl_shunt/core/proc/do_effects(var/distance) //If we're jumping too far, have some !!FUN!! with people and ship systems.
var/shunt_sev
- switch(distance)
- if(1 to safe_jump_distance)
- shunt_sev = SHUNT_SEVERITY_MINOR
- if(safe_jump_distance to moderate_jump_distance)
- shunt_sev = SHUNT_SEVERITY_MAJOR
- if(moderate_jump_distance to INFINITY)
- shunt_sev = SHUNT_SEVERITY_CRITICAL
+ if(distance < safe_jump_distance)
+ shunt_sev = SHUNT_SEVERITY_MINOR
+ else if(distance < moderate_jump_distance)
+ shunt_sev = SHUNT_SEVERITY_MAJOR
+ else
+ shunt_sev = SHUNT_SEVERITY_CRITICAL
for(var/mob/living/carbon/human/H in global.living_mob_list_) //Affect mobs, skip synthetics.
sound_to(H, 'sound/machines/hyperspace_end.ogg')
@@ -393,15 +392,14 @@
if(SHUNT_SABOTAGE_MINOR)
announcetxt = shunt_sabotage_text_minor
for(var/mob/living/carbon/human/H in view(7))
- to_chat(H, SPAN_DANGER("[src] emits a flash of incredibly bright, searing light!"))
+ H.show_message(SPAN_DANGER("\The [src] emits a flash of incredibly bright, searing light!"), VISIBLE_MESSAGE)
H.flash_eyes(FLASH_PROTECTION_NONE)
empulse(src, 8, 10)
if(SHUNT_SABOTAGE_MAJOR)
announcetxt = shunt_sabotage_text_major
- for(var/mob/living/carbon/human/H in view(7)) //Effect One: scary text.
- to_chat(H, SPAN_DANGER("[src] hisses and sparks, before coolant lines burst and spew superheated coolant!"))
+ visible_message(SPAN_DANGER("\The [src] hisses and sparks, before the coolant lines burst and spew superheated coolant!")) //Effect One: scary text.
explosion(get_turf(src),-1,-1,8,10) //Effect Two: blow the windows out.
@@ -419,7 +417,7 @@
A.energy_fail(rand(100,120))
for(var/mob/living/carbon/human/H in view(7)) //scary text if you're in view, because you're fucked now boy.
- to_chat(H, SPAN_DANGER("The light around [src] warps before it emits a flash of incredibly bright, searing light!"))
+ H.show_message(SPAN_DANGER("The light around \the [src] warps before it emits a flash of incredibly bright, searing light!"), VISIBLE_MESSAGE)
H.flash_eyes(FLASH_PROTECTION_NONE)
new /obj/singularity/(get_turf(src))
@@ -525,7 +523,7 @@
var/drawn_charge = use_power_oneoff(input)
last_power_drawn = drawn_charge
accumulated_charge += drawn_charge * CELLRATE
-
+
return TRUE
/obj/machinery/ftl_shunt/core/proc/get_total_fuel_conversion_rate()
@@ -640,9 +638,9 @@
return TRUE
-//
+//
// Construction MacGuffins down here.
-//
+//
/obj/item/stock_parts/circuitboard/ftl_shunt
name = "circuit board (superluminal shunt)"
diff --git a/code/modules/overmap/internet/internet_uplink.dm b/code/modules/overmap/internet/internet_uplink.dm
index 19cf7c66f6e..5b9d506cba6 100644
--- a/code/modules/overmap/internet/internet_uplink.dm
+++ b/code/modules/overmap/internet/internet_uplink.dm
@@ -22,7 +22,7 @@ var/global/list/internet_uplinks = list()
var/restrict_networks = FALSE // Whether or not a network needs to be permitted to use this uplink.
var/list/permitted_networks = list() // Network IDs which are permitted to connect through this uplink.
- var/initial_id_tag = "plexus"
+ var/initial_id_tag = "plexus"
/obj/machinery/internet_uplink/Initialize()
. = ..()
@@ -51,10 +51,10 @@ var/global/list/internet_uplinks = list()
if(use_power != POWER_USE_ACTIVE)
return
-
+
// Larger ranges not only require more power, but greater cooling.
var/inefficiency = clamp(0.3 + (0.1 * (overmap_range - BASE_INTERNET_RANGE)), 0.1, 0.6)
-
+
var/datum/gas_mixture/env = return_air()
if(!istype(env) || env.return_pressure() < 10) // Vacuum cooling is insufficient for this machine.
take_damage(10, BURN)
@@ -73,7 +73,7 @@ var/global/list/internet_uplinks = list()
if((use_power == POWER_USE_ACTIVE) && !(stat & NOPOWER))
if(icon_state == "unpowered") // Switching states, flash an animation.
flick("startup", src)
-
+
icon_state = "powered"
else
icon_state = "unpowered"
@@ -82,12 +82,12 @@ var/global/list/internet_uplinks = list()
. = ..()
if(.)
return
-
+
if(href_list["toggle_power"])
var/new_power = (use_power == POWER_USE_ACTIVE ? POWER_USE_IDLE : POWER_USE_ACTIVE)
update_use_power(new_power)
return TOPIC_REFRESH
-
+
if(href_list["modify_range"])
var/new_range = input(user,"Enter the desired range in standard sectors (1 - [max_overmap_range]). Higher ranges increase power usage and heat production.", "Enter new range") as num|null
if(!CanInteract(user, state))
@@ -117,7 +117,7 @@ var/global/list/internet_uplinks = list()
return TOPIC_HANDLED
/obj/machinery/internet_uplink/proc/update_range(new_range)
- overmap_range = Clamp(1, new_range, max_overmap_range)
+ overmap_range = clamp(1, new_range, max_overmap_range)
change_power_consumption(power_per_range * overmap_range, POWER_USE_ACTIVE)
/obj/machinery/internet_uplink/power_change()
@@ -148,7 +148,7 @@ var/global/list/internet_uplinks = list()
ui.open()
/obj/machinery/internet_uplink/RefreshParts()
- max_overmap_range = BASE_INTERNET_RANGE + Clamp(total_component_rating_of_type(/obj/item/stock_parts/smes_coil), 0, 10)
+ max_overmap_range = BASE_INTERNET_RANGE + clamp(total_component_rating_of_type(/obj/item/stock_parts/smes_coil), 0, 10)
update_range(overmap_range) // Check to ensure the set overmap range is still below the new maximum.
. = ..()
@@ -162,7 +162,7 @@ var/global/list/internet_uplinks = list()
idle_power_usage = 250
active_power_usage = 500
var/initial_id_tag = "plexus"
-
+
var/current_uplink = 1
/obj/machinery/computer/internet_uplink/Initialize()
@@ -186,7 +186,7 @@ var/global/list/internet_uplinks = list()
// Internet uplinks are restricted to one per network, so this should return the only uplink linked.
var/obj/machinery/internet_uplink/linked = lan.get_devices(/obj/machinery/internet_uplink)?[1]
-
+
if(!istype(linked))
to_chat(user, SPAN_WARNING("\The [src] flashes an error: No PLEXUS uplink connected!"))
return FALSE
diff --git a/code/modules/overmap/overmap_object.dm b/code/modules/overmap/overmap_object.dm
index 6e4ca4662dd..3d55e54f533 100644
--- a/code/modules/overmap/overmap_object.dm
+++ b/code/modules/overmap/overmap_object.dm
@@ -25,7 +25,8 @@ var/global/list/overmap_unknown_ids = list()
var/last_burn = 0 // worldtime when ship last acceleated
var/burn_delay = 1 SECOND // how often ship can do burns
- var/overmap_id = OVERMAP_ID_SPACE
+ var/overmap_id = OVERMAP_ID_SPACE // Which overmap datum this object expects to be dealing with
+ var/adjacency_radius = 0 // draws a circle under the effect scaled to this size, 1 = 1 turf
/obj/effect/overmap/proc/get_heading_angle()
. = round(Atan2(speed[2], speed[1]))
@@ -54,6 +55,9 @@ var/global/list/overmap_unknown_ids = list()
if(scannable)
unknown_id = "[pick(global.phonetic_alphabet)]-[random_id(/obj/effect/overmap, 100, 999)]"
+ update_moving()
+
+ add_filter("glow", 1, list("drop_shadow", color = color + "F0", size = 2, offset = 1,x = 0, y = 0))
update_icon()
/obj/effect/overmap/Crossed(var/obj/effect/overmap/visitable/other)
@@ -68,7 +72,18 @@ var/global/list/overmap_unknown_ids = list()
SSskybox.rebuild_skyboxes(O.map_z)
/obj/effect/overmap/on_update_icon()
- add_filter("glow", 1, list("drop_shadow", color = color + "F0", size = 2, offset = 1,x = 0, y = 0))
+ . = ..()
+ underlays.Cut()
+ if(adjacency_radius)
+ var/image/radius = image(icon = 'icons/obj/overmap.dmi', icon_state = "radius")
+ if(adjacency_radius != 1)
+ var/matrix/M = matrix()
+ M.Scale(adjacency_radius)
+ radius.transform = M
+ radius.appearance_flags = (RESET_ALPHA | KEEP_APART)
+ radius.alpha = 50
+ radius.filters = filter(type="blur", size = 1)
+ underlays += radius
/obj/effect/overmap/proc/handle_wraparound()
@@ -79,7 +94,7 @@ var/global/list/overmap_unknown_ids = list()
var/datum/overmap/overmap = global.overmaps_by_z["[T.z]"]
var/nx = x
var/ny = y
-
+
var/heading_dir = get_heading_dir()
if((heading_dir & WEST) && x == 1)
@@ -120,8 +135,22 @@ var/global/list/overmap_unknown_ids = list()
/obj/effect/overmap/proc/adjust_speed(n_x, n_y)
CHANGE_SPEED_BY(speed[1], n_x, min_speed)
CHANGE_SPEED_BY(speed[2], n_y, min_speed)
+ update_moving()
+
+/obj/effect/overmap/proc/update_moving()
+ if(is_still())
+ SSovermap.moving_entities -= src
+ else
+ SSovermap.moving_entities[src] = TRUE
update_icon()
+/obj/effect/overmap/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ SSovermap.moving_entities -= src
+ speed = list(0, 0)
+ position = list(0, 0)
+ . = ..()
+
/obj/effect/overmap/proc/can_burn()
if(halted)
return FALSE
@@ -130,26 +159,39 @@ var/global/list/overmap_unknown_ids = list()
else
return TRUE
-/obj/effect/overmap/Process()
- if(!halted && !is_still() && can_move)
- var/list/deltas = list(0,0)
- for(var/i = 1 to 2)
- if(MOVING(speed[i], min_speed))
- position[i] += speed[i] * OVERMAP_SPEED_CONSTANT
- if(position[i] < 0)
- deltas[i] = CEILING(position[i])
- else if(position[i] > 0)
- deltas[i] = FLOOR(position[i])
- if(deltas[i] != 0)
- position[i] -= deltas[i]
- position[i] += (deltas[i] > 0) ? -1 : 1
-
- update_icon()
+/obj/effect/overmap/proc/ProcessOvermap(wait, tick)
+
+ if(halted || is_still())
+ return PROCESS_KILL
+
+ if(!can_move)
+ return
+
+ var/moved = FALSE
+ var/list/deltas = list(0,0)
+ for(var/i = 1 to 2)
+ if(!MOVING(speed[i], min_speed))
+ continue
+ // Add speed to this dimension of our position.
+ position[i] += clamp((speed[i] * OVERMAP_SPEED_CONSTANT) * (wait / (1 SECOND)), -1, 1)
+ if(position[i] < 0)
+ deltas[i] = CEILING(position[i])
+ else if(position[i] > 0)
+ deltas[i] = FLOOR(position[i])
+ moved = TRUE
+ // Delta over 0 means we've moved a turf, so we adjust our position accordingly.
+ if(deltas[i] != 0)
+ position[i] -= deltas[i]
+ // Note for future self when confused: this line offsets the effect within the new turf.
+ // Can probably be tidied up at some point but math is spooky.
+ position[i] += (deltas[i] > 0) ? -1 : 1
+
+ if(moved)
var/turf/newloc = locate(x + deltas[1], y + deltas[2], z)
if(newloc && loc != newloc)
Move(newloc)
handle_wraparound()
- handle_overmap_pixel_movement()
+ handle_overmap_pixel_movement()
/obj/effect/overmap/proc/accelerate(var/direction, var/accel_limit)
var/actual_accel_limit = accel_limit / KM_OVERMAP_RATE
@@ -158,7 +200,7 @@ var/global/list/overmap_unknown_ids = list()
var/delta_v = get_delta_v() / KM_OVERMAP_RATE
if(delta_v == 0)
return
- var/partial_power = Clamp(actual_accel_limit / delta_v, 0, 1)
+ var/partial_power = clamp(actual_accel_limit / delta_v, 0, 1)
var/acceleration = min(get_delta_v(TRUE, partial_power) / KM_OVERMAP_RATE, actual_accel_limit)
if(direction & EAST)
adjust_speed(acceleration, 0)
@@ -180,7 +222,7 @@ var/global/list/overmap_unknown_ids = list()
var/spd = speed[i]
var/abs_spd = abs(spd)
if(abs_spd)
- var/partial_power = Clamp(abs_spd / (get_delta_v() / KM_OVERMAP_RATE), 0, 1)
+ var/partial_power = clamp(abs_spd / (get_delta_v() / KM_OVERMAP_RATE), 0, 1)
var/delta_v = min(get_delta_v(TRUE, partial_power) / KM_OVERMAP_RATE, abs_spd)
.[i] = -SIGN(spd) * delta_v
burn = TRUE
diff --git a/code/modules/overmap/ships/computers/engine_control.dm b/code/modules/overmap/ships/computers/engine_control.dm
index 3145dd53829..6aa0a035a86 100644
--- a/code/modules/overmap/ships/computers/engine_control.dm
+++ b/code/modules/overmap/ships/computers/engine_control.dm
@@ -64,7 +64,7 @@
var/newlim = input("Input new thrust limit (0..100%)", "Thrust limit", linked.get_thrust_limit() * 100) as num
if(!CanInteract(user, state))
return TOPIC_NOACTION
- var/thrust_limit = Clamp(newlim / 100, 0, 1)
+ var/thrust_limit = clamp(newlim / 100, 0, 1)
for(var/datum/extension/ship_engine/E in linked.engines)
E.thrust_limit = thrust_limit
return TOPIC_REFRESH
@@ -79,13 +79,13 @@
var/newlim = input("Input new thrust limit (0..100)", "Thrust limit", E.thrust_limit) as num
if(!CanInteract(user, state))
return
- var/limit = Clamp(newlim/100, 0, 1)
+ var/limit = clamp(newlim/100, 0, 1)
if(istype(E))
E.thrust_limit = limit
return TOPIC_REFRESH
if(href_list["limit"])
var/datum/extension/ship_engine/E = locate(href_list["engine"])
- var/limit = Clamp(E.thrust_limit + text2num(href_list["limit"]), 0, 1)
+ var/limit = clamp(E.thrust_limit + text2num(href_list["limit"]), 0, 1)
if(istype(E))
E.thrust_limit = limit
return TOPIC_REFRESH
diff --git a/code/modules/overmap/ships/computers/helm.dm b/code/modules/overmap/ships/computers/helm.dm
index 8ad56a3af5e..79c195b6966 100644
--- a/code/modules/overmap/ships/computers/helm.dm
+++ b/code/modules/overmap/ships/computers/helm.dm
@@ -197,8 +197,8 @@ var/global/list/overmap_helm_computers
var/newy = input("Input new entry y coordinate", "Coordinate input", linked.y) as num
if(!CanInteract(user,state))
return TOPIC_NOACTION
- R.fields["x"] = Clamp(newx, 1, world.maxx)
- R.fields["y"] = Clamp(newy, 1, world.maxy)
+ R.fields["x"] = clamp(newx, 1, world.maxx)
+ R.fields["y"] = clamp(newy, 1, world.maxy)
known_sectors[sec_name] = R
compass.set_waypoint("\ref[R]", R.fields["name"], R.fields["x"], R.fields["y"], 1, R.fields["color"] || COLOR_CYAN)
compass.hide_waypoint("\ref[R]")
@@ -236,14 +236,14 @@ var/global/list/overmap_helm_computers
if(!CanInteract(user,state))
return TOPIC_NOACTION
if (newx)
- dx = Clamp(newx, 1, world.maxx)
+ dx = clamp(newx, 1, world.maxx)
if (href_list["sety"])
var/newy = input("Input new destiniation y coordinate", "Coordinate input", dy) as num|null
if(!CanInteract(user,state))
return TOPIC_NOACTION
if (newy)
- dy = Clamp(newy, 1, world.maxy)
+ dy = clamp(newy, 1, world.maxy)
if (href_list["x"] && href_list["y"])
dx = text2num(href_list["x"])
@@ -256,7 +256,7 @@ var/global/list/overmap_helm_computers
if (href_list["speedlimit"])
var/newlimit = input("Input new speed limit for autopilot (0 to brake)", "Autopilot speed limit", speedlimit) as num|null
if(newlimit)
- speedlimit = Clamp(newlimit, 0, 100)
+ speedlimit = clamp(newlimit, 0, 100)
if (href_list["accellimit"])
var/newlimit = input("Input new acceleration limit", "Acceleration limit", accellimit) as num|null
if(newlimit)
diff --git a/code/modules/overmap/ships/computers/sensors.dm b/code/modules/overmap/ships/computers/sensors.dm
index f9f315a96c2..d1809a4d2d5 100644
--- a/code/modules/overmap/ships/computers/sensors.dm
+++ b/code/modules/overmap/ships/computers/sensors.dm
@@ -133,7 +133,7 @@
if(!CanInteract(user,state))
return TOPIC_NOACTION
if (nrange)
- sensors.set_range(Clamp(nrange, 1, world.view))
+ sensors.set_range(clamp(nrange, 1, world.view))
return TOPIC_REFRESH
if (href_list["toggle"])
sensors.toggle()
@@ -228,7 +228,7 @@
/obj/machinery/shipsensors/RefreshParts()
..()
- sensor_strength = Clamp(total_component_rating_of_type(/obj/item/stock_parts/manipulator), 0, 5)
+ sensor_strength = clamp(total_component_rating_of_type(/obj/item/stock_parts/manipulator), 0, 5)
/obj/machinery/shipsensors/weak
heat_reduction = 0.2
diff --git a/code/modules/overmap/ships/device_types/bluespace_thruster.dm b/code/modules/overmap/ships/device_types/bluespace_thruster.dm
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/code/modules/overmap/ships/device_types/gas_thruster.dm b/code/modules/overmap/ships/device_types/gas_thruster.dm
index 3c02b574678..1568f527fe6 100644
--- a/code/modules/overmap/ships/device_types/gas_thruster.dm
+++ b/code/modules/overmap/ships/device_types/gas_thruster.dm
@@ -82,7 +82,7 @@
if(ratio_specific_heat == 0 || ratio_specific_heat == 1)
// rare case of avoiding a divide by zero error.
ratio_specific_heat += 0.01
- return Clamp(ratio_specific_heat, MINIMUM_RATIO_SPECIFIC_HEAT, MAXIMUM_RATIO_SPECIFIC_HEAT)
+ return clamp(ratio_specific_heat, MINIMUM_RATIO_SPECIFIC_HEAT, MAXIMUM_RATIO_SPECIFIC_HEAT)
/datum/extension/ship_engine/gas/get_specific_wet_mass()
var/datum/gas_mixture/propellant = get_propellant()
diff --git a/code/modules/overmap/ships/landable.dm b/code/modules/overmap/ships/landable.dm
index a001d3a661c..7c1b0f8ca27 100644
--- a/code/modules/overmap/ships/landable.dm
+++ b/code/modules/overmap/ships/landable.dm
@@ -26,7 +26,7 @@
return ..()
/obj/effect/overmap/visitable/ship/landable/burn()
- if(status != SHIP_STATUS_OVERMAP && status != SHIP_STATUS_ENCOUNTER)
+ if(status != SHIP_STATUS_OVERMAP && status != SHIP_STATUS_ENCOUNTER)
return 0
return ..()
@@ -46,8 +46,8 @@
return FALSE // Cannot encounter a shuttle while it is landed elsewhere.
. = ..()
-/obj/effect/overmap/visitable/ship/landable/Process()
- . = ..()
+/obj/effect/overmap/visitable/ship/landable/Process(wait, tick)
+ ..()
var/datum/shuttle/autodock/overmap/child_shuttle = SSshuttle.shuttles[shuttle]
if(!child_shuttle || !istype(child_shuttle))
return
diff --git a/code/modules/overmap/ships/machines/gas_thruster.dm b/code/modules/overmap/ships/machines/gas_thruster.dm
index ed7008aeecf..8a24127b892 100644
--- a/code/modules/overmap/ships/machines/gas_thruster.dm
+++ b/code/modules/overmap/ships/machines/gas_thruster.dm
@@ -40,7 +40,7 @@
else
to_chat(user, SPAN_WARNING("\The [src] flashes an error!"))
return TRUE
-
+
. = ..()
/obj/machinery/atmospherics/unary/engine/CanPass(atom/movable/mover, turf/target, height=0, air_group=0)
@@ -61,11 +61,11 @@
if(!E)
return
//allows them to upgrade the max limit of fuel intake (which only gives diminishing returns) for increase in max thrust but massive reduction in fuel economy
- var/bin_upgrade = 10 * Clamp(total_component_rating_of_type(/obj/item/stock_parts/matter_bin), 0, 6)//5 litre per rank
+ var/bin_upgrade = 10 * clamp(total_component_rating_of_type(/obj/item/stock_parts/matter_bin), 0, 6)//5 litre per rank
E.volume_per_burn = bin_upgrade ? initial(E.volume_per_burn) + bin_upgrade : 2 //Penalty missing part: 10% fuel use, no thrust
E.boot_time = bin_upgrade ? initial(E.boot_time) - bin_upgrade : initial(E.boot_time) * 2
//energy cost - thb all of this is to limit the use of back up batteries
- var/energy_upgrade = Clamp(total_component_rating_of_type(/obj/item/stock_parts/capacitor), 0.1, 6)
+ var/energy_upgrade = clamp(total_component_rating_of_type(/obj/item/stock_parts/capacitor), 0.1, 6)
E.charge_per_burn = initial(E.charge_per_burn) / energy_upgrade
change_power_consumption(initial(idle_power_usage) / energy_upgrade, POWER_USE_IDLE)
diff --git a/code/modules/overmap/ships/machines/ion_thruster.dm b/code/modules/overmap/ships/machines/ion_thruster.dm
index 74b3be72d60..51641a0c887 100644
--- a/code/modules/overmap/ships/machines/ion_thruster.dm
+++ b/code/modules/overmap/ships/machines/ion_thruster.dm
@@ -3,7 +3,9 @@
/datum/extension/ship_engine/ion_thruster/burn(var/partial = 1)
var/obj/machinery/ion_thruster/thruster = holder
- . = istype(thruster) && thruster.burn(partial)
+ if(istype(thruster) && thruster.get_thrust(partial))
+ return get_exhaust_velocity() * thruster.thrust_effectiveness
+ return 0
/datum/extension/ship_engine/ion_thruster/get_exhaust_velocity()
. = 300 // Arbitrary value based on being slightly less than a default configuration gas engine.
@@ -39,9 +41,10 @@
construct_state = /decl/machine_construction/default/panel_closed
use_power = POWER_USE_IDLE
+ // TODO: modify these with upgraded parts?
var/thrust_limit = 1
- var/burn_cost = 750
- var/generated_thrust = 2.5
+ var/thrust_cost = 750
+ var/thrust_effectiveness = 1
/obj/machinery/ion_thruster/attackby(obj/item/I, mob/user)
if(IS_MULTITOOL(I) && !panel_open)
@@ -54,11 +57,10 @@
. = ..()
-/obj/machinery/ion_thruster/proc/burn(var/partial)
+/obj/machinery/ion_thruster/proc/get_thrust()
if(!use_power || (stat & NOPOWER))
return 0
- use_power_oneoff(burn_cost)
- . = thrust_limit * generated_thrust
+ return thrust_limit
/obj/machinery/ion_thruster/on_update_icon()
cut_overlays()
diff --git a/code/modules/overmap/ships/ship.dm b/code/modules/overmap/ships/ship.dm
index 0fefbac8b05..6b6975a6932 100644
--- a/code/modules/overmap/ships/ship.dm
+++ b/code/modules/overmap/ships/ship.dm
@@ -35,7 +35,6 @@ var/global/const/OVERMAP_SPEED_CONSTANT = (1 SECOND)
base_sensor_visibility = round((vessel_mass/SENSOR_COEFFICENT),1)
/obj/effect/overmap/visitable/ship/Destroy()
- STOP_PROCESSING(SSobj, src)
SSshuttle.ships -= src
for(var/thing in get_linked_machines_of_type(/obj/machinery/computer/ship))
var/obj/machinery/computer/ship/machine = thing
@@ -45,7 +44,7 @@ var/global/const/OVERMAP_SPEED_CONSTANT = (1 SECOND)
/obj/effect/overmap/visitable/ship/proc/set_thrust_limit(var/thrust_limit)
for(var/datum/extension/ship_engine/E in engines)
- E.thrust_limit = Clamp(thrust_limit, 0, 1)
+ E.thrust_limit = clamp(thrust_limit, 0, 1)
/obj/effect/overmap/visitable/ship/proc/set_engine_power(var/engine_power)
for(var/datum/extension/ship_engine/E in engines)
@@ -114,8 +113,7 @@ var/global/const/OVERMAP_SPEED_CONSTANT = (1 SECOND)
var/burns_per_grid = 1/ (burn_delay * get_speed())
return round(num_burns / burns_per_grid)
-/obj/effect/overmap/visitable/ship/Process()
- . = ..()
+/obj/effect/overmap/visitable/ship/Process(wait, tick)
damping_strength = 0
for(var/datum/ship_inertial_damper/I in inertial_dampers)
var/obj/machinery/inertial_damper/ID = I.holder
@@ -156,11 +154,12 @@ var/global/const/OVERMAP_SPEED_CONSTANT = (1 SECOND)
/obj/effect/overmap/visitable/ship/proc/halt()
adjust_speed(-speed[1], -speed[2])
- halted = 1
+ halted = TRUE
/obj/effect/overmap/visitable/ship/proc/unhalt()
if(!SSshuttle.overmap_halted)
- halted = 0
+ halted = FALSE
+ update_moving()
/obj/effect/overmap/visitable/ship/Bump(var/atom/A)
if(istype(A,/turf/unsimulated/map/edge))
diff --git a/code/modules/overmap/ships/ship_physics.dm b/code/modules/overmap/ships/ship_physics.dm
index 217fad889c5..c8040b0a926 100644
--- a/code/modules/overmap/ships/ship_physics.dm
+++ b/code/modules/overmap/ships/ship_physics.dm
@@ -5,7 +5,7 @@
// partial power is used with burn() in order to only do partial burns.
/obj/effect/overmap/visitable/ship/get_delta_v(var/real_burn = FALSE, var/partial_power = 1)
var/total_exhaust_velocity = 0
- partial_power = Clamp(partial_power, 0, 1)
+ partial_power = clamp(partial_power, 0, 1)
for(var/datum/extension/ship_engine/E in engines)
total_exhaust_velocity += real_burn ? E.burn(partial_power) : E.get_exhaust_velocity()
var/vessel_mass = get_vessel_mass()
diff --git a/code/modules/paperwork/adminpaper.dm b/code/modules/paperwork/adminpaper.dm
index 91d853c4f2b..a6a1cf86f12 100644
--- a/code/modules/paperwork/adminpaper.dm
+++ b/code/modules/paperwork/adminpaper.dm
@@ -8,7 +8,7 @@
var/isCrayon = 0
var/origin = null
var/mob/sender = null
- var/obj/machinery/photocopier/faxmachine/destination
+ var/weakref/destination_ref
var/header = null
var/headerOn = TRUE
@@ -18,12 +18,12 @@
var/logo_list = list()
var/logo = ""
+ var/signature //Signature entered by the admin
-/obj/item/paper/admin/Initialize()
+/obj/item/paper/admin/Initialize(mapload, material_key, _text, _title, list/md)
. = ..()
generateInteractions()
-
/obj/item/paper/admin/proc/generateInteractions()
//clear first
interactions = null
@@ -63,7 +63,6 @@
footer = text
-
/obj/item/paper/admin/proc/adminbrowse()
updateinfolinks()
generateHeader()
@@ -71,9 +70,7 @@
updateDisplay()
/obj/item/paper/admin/proc/updateDisplay()
- show_browser(usr, "[name][headerOn ? header : ""][info_links][stamps][footerOn ? footer : ""][interactions]", "window=[name];can_close=0")
-
-
+ show_browser(usr, "[name][headerOn ? header : ""][info_links][stamp_text][footerOn ? footer : ""][interactions]", "window=[name];can_close=0")
/obj/item/paper/admin/Topic(href, href_list)
if(href_list["write"])
@@ -113,6 +110,10 @@
return
if(href_list["confirm"])
+ var/obj/machinery/faxmachine/F = destination_ref.resolve()
+ if(!istype(F))
+ to_chat(usr, "The destination machines doesn't exist anymore..")
+ return
switch(alert("Are you sure you want to send the fax as is?",, "Yes", "No"))
if("Yes")
if(headerOn)
@@ -121,7 +122,7 @@
info += footer
updateinfolinks()
close_browser(usr, "window=[name]")
- admindatum.faxCallback(src, destination)
+ admindatum.faxCallback(src, F)
return
if(href_list["penmode"])
@@ -156,5 +157,8 @@
updateDisplay()
return
-/obj/item/paper/admin/get_signature()
- return input(usr, "Enter the name you wish to sign the paper with (will prompt for multiple entries, in order of entry)", "Signature") as text|null
\ No newline at end of file
+/obj/item/paper/admin/get_signature(obj/item/pen/P, mob/user)
+ return signature
+
+/obj/item/paper/admin/proc/set_signature(var/sig)
+ signature = sig
\ No newline at end of file
diff --git a/code/modules/paperwork/bodyscan_paper.dm b/code/modules/paperwork/bodyscan_paper.dm
index b6fd84bc3ed..eb25f84a12b 100644
--- a/code/modules/paperwork/bodyscan_paper.dm
+++ b/code/modules/paperwork/bodyscan_paper.dm
@@ -2,10 +2,6 @@
color = COLOR_OFF_WHITE
scan_file_type = /datum/computer_file/data/bodyscan
-/obj/item/paper/bodyscan/examine(mob/user)
- set_content(display_medical_data(metadata, user.get_skill_value(SKILL_MEDICAL), TRUE))
- . = ..()
-
-/obj/item/paper/bodyscan/show_info(var/mob/user)
+/obj/item/paper/bodyscan/interact(mob/user, forceshow, readonly)
set_content(display_medical_data(metadata, user.get_skill_value(SKILL_MEDICAL), TRUE))
. = ..()
\ No newline at end of file
diff --git a/code/modules/paperwork/carbonpaper.dm b/code/modules/paperwork/carbonpaper.dm
index d5b06c9f0e2..34b1ad92d19 100644
--- a/code/modules/paperwork/carbonpaper.dm
+++ b/code/modules/paperwork/carbonpaper.dm
@@ -1,52 +1,42 @@
+/////////////////////////////////////////////////
+// Carbon paper
+/////////////////////////////////////////////////
/obj/item/paper/carbon
- name = "sheet of paper"
+ name = "sheets of paper with carbon paper"
icon_state = "paper_stack"
item_state = "paper"
- var/copied = 0
- var/iscopy = 0
+/obj/item/paper/carbon/update_contents_overlays()
+ if(length(info))
+ add_overlay("paper_stack_words")
-/obj/item/paper/carbon/on_update_icon()
- if(iscopy)
- if(info)
- icon_state = "cpaper_words"
- return
- icon_state = "cpaper"
- else if (copied)
- if(info)
- icon_state = "paper_words"
- return
- icon_state = "paper"
- else
- if(info)
- icon_state = "paper_stack_words"
- return
- icon_state = "paper_stack"
+/obj/item/paper/carbon/proc/remove_copy(var/mob/user)
+ var/obj/item/paper/original = Clone()
+ var/obj/item/paper/copy = Clone()
+ LAZYSET(copy.metadata, "is_copy", TRUE)
+ copy.set_color("#ffccff")
+ //Silly hack to make all the text grayscale, since nobody is using standard css classes for pens we could override instead. Also, all using deprecated tags as well.
+ var/copycontents = copy.info
+ copycontents = replacetext(copycontents, "[copycontents]", "Copy - [original.name]")
+ qdel(src)
+ user.put_in_active_hand(original)
+ user.put_in_hands(copy)
+ return copy
-/obj/item/paper/carbon/verb/removecopy()
- set name = "Remove carbon-copy"
- set category = "Object"
- set src in usr
+/obj/item/paint_sprayer/get_alt_interactions(mob/user)
+ . = ..()
+ LAZYADD(., /decl/interaction_handler/carbon_paper_remove)
- if (copied == 0)
- var/obj/item/paper/carbon/c = src
- var/copycontents = c.info
- var/obj/item/paper/carbon/copy = new /obj/item/paper/carbon (usr.loc)
- //
- if(info)
- copycontents = replacetext(copycontents, ""
- copy.SetName("Copy - " + c.name)
- copy.fields = c.fields
- copy.updateinfolinks()
- to_chat(usr, "You tear off the carbon-copy!")
- c.copied = 1
- copy.iscopy = 1
- copy.update_icon()
- c.update_icon()
- else
- to_chat(usr, "There are no more carbon copies attached to this paper!")
+/////////////////////////////////////////////////
+// Carbon Paper Alt Interactions
+/////////////////////////////////////////////////
+/decl/interaction_handler/carbon_paper_remove
+ name = "remove carbon-copy"
+ expected_target_type = /obj/item/paper/carbon
+
+/decl/interaction_handler/carbon_paper_remove/invoked(obj/item/paper/carbon/target, mob/user)
+ target.remove_copy(user)
diff --git a/code/modules/paperwork/clipboard.dm b/code/modules/paperwork/clipboard.dm
index e80b9f36cde..c839faee173 100644
--- a/code/modules/paperwork/clipboard.dm
+++ b/code/modules/paperwork/clipboard.dm
@@ -1,28 +1,31 @@
/obj/item/clipboard
- name = "clipboard"
- desc = "It's a board with a clip used to organise papers."
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "clipboard"
- item_state = "clipboard"
- throwforce = 0
- w_class = ITEM_SIZE_SMALL
- throw_speed = 3
- throw_range = 10
- slot_flags = SLOT_LOWER_BODY
- applies_material_name = FALSE
- material = /decl/material/solid/wood
- drop_sound = 'sound/foley/tooldrop5.ogg'
- pickup_sound = 'sound/foley/paperpickup2.ogg'
-
- var/obj/item/pen/haspen //The stored pen.
- var/obj/item/toppaper //The topmost piece of paper.
-
-/obj/item/clipboard/Initialize()
+ name = "clipboard"
+ desc = "It's a board with a clip used to organise papers."
+ icon = 'icons/obj/items/clipboard.dmi'
+ icon_state = "clipboard"
+ item_state = "clipboard"
+ throwforce = 0
+ w_class = ITEM_SIZE_SMALL
+ throw_speed = 3
+ throw_range = 10
+ slot_flags = SLOT_LOWER_BODY
+ material_alteration = MAT_FLAG_ALTERATION_COLOR
+ material = /decl/material/solid/wood
+ drop_sound = 'sound/foley/tooldrop5.ogg'
+ pickup_sound = 'sound/foley/paperpickup2.ogg'
+
+ var/obj/item/stored_pen //The stored pen.
+ var/list/papers
+ var/tmp/max_papers = 50
+
+/obj/item/clipboard/Initialize(ml, material_key)
. = ..()
update_icon()
- if(material)
- desc = initial(desc)
- desc += " It's made of [material.use_name]."
+
+/obj/item/clipboard/Destroy()
+ QDEL_NULL_LIST(papers)
+ stored_pen = null
+ return ..()
/obj/item/clipboard/handle_mouse_drop(atom/over, mob/user)
if(ishuman(user) && istype(over, /obj/screen/inventory))
@@ -33,156 +36,184 @@
return TRUE
. = ..()
+/obj/item/clipboard/examine(mob/user, distance, infix, suffix)
+ . = ..()
+ if(stored_pen)
+ to_chat(user, "It's holding \a [stored_pen].")
+ if(!LAZYLEN(papers))
+ to_chat(user, "It contains [length(papers)] / [max_papers] paper\s.")
+ else
+ to_chat(user, "It has room for [max_papers] paper\s.")
+
+/obj/item/clipboard/proc/top_paper()
+ return LAZYACCESS(papers, 1)
+
+/obj/item/clipboard/proc/push_paper(var/obj/item/P)
+ LAZYINSERT(papers, P, 1)
+ updateUsrDialog()
+ update_icon()
+
+/obj/item/clipboard/proc/pop_paper()
+ . = top_paper()
+ LAZYREMOVE(papers, 1)
+ updateUsrDialog()
+ update_icon()
+
/obj/item/clipboard/on_update_icon()
..()
- if(toppaper)
- overlays += overlay_image(toppaper.icon, toppaper.icon_state, flags=RESET_COLOR)
- overlays += toppaper.overlays
- if(haspen)
- overlays += overlay_image(icon, "clipboard_pen", flags=RESET_COLOR)
- overlays += overlay_image(icon, "clipboard_over", flags=RESET_COLOR)
- return
+ var/obj/item/top_paper = top_paper()
+ if(top_paper)
+ var/mutable_appearance/I = new /mutable_appearance(top_paper)
+ I.appearance_flags |= RESET_COLOR
+ I.plane = FLOAT_PLANE
+ I.pixel_x = 0
+ I.pixel_y = 0
+ I.pixel_w = 0
+ I.pixel_z = 0 //randpixel
+ add_overlay(I)
+ if(stored_pen)
+ add_overlay(overlay_image(icon, "clipboard_pen", stored_pen.color, RESET_COLOR))
+ add_overlay(overlay_image(icon, "clipboard_over", flags=RESET_COLOR))
/obj/item/clipboard/attackby(obj/item/W, mob/user)
-
+ var/obj/item/top_paper = top_paper()
if(istype(W, /obj/item/paper) || istype(W, /obj/item/photo))
if(!user.unEquip(W, src))
return
- if(istype(W, /obj/item/paper))
- toppaper = W
- to_chat(user, "You clip the [W] onto \the [src].")
- update_icon()
+ push_paper(W)
+ to_chat(user, SPAN_NOTICE("You clip the [W] onto \the [src]."))
+ return TRUE
- else if(istype(toppaper) && istype(W, /obj/item/pen))
- toppaper.attackby(W, usr)
+ else if(top_paper?.attackby(W, user))
+ updateUsrDialog()
update_icon()
+ return TRUE
- return
+ else if(IS_PEN(W) && add_pen(W, user)) //If we don't have any paper, and hit it with a pen, try slotting it in
+ return TRUE
+
+ return ..()
+
+/obj/item/clipboard/AltClick(mob/user)
+ if(stored_pen)
+ remove_pen(user)
/obj/item/clipboard/attack_self(mob/user)
+ if(CanPhysicallyInteractWith(user, src))
+ interact(user)
+ return TRUE
+
+/obj/item/clipboard/interact(mob/user)
var/dat = "Clipboard"
- if(haspen)
+ if(stored_pen)
dat += "Remove Pen
"
else
dat += "Add Pen
"
- //The topmost paper. I don't think there's any way to organise contents in byond, so this is what we're stuck with. -Pete
- if(toppaper)
- var/obj/item/paper/P = toppaper
- dat += "Write Remove Rename - [P.name]
"
-
- for(var/obj/item/paper/P in src)
- if(P==toppaper)
- continue
- dat += "Remove Rename - [P.name]
"
- for(var/obj/item/photo/Ph in src)
- dat += "Remove Rename - [Ph.name]
"
-
- show_browser(user, dat, "window=clipboard")
- onclose(user, "clipboard")
+ dat += ""
+ for(var/i = 1 to LAZYLEN(papers))
+ var/obj/item/P = papers[i]
+ dat += "| [P.name] | "
+ if(i == 1)
+ dat += " | Write | "
+ else
+ dat += " | | "
+ dat += "Remove | Rename |
"
+ dat += "
"
+
+ user.set_machine(src)
+ show_browser(user, dat, "window=[initial(name)]")
+ onclose(user, initial(name))
add_fingerprint(usr)
return
-/obj/item/clipboard/Topic(href, href_list)
- ..()
- if((usr.stat || usr.restrained()))
- return
-
- if(src.loc == usr)
-
- if(href_list["pen"])
- if(istype(haspen) && (haspen.loc == src))
- usr.put_in_hands_or_store_or_drop(haspen)
- haspen = null
-
- else if(href_list["addpen"])
- if(!haspen)
- var/obj/item/pen/W = usr.get_active_hand()
- if(istype(W, /obj/item/pen))
- if(!usr.unEquip(W, src))
- return
- haspen = W
- to_chat(usr, "You slot the pen into \the [src].")
-
- else if(href_list["write"])
- var/obj/item/P = locate(href_list["write"])
-
- if(P && (P.loc == src) && istype(P, /obj/item/paper) && (P == toppaper) )
-
- var/obj/item/I = usr.get_active_hand()
-
- if(istype(I, /obj/item/pen))
-
- P.attackby(I, usr)
-
- else if(href_list["remove"])
- var/obj/item/P = locate(href_list["remove"])
-
- if(P && (P.loc == src) && (istype(P, /obj/item/paper) || istype(P, /obj/item/photo)) )
- usr.put_in_hands_or_store_or_drop(P)
- if(P == toppaper)
- toppaper = null
- var/obj/item/paper/newtop = locate(/obj/item/paper) in src
- if(newtop && (newtop != P))
- toppaper = newtop
- else
- toppaper = null
-
- else if(href_list["rename"])
- var/obj/item/O = locate(href_list["rename"])
-
- if(O && (O.loc == src))
- if(istype(O, /obj/item/paper))
- var/obj/item/paper/to_rename = O
- to_rename.rename()
-
- else if(istype(O, /obj/item/photo))
- var/obj/item/photo/to_rename = O
- to_rename.rename()
-
- else if(href_list["read"])
- var/obj/item/paper/P = locate(href_list["read"])
-
- if(P && (P.loc == src) && istype(P, /obj/item/paper) )
-
- if(!(istype(usr, /mob/living/carbon/human) || isghost(usr) || istype(usr, /mob/living/silicon)))
- show_browser(usr, "[P.name][stars(P.info)][P.stamps]", "window=[P.name]")
- onclose(usr, "[P.name]")
- else
- show_browser(usr, "[P.name][P.info][P.stamps]", "window=[P.name]")
- onclose(usr, "[P.name]")
-
- else if(href_list["look"])
- var/obj/item/photo/P = locate(href_list["look"])
- if(P && (P.loc == src) && istype(P, /obj/item/photo) )
- P.show(usr)
-
- else if(href_list["top"]) // currently unused
- var/obj/item/P = locate(href_list["top"])
- if(P && (P.loc == src) && istype(P, /obj/item/paper) )
- toppaper = P
- to_chat(usr, "You move [P.name] to the top.")
-
- //Update everything
- attack_self(usr)
+/obj/item/clipboard/proc/add_pen(var/obj/item/I, var/mob/user)
+ if(!stored_pen && I.w_class <= ITEM_SIZE_TINY && IS_PEN(I) && user.unEquip(I, src))
+ stored_pen = I
+ to_chat(user, SPAN_NOTICE("You slot \the [I] into \the [src]."))
+ updateUsrDialog()
update_icon()
- return
+ return TRUE
+ else if(stored_pen)
+ to_chat(user, SPAN_WARNING("There is already \a [stored_pen] in \the [src]."))
+ else if(I.w_class > ITEM_SIZE_TINY)
+ to_chat(user, SPAN_WARNING("\The [I] is too big to fit in \the [src]."))
+
+/obj/item/clipboard/proc/remove_pen(var/mob/user)
+ if(stored_pen && user.get_empty_hand_slot())
+ to_chat(user, SPAN_NOTICE("You pull \the [stored_pen] from your [src]."))
+ user.put_in_hands(stored_pen)
+ . = stored_pen
+ stored_pen = null
+ updateUsrDialog()
+ update_icon()
+ return .
+ else if(!stored_pen)
+ to_chat(user, SPAN_WARNING("There is no pen in \the [src]."))
+ else
+ to_chat(user, SPAN_WARNING("Your hands are full."))
+
+/obj/item/clipboard/DefaultTopicState()
+ return global.physical_topic_state
+
+/obj/item/clipboard/OnTopic(mob/user, href_list, datum/topic_state/state)
+ . = ..()
+ var/obj/item/tpaper = top_paper()
+
+ if(href_list["pen"] && remove_pen(user))
+ . = TOPIC_REFRESH
+
+ else if(href_list["addpen"] && add_pen(user.get_accessible_pen(), user))
+ . = TOPIC_REFRESH
+
+ else if(href_list["write"])
+ if(tpaper)
+ var/obj/item/I = user.get_accessible_pen()
+ //We can also use the stored pen if we have one and a free hand
+ if(!I && IS_PEN(stored_pen))
+ I = remove_pen(user)
+ else if(!I)
+ to_chat(user, SPAN_WARNING("You don't have a pen!"))
+ return TOPIC_NOACTION
+
+ if(I)
+ tpaper.attackby(I, user)
+ . = TOPIC_REFRESH
+ else
+ . = TOPIC_NOACTION
+
+ else if(href_list["remove"])
+ var/obj/item/P = locate(href_list["remove"])
+ user.put_in_hands(P)
+ papers.Remove(P)
+ . = TOPIC_REFRESH
+
+ else
+ . = handle_paper_stack_shared_topics(user, href_list)
+
+ //Update everything
+ if(. & TOPIC_REFRESH)
+ updateUsrDialog()
+ update_icon()
+
+/obj/item/clipboard/dropped(mob/user)
+ . = ..()
+ if(CanUseTopic(user, DefaultTopicState()))
+ updateUsrDialog()
+ else
+ close_browser(user, initial(name))
/obj/item/clipboard/ebony
material = /decl/material/solid/wood/ebony
/obj/item/clipboard/steel
material = /decl/material/solid/metal/steel
- material = /decl/material/solid/metal/steel
/obj/item/clipboard/aluminium
material = /decl/material/solid/metal/aluminium
- material = /decl/material/solid/metal/aluminium
/obj/item/clipboard/glass
material = /decl/material/solid/glass
- material = /decl/material/solid/glass
/obj/item/clipboard/plastic
material = /decl/material/solid/plastic
- material = /decl/material/solid/plastic
\ No newline at end of file
diff --git a/code/modules/paperwork/faxmachine.dm b/code/modules/paperwork/faxmachine.dm
index dea037bdcbf..1af69a54c88 100644
--- a/code/modules/paperwork/faxmachine.dm
+++ b/code/modules/paperwork/faxmachine.dm
@@ -1,235 +1,692 @@
-var/global/list/allfaxes = list()
-var/global/list/alldepartments = list()
-
-var/global/list/adminfaxes = list() //cache for faxes that have been sent to admins
-
-/obj/machinery/photocopier/faxmachine
- name = "fax machine"
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "fax"
- insert_anim = "faxsend"
- var/send_access = list(list(access_lawyer, access_bridge, access_armory, access_qm))
-
- idle_power_usage = 30
- active_power_usage = 200
-
- var/obj/item/card/id/scan = null // identification
- var/authenticated = 0
- var/sendcooldown = 0 // to avoid spamming fax messages
- var/department = "Unknown" // our department
- var/destination = null // the department we're sending to
+#define FAX_QUICK_DIAL_FILE "quickdial"
+#define FAX_HISTORY_FILE "fax_history"
+#define FAX_COOLDOWN 15 SECONDS //Cooldown after sending a regular fax
+#define FAX_ADMIN_COOLDOWN 30 SECONDS //Cooldown after faxing an admin
- var/static/list/admin_departments
-
-/obj/machinery/photocopier/faxmachine/Initialize()
+var/global/list/allfaxes = list()
+var/global/list/alldepartments = list()
+var/global/list/adminfaxes = list() //cache for faxes that have been sent to admins
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Fax Machine Board
+////////////////////////////////////////////////////////////////////////////////////////
+/obj/item/stock_parts/circuitboard/fax_machine
+ name = "circuitboard (fax machine)"
+ build_path = /obj/machinery/faxmachine
+ board_type = "machine"
+ origin_tech = "{'engineering':1, 'programming':1}"
+ req_components = list(
+ /obj/item/stock_parts/printer = 1,
+ /obj/item/stock_parts/manipulator = 1,
+ /obj/item/stock_parts/scanning_module = 1,
+ )
+ additional_spawn_components = list(
+ /obj/item/stock_parts/console_screen = 1,
+ /obj/item/stock_parts/keyboard = 1,
+ /obj/item/stock_parts/power/apc/buildable = 1,
+ /obj/item/stock_parts/computer/lan_port = 1,
+ /obj/item/stock_parts/computer/network_card = 1,
+ /obj/item/stock_parts/item_holder/disk_reader/buildable = 1,
+ /obj/item/stock_parts/item_holder/card_reader/buildable = 1,
+ /obj/item/stock_parts/access_lock/buildable = 1,
+ )
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Fax Machine Network Device
+////////////////////////////////////////////////////////////////////////////////////////
+/datum/extension/network_device/fax
+ has_commands = TRUE
+ long_range = TRUE
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Fax Machine Quick-Dial file
+////////////////////////////////////////////////////////////////////////////////////////
+/datum/computer_file/data/fax_quick_dial
+ filetype = "FQD"
+
+/datum/computer_file/data/fax_quick_dial/proc/save_quick_dial(var/list/quick_dial_list)
+ stored_data = json_encode(quick_dial_list)
+
+/datum/computer_file/data/fax_quick_dial/proc/load_quick_dial()
+ return json_decode(stored_data)
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Fax Machine
+////////////////////////////////////////////////////////////////////////////////////////
+/obj/machinery/faxmachine
+ name = "fax machine"
+ icon = 'icons/obj/machines/fax_machine.dmi'
+ icon_state = "fax"
+ anchored = TRUE
+ density = TRUE
+ idle_power_usage = 30
+ active_power_usage = 200
+ construct_state = /decl/machine_construction/default/panel_closed
+ maximum_component_parts = list(
+ /obj/item/stock_parts/item_holder/disk_reader = 1,
+ /obj/item/stock_parts/item_holder/card_reader = 1,
+ /obj/item/stock_parts/printer = 1,
+ /obj/item/stock_parts = 15,
+ )
+ stock_part_presets = list(
+ /decl/stock_part_preset/access_lock, //Empty access list
+ )
+ uncreated_component_parts = null
+ public_methods = list(
+ /decl/public_access/public_method/fax_receive_document,
+ )
+ var/obj/item/stock_parts/item_holder/disk_reader/disk_reader //Cached ref to the disk_reader component. Used for handling data disks.
+ var/obj/item/stock_parts/item_holder/card_reader/card_reader //Cached ref to the card_reader component. Used for scanning a user's id card.
+ var/obj/item/stock_parts/printer/printer //Cached ref to the printer component. Used for printing things.
+ var/obj/item/scanner_item //Item to fax
+ var/list/quick_dial //List of name tag to network ids for other fax machines that the user added as quick dial options
+ var/list/fax_history //List of the last 10 devices that sent us faxes, and when
+
+ var/tmp/time_cooldown_end = 0
+ var/tmp/current_page = "main"
+ var/tmp/dest_uri //The currently set destination URI for the target machine. Format is "[network_tag].[network_id]"
+
+/obj/machinery/faxmachine/Initialize(mapload, d=0, populate_parts = TRUE)
+ . = ..()
+ if(. != INITIALIZE_HINT_QDEL)
+ global.allfaxes += src
+ set_extension(src, /datum/extension/network_device/fax)
+ if(populate_parts && printer)
+ printer.make_full()
+
+/obj/machinery/faxmachine/Destroy()
+ global.allfaxes -= src
+ disk_reader = null
+ card_reader = null
+ printer = null
+ scanner_item = null
. = ..()
- if(!admin_departments)
- admin_departments = list("[global.using_map.boss_name]", "Sol Federal Police", "[global.using_map.boss_short] Supply") + global.using_map.map_admin_faxes
- global.allfaxes += src
- if(!destination)
- destination = "[global.using_map.boss_name]"
- if( !(("[department]" in global.alldepartments) || ("[department]" in admin_departments)))
- global.alldepartments |= department
-
-/obj/machinery/photocopier/faxmachine/attackby(obj/item/O, mob/user)
- if(istype(O, /obj/item/card/id))
- if(!user.unEquip(O, src))
- return
- scan = O
- to_chat(user, "You insert \the [O] into \the [src].")
+/obj/machinery/faxmachine/on_update_icon()
+ if(!QDELETED(scanner_item))
+ icon_state = "faxpaper" //not using an overlay, because animations
else
- ..()
+ icon_state = initial(icon_state)
-/obj/machinery/photocopier/faxmachine/interface_interact(mob/user)
- interact(user)
+/obj/machinery/faxmachine/RefreshParts()
+ . = ..()
+ disk_reader = get_component_of_type(/obj/item/stock_parts/item_holder/disk_reader)
+ card_reader = get_component_of_type(/obj/item/stock_parts/item_holder/card_reader)
+ printer = get_component_of_type(/obj/item/stock_parts/printer)
+
+ if(disk_reader)
+ disk_reader.register_on_insert(CALLBACK(src, /obj/machinery/faxmachine/proc/on_insert_disk))
+ disk_reader.register_on_eject( CALLBACK(src, /obj/machinery/faxmachine/proc/update_ui))
+
+ if(card_reader)
+ card_reader.register_on_insert(CALLBACK(src, /obj/machinery/faxmachine/proc/on_insert_card))
+ card_reader.register_on_eject( CALLBACK(src, /obj/machinery/faxmachine/proc/update_ui))
+
+ if(printer)
+ printer.register_on_printed_page( CALLBACK(src, /obj/machinery/faxmachine/proc/on_printed_page))
+ printer.register_on_finished_queue(CALLBACK(src, /obj/machinery/faxmachine/proc/on_queue_finished))
+ printer.register_on_print_error( CALLBACK(src, /obj/machinery/faxmachine/proc/on_print_error))
+ printer.register_on_status_changed(CALLBACK(src, /obj/machinery/faxmachine/proc/update_ui))
+
+/obj/machinery/faxmachine/interface_interact(mob/user)
+ ui_interact(user)
return TRUE
-/obj/machinery/photocopier/faxmachine/interact(mob/user)
- user.set_machine(src)
-
- var/dat = "Fax Machine
"
-
- var/scan_name
- if(scan)
- scan_name = scan.name
- else
- scan_name = "--------"
-
- dat += "Confirm Identity: [scan_name]
"
-
- if(authenticated)
- dat += "{Log Out}"
- else
- dat += "{Log In}"
-
- dat += "
"
+/obj/machinery/faxmachine/attackby(obj/item/I, mob/user)
+ if(istype(construct_state, /decl/machine_construction/default/panel_closed))
+ if(istype(I, /obj/item/paper) || istype(I, /obj/item/photo) || istype(I, /obj/item/paper_bundle))
+ return insert_scanner_item(I, user)
+ . = ..()
- if(authenticated)
- dat += "Logged in to: [global.using_map.boss_name] Quantum Entanglement Network
"
+/obj/machinery/faxmachine/ui_data(mob/user, ui_key)
+ . = ..()
+ var/datum/extension/network_device/fax/net = get_extension(src, /datum/extension/network_device)
+ LAZYADD(., net.ui_data(user)) //Grab some of the network data stuff
+
+ //Convert the list to something we can use in the nanoui
+ var/list/uiqd
+ for(var/key in quick_dial)
+ var/list/element = list("key" = key, "value" = quick_dial[key])
+ LAZYADD(uiqd, list(element))
+
+ LAZYSET(., "quick_dial_targets", uiqd)
+ LAZYSET(., "fax_history", fax_history)
+ LAZYSET(., "scanner_item", "[!QDELETED(scanner_item)? scanner_item : ""]")
+ LAZYSET(., "is_cooling_down", (time_cooldown_end > world.timeofday))
+ LAZYSET(., "dest_uri", dest_uri)
+ LAZYSET(., "src", "\ref[src]")
+ LAZYSET(., "emagged", emagged)
+ LAZYSET(., "is_operational", operable())
+ LAZYSET(., "page", current_page)
+
+ //Printer stuff
+ if(printer)
+ LAZYADD(., printer.ui_data(user))
+
+ //Card reader stuff
+ var/obj/item/card/C = card_reader?.get_inserted()
+ LAZYSET(., "has_card_reader", !isnull(card_reader))
+ LAZYSET(., "id_card", C)
+ LAZYSET(., "data_card", card_reader?.get_data_card())
+ if(C)
+ var/info = C.name
+ if(istype(C, /obj/item/card/id))
+ var/obj/item/card/id/ID = C
+ info = ID.get_display_name()
+ LAZYSET(., "id_card_info", info)
+
+ //Disk stuff
+ var/obj/item/disk/D = disk_reader?.get_inserted()
+ LAZYSET(., "has_disk_drive", !isnull(disk_reader))
+ LAZYSET(., "disk", D)
+ LAZYSET(., "disk_name", D?.name)
+ LAZYSET(., "disk_has_qd", D?.contains_file_type("FQD")) //If the disk has a quick dial file
+ LAZYSET(., "disk_has_file", (D?.free_blocks < D?.block_capacity))
+
+/obj/machinery/faxmachine/ui_interact(mob/user, ui_key, datum/nanoui/ui, force_open, datum/nanoui/master_ui, datum/topic_state/state)
+ var/list/data = ui_data(user, ui_key)
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "fax_machine.tmpl", name, 640, 480)
+ ui.add_template("net_shared", "network_shared.tmpl") //Shared network UI stuff
+ ui.add_template("stock_parts_printer_shared", "stock_parts_printer.tmpl")
+ ui.set_initial_data(data)
+ ui.open()
+
+/obj/machinery/faxmachine/DefaultTopicState()
+ return global.physical_topic_state
+
+/obj/machinery/faxmachine/OnTopic(mob/user, list/href_list, datum/topic_state/state)
+ if(!CanInteract(user, state))
+ to_chat(user, SPAN_WARNING("You must be close to \the [src] to do this!"))
+ return TOPIC_NOACTION
+
+ if(href_list["change_page"])
+ var/pagename = href_list["change_page"]
+ current_page = pagename
+ return TOPIC_REFRESH
- if(copyitem)
- dat += "Remove Item
"
+ if(href_list["network_settings"])
+ var/datum/extension/network_device/fax/net = get_extension(src, /datum/extension/network_device)
+ net.ui_interact(user)
+ return TOPIC_REFRESH
- if(sendcooldown)
- dat += "Transmitter arrays realigning. Please stand by.
"
+ // --- Sending Fax ---
+ if(href_list["send"])
+ if(QDELETED(scanner_item))
+ to_chat(user, SPAN_WARNING("You must insert something to send first!"))
+ return TOPIC_NOACTION
+ if(world.timeofday < time_cooldown_end)
+ to_chat(user, SPAN_WARNING("\The [src] isn't ready yet for sending again! [max(0, time_cooldown_end - world.timeofday) / (1 SECOND)] second\s left."))
+ return TOPIC_REFRESH
+
+ //Prioritize the quick dial value
+ dest_uri = replacetext(sanitize(href_list["quick_dial"], encode = FALSE), " ", "_")
+ if(!length(dest_uri))
+ dest_uri = replacetext(sanitize(href_list["network_uri"], encode = FALSE), " ", "_")
+ if(!length(dest_uri))
+ to_chat(user, SPAN_WARNING("You must specify a destination!"))
+ return TOPIC_NOACTION
+
+ var/list/parsed = parse_network_uri(dest_uri)
+ if(!length(parsed))
+ to_chat(user, SPAN_WARNING("Bad target URI!"))
+ return TOPIC_NOACTION
+ send_fax(user, parsed["network_tag"], parsed["network_id"])
+ return TOPIC_REFRESH
+ // --- Insert/Eject ---
+ if(href_list["id_card"])
+ if(card_reader)
+ var/obj/item/card/C = card_reader.get_inserted()
+ if(C)
+ eject_card(user)
else
+ var/obj/item/card/id/ID = user.get_active_hand()
+ if(!istype(ID) && !istype(ID, /obj/item/card/data))
+ to_chat(user, SPAN_WARNING("You need to hold a valid id/data card!"))
+ else if(card_reader.should_swipe)
+ to_chat(user, SPAN_WARNING("\The [card_reader] is currently set to swipe mode, which is unsupported by this machine. Please contact your system administrator."))
+ else
+ card_reader.insert_item(ID, user)
+ else
+ to_chat(user, SPAN_WARNING("The card reader is not responding!"))
+ return TOPIC_NOACTION
+ return TOPIC_REFRESH
- dat += "Send
"
- dat += "Currently sending: [copyitem.name]
"
- dat += "Sending to: [destination]
"
-
+ if(href_list["insert_item"])
+ if(!QDELETED(scanner_item))
+ to_chat(user, SPAN_WARNING("There's already a [scanner_item] in \the [src]!"))
+ return TOPIC_NOACTION
else
- if(sendcooldown)
- dat += "Please insert paper to send via secure connection.
"
- dat += "Transmitter arrays realigning. Please stand by.
"
+ var/obj/item/I = user.get_active_hand()
+ if(I)
+ insert_scanner_item(I, user)
else
- dat += "Please insert paper to send via secure connection.
"
-
- else
- dat += "Proper authentication is required to use this device.
"
-
- if(copyitem)
- dat += "Remove Item
"
+ to_chat(user, SPAN_WARNING("You're not holding anything!"))
+ return TOPIC_NOACTION
+ return TOPIC_REFRESH
- show_browser(user, dat, "window=copier")
- onclose(user, "copier")
- return
-
-/obj/machinery/photocopier/faxmachine/OnTopic(mob/user, href_list, state)
- if(href_list["send"])
- if(copyitem)
- if (destination in admin_departments)
- send_admin_fax(user, destination)
- else
- sendfax(destination)
+ if(href_list["remove_item"])
+ eject_scanner_item(user)
+ return TOPIC_REFRESH
- if (sendcooldown)
- spawn(sendcooldown) // cooldown time
- sendcooldown = 0
+ //Handle extra disk ops here
+ if(disk_reader && (OnTopic_DiskOps(user, href_list, state) != TOPIC_NOACTION))
return TOPIC_REFRESH
- if(href_list["remove"])
- OnRemove(user)
+ if(printer?.OnTopic(user, href_list, state) != TOPIC_NOACTION)
return TOPIC_REFRESH
- if(href_list["scan"])
- if (scan)
- if(ishuman(user))
- user.put_in_hands(scan)
- else
- scan.dropInto(loc)
- scan = null
+/**Handle disk operations topics. */
+/obj/machinery/faxmachine/proc/OnTopic_DiskOps(mob/user, list/href_list, datum/topic_state/state)
+ if(href_list["insert_disk"])
+ if(!disk_reader)
+ to_chat(user, SPAN_WARNING("There is no disk drive installed on \the [src]!"))
+ return TOPIC_NOACTION
+ var/obj/item/disk/D = user.get_active_hand()
+ if(!istype(D))
+ to_chat(user, SPAN_WARNING("You need to hold a valid data disk!"))
+ return TOPIC_NOACTION
else
- var/obj/item/I = user.get_active_hand()
- if (istype(I, /obj/item/card/id) && user.unEquip(I, src))
- scan = I
- authenticated = 0
+ disk_reader.insert_item(D, user)
return TOPIC_REFRESH
-
- if(href_list["dept"])
- var/desired_destination = input(user, "Which department?", "Choose a department", "") as null|anything in (global.alldepartments + admin_departments)
- if(desired_destination && CanInteract(user, state))
- destination = desired_destination
+
+ if(href_list["eject_disk"])
+ eject_disk(user)
return TOPIC_REFRESH
- if(href_list["auth"])
- if ( (!( authenticated ) && (scan)) )
- if (has_access(send_access, scan.GetAccess()))
- authenticated = 1
- return TOPIC_REFRESH
+ // --- Quick Dial Operations ---
+ if(href_list["add_qd"])
+ var/qduri = uppertext(replacetext(sanitize(href_list["network_uri"], encode = FALSE), " ", "_"))
+ if(!length(qduri))
+ to_chat(user, SPAN_WARNING("Please specify a destination URI!"))
+ return TOPIC_NOACTION
- if(href_list["logout"])
- authenticated = 0
+ var/inputname = input(user, "Name for the new quick dial contact?", "New quick dial contact")
+ if(length(inputname) && CanPhysicallyInteract(user))
+ add_quick_dial_contact(sanitize_name(inputname, MAX_NAME_LEN, TRUE, TRUE), qduri, user)
return TOPIC_REFRESH
-/obj/machinery/photocopier/faxmachine/proc/sendfax(var/destination)
- if(stat & (BROKEN|NOPOWER))
+ if(href_list["rem_qd"])
+ var/qduri = uppertext(replacetext(sanitize(href_list["quick_dial"], encode = FALSE), " ", "_"))
+ var/qdname
+ for(var/key in quick_dial)
+ if(quick_dial[key] == qduri)
+ qdname = key
+ break
+ rem_quick_dial_contact(qdname, user)
+ return TOPIC_REFRESH
+ return TOPIC_NOACTION
+
+/**Returns the inserted card if there is a reader and if there is a card. Otherwise print a warning to the user, if a user was passed */
+/obj/machinery/faxmachine/proc/try_get_card(var/mob/user)
+ var/obj/item/card/C = card_reader?.get_inserted()
+ if(!card_reader)
+ if(user)
+ to_chat(user, SPAN_WARNING("There is no card reader!"))
+ return
+ else if(!C)
+ if(user)
+ to_chat(user, SPAN_WARNING("There is no card in \the [card_reader]!"))
+ return
+ return C
+
+/**Warn the user if the disk cannot be accessed. Otherwise returns the disk in the disk reader. */
+/obj/machinery/faxmachine/proc/try_get_disk(var/mob/user)
+ var/obj/item/disk/D = disk_reader?.get_inserted()
+ if(!disk_reader)
+ if(user)
+ to_chat(user, SPAN_WARNING("There is no disk drive!"))
+ return
+ else if(!D)
+ if(user)
+ to_chat(user, SPAN_WARNING("There is no disk in \the [disk_reader]!"))
return
+ return D
+
+/obj/machinery/faxmachine/proc/insert_scanner_item(var/obj/item/I, var/mob/user)
+ if(!QDELETED(scanner_item))
+ if(user)
+ to_chat(user, SPAN_WARNING("\The [src] already has something being scanned!"))
+ return FALSE
+
+ if(user)
+ to_chat(user, SPAN_NOTICE("You place \the [I] into \the [src]'s scanner."))
+ if(!user.unEquip(I, src))
+ return FALSE
+ else
+ I.dropInto(src)
+ scanner_item = I
+ update_ui()
+ return TRUE
+
+/obj/machinery/faxmachine/proc/eject_scanner_item(var/mob/user)
+ if(QDELETED(scanner_item))
+ if(user)
+ to_chat(user, SPAN_WARNING("There's nothing to eject from \the [src]."))
+ return FALSE
+
+ if(user)
+ to_chat(user, SPAN_NOTICE("You eject \the [scanner_item] from \the [src]."))
+ user.put_in_hands(scanner_item)
+ else
+ scanner_item.dropInto(get_turf(src))
+ scanner_item = null
+ update_ui()
+ return TRUE
- use_power_oneoff(200)
+/obj/machinery/faxmachine/proc/on_insert_card(var/obj/item/card/C, var/mob/user)
+ if(card_reader.should_swipe) //We don't support swiping
+ if(user)
+ to_chat(user, SPAN_WARNING("\The [card_reader] is currently set to swipe mode, which is unsupported by this machine. Please contact your system administrator."))
+ return
+ if(user)
+ to_chat(user, SPAN_NOTICE("Loading \the '[C]'..."))
+ update_ui()
+
+/obj/machinery/faxmachine/proc/eject_card(var/mob/user)
+ if(!card_reader)
+ if(user)
+ to_chat(user, SPAN_WARNING("\The [src] has no card reader installed."))
+ return FALSE
+ card_reader.eject_item(user)
+ update_ui()
+ return TRUE
+
+/obj/machinery/faxmachine/proc/on_insert_disk(var/obj/item/disk/D, var/mob/user)
+ //Read any existing disk data
+ var/datum/computer_file/data/qdfile = D.read_file(FAX_QUICK_DIAL_FILE)
+ if(qdfile)
+ quick_dial = json_decode(qdfile.stored_data)
+
+ var/datum/computer_file/data/histfile = D.read_file(FAX_HISTORY_FILE)
+ if(histfile)
+ fax_history = json_decode(histfile.stored_data)
+ update_ui()
+
+/obj/machinery/faxmachine/proc/eject_disk(var/mob/user)
+ if(!disk_reader)
+ to_chat(user, SPAN_WARNING("\The [src] has no disk drive installed."))
+ return FALSE
+ disk_reader.eject_item(user)
+ LAZYCLEARLIST(quick_dial)
+ LAZYCLEARLIST(fax_history)
+ update_ui()
+ return TRUE
- var/success = 0
- for(var/obj/machinery/photocopier/faxmachine/F in global.allfaxes)
- if( F.department == destination )
- success = F.recievefax(copyitem)
+/obj/machinery/faxmachine/proc/send_fax(var/mob/user, var/target_net_tag, var/target_net_id)
+ if(QDELETED(scanner_item))
+ to_chat(user, SPAN_WARNING("You need to insert something to fax first!"))
+ return FALSE
+ if(inoperable())
+ return FALSE
+ if(world.timeofday < time_cooldown_end)
+ to_chat(user, SPAN_WARNING("Cannot send while busy! [max(0, world.timeofday - time_cooldown_end) / (1 SECOND)] second\s remaining."))
+ return FALSE
+
+ //Try to get enough power
+ if(can_use_power_oneoff(active_power_usage) > 0)
+ return FALSE
+ use_power_oneoff(active_power_usage)
- if (success)
- visible_message("[src] beeps, \"Message transmitted successfully.\"")
- //sendcooldown = 600
+ //Grab our network
+ var/datum/extension/network_device/sender_dev = get_extension(src, /datum/extension/network_device)
+ var/datum/computer_network/origin_network = sender_dev?.get_network()
+ if(!origin_network)
+ to_chat(user, SPAN_WARNING("No network connection!"))
+ return FALSE
+
+ if(!length(target_net_id))
+ target_net_id = origin_network.network_id //Use the current network if none specified
+
+ if((target_net_id != origin_network.network_id) && !sender_dev.has_internet_connection(origin_network))
+ to_chat(user, SPAN_WARNING("No internet connection!"))
+ return FALSE
+
+ var/obj/item/card/id/ID = try_get_card()
+ ID = istype(ID)? ID : null
+
+ //If sending to admins, don't try to get a target network or device
+ var/target_URI = uppertext("[target_net_tag].[target_net_id]")
+ var/list/target_admin_fax = LAZYACCESS(global.using_map.map_admin_faxes, target_URI)
+ if(target_admin_fax)
+ var/list/fax_req_access = target_admin_fax["access"]
+ if(!emagged)
+ if(!has_access(req_access, ID?.GetAccess()))
+ to_chat(user, SPAN_WARNING("You do not have the right credentials to use the send function on this device!"))
+ return FALSE
+ if(!has_access(fax_req_access, ID?.GetAccess()))
+ to_chat(user, SPAN_WARNING("You do not have the right credentials to send a fax to this recipient!"))
+ return FALSE
+ else if(prob(1))
+ spark_at(src, 1)
+
+ .= send_fax_to_admin(user, scanner_item, target_URI, src)
+ log_history("Outgoing", "'[scanner_item]', from '[get_area(src)]'s [src]' @ '[sender_dev.get_network_URI()]' to [target_admin_fax["name"]]' @ '[target_URI]'.")
+ update_ui()
+ flick("faxsend", src)
+ //#TODO: sync the animation, sound and message together when I got enough patience.
+ playsound(src, 'sound/machines/fax_send.ogg', 30, FALSE, 0, 4)
+ ping("Message transmitted successfully!")
+ time_cooldown_end = world.timeofday + FAX_ADMIN_COOLDOWN
+ return TRUE
+
+ var/datum/computer_network/target_net
+ if(target_net_id != origin_network.network_id)
+ target_net = origin_network.get_internet_connection(target_net_id)
else
- visible_message("[src] beeps, \"Error transmitting message.\"")
+ target_net = origin_network
+
+ if(!target_net)
+ to_chat(user, SPAN_WARNING("No such network '[target_net_id]'!"))
+ return FALSE
+
+ var/datum/extension/network_device/fax/target_dev = target_net.get_device_by_tag(target_net_tag)
+ if(!target_dev)
+ to_chat(user, SPAN_WARNING("No such devices '[target_net_tag]'!"))
+ return FALSE
+ if(!istype(target_dev) || !target_dev.has_commands)
+ to_chat(user, SPAN_WARNING("Invalid target device '[target_net_tag]'!"))
+ return FALSE
+ var/obj/machinery/target = target_dev.holder
+ if(!target)
+ CRASH("There is a network_device extension without a holder!")
+
+ //Authenticate as needed
+ if(!emagged)
+ if(!has_access(req_access, ID?.GetAccess()))
+ to_chat(user, SPAN_WARNING("You do not have the right credentials to use the send function on this device!"))
+ return FALSE
+ if(!has_access(target.req_access, ID?.GetAccess()))
+ to_chat(user, SPAN_WARNING("You do not have the right credentials to send a fax to this recipient!"))
+ return FALSE
+ else if(prob(1))
+ spark_at(src, 1)
+
+ //Setup cooldown
+ time_cooldown_end = world.timeofday + FAX_COOLDOWN
+
+ //Call receive on the target machine
+ var/decl/public_access/public_method/send_method = GET_DECL(/decl/public_access/public_method/fax_receive_document)
+ if(send_method.perform(target, scanner_item.Clone(), "[get_area(src)]'s [src]", sender_dev.get_network_URI()))
+ log_history("Outgoing", "'[scanner_item]', from '[get_area(src)]'s [src]' @ '[sender_dev.get_network_URI()]' to '[get_area(target)]'s [target]' @ '[target_dev.get_network_URI()]'.")
+ //#TODO: sync the animation, sound and message together when I got enough patience.
+ ping("Message transmitted successfully.")
+ playsound(src, 'sound/machines/fax_send.ogg', 30, FALSE, 0, 4)
+ update_ui()
+ flick("faxsend", src)
+ return TRUE
+ else if(target.inoperable())
+ buzz("Error transmitting message, receiving machine won't reply.")
+ else
+ buzz("Error transmitting message.")
+ return FALSE
-/obj/machinery/photocopier/faxmachine/proc/recievefax(var/obj/item/incoming)
- if(stat & (BROKEN|NOPOWER))
- return 0
+/**Handles queuing any received fax message in the printer queue, and starts the printer if needed. */
+/obj/machinery/faxmachine/proc/receive_fax(var/obj/item/incoming, var/source_name = "unknown", var/source_URI = "unknown.unknown")
+ if(inoperable())
+ return FALSE
- if(department == "Unknown")
- return 0 //You can't send faxes to "Unknown"
+ //Try to get enough power
+ if(can_use_power_oneoff(active_power_usage) > 0)
+ return FALSE
- flick("faxreceive", src)
- playsound(loc, "sound/machines/dotprinter.ogg", 50, 1)
+ printer?.queue_job(incoming) //Add it to the printer queue so we don't lose it on failure
+ if(printer?.is_printing())
+ return TRUE //Don't do anything extra if we're already printing
- // give the sprite some time to flick
- sleep(20)
+ if(!printer?.has_enough_to_print())
+ buzz("Error while printing, not enough toner or paper to print the received message! Please refill, and resume printing!")
+ return FALSE
+
+ //Tell the printer to do its job
+ printer.start_printing_queue()
+ use_power_oneoff(active_power_usage)
+ var/datum/extension/network_device/fax/ND = get_extension(src, /datum/extension/network_device)
+ log_history("Incoming", "'[incoming]', from '[source_name]'@'[source_URI]' to '[get_area(src)]'s [src]'@'[ND.get_network_URI()]'.")
+ return TRUE
- if (istype(incoming, /obj/item/paper))
- copy(incoming)
- else if (istype(incoming, /obj/item/photo))
- photocopy(incoming)
- else if (istype(incoming, /obj/item/paper_bundle))
- bundlecopy(incoming)
+/**Cause the nanoui to update, also updates the icon of the machine. */
+/obj/machinery/faxmachine/proc/update_ui()
+ SSnano.update_uis(src)
+ update_icon()
+
+/**Check if the card we inserted has enough credentials to print on the target fax machine on the network. */
+/obj/machinery/faxmachine/proc/can_send_fax_to(var/network_tag, var/network_id, var/list/provided_access)
+ var/datum/extension/network_device/fax/sender = get_extension(src, /datum/extension/network_device)
+ var/datum/computer_network/sender_net = sender?.get_network()
+ if(!sender_net)
+ return FALSE
+ if((network_id != sender_net.network_id) && !sender.has_internet_connection(network_id))
+ return FALSE
+
+ //Handle fake admin network addresses
+ var/target_uri = uppertext("[network_tag].[network_id]")
+ if(target_uri in global.using_map.map_admin_faxes)
+ var/list/admin_faxes = LAZYACCESS(global.using_map.map_admin_faxes, target_uri)
+ var/list/required_access = LAZYACCESS(admin_faxes, "access")
+ return has_access(required_access, provided_access) //With access we can send faxes to the selected admin address
+
+ var/datum/computer_network/target_net
+ if(network_id != sender_net.network_id)
+ target_net = sender_net?.get_internet_connection(network_id)
else
- return 0
+ target_net = sender_net
+ var/datum/extension/network_device/fax/target = target_net?.get_device_by_tag(network_tag)
+ return istype(target) && target.has_access(provided_access)
- use_power_oneoff(active_power_usage)
- return 1
+/**Plays print animation async. */
+/obj/machinery/faxmachine/proc/on_printed_page()
+ flick("faxreceive", src)
-/obj/machinery/photocopier/faxmachine/proc/send_admin_fax(var/mob/sender, var/destination)
- if(stat & (BROKEN|NOPOWER))
+/**Tells the user we just printed the last page of the queue, and do the print anim. */
+/obj/machinery/faxmachine/proc/on_queue_finished()
+ if(QDELETED(src))
return
+ state("Printing tasks complete!")
+ on_printed_page()
- use_power_oneoff(200)
-
- //recieved copies should not use toner since it's being used by admins only.
- var/obj/item/rcvdcopy
- if (istype(copyitem, /obj/item/paper))
- rcvdcopy = copy(copyitem, 0)
- else if (istype(copyitem, /obj/item/photo))
- rcvdcopy = photocopy(copyitem, 0)
- else if (istype(copyitem, /obj/item/paper_bundle))
- rcvdcopy = bundlecopy(copyitem, 0)
+/**Warn the user that something interrupted printing. */
+/obj/machinery/faxmachine/proc/on_print_error()
+ if(!printer?.has_enough_to_print())
+ buzz("Error while printing, not enough toner or paper to print the received message! Please refill, and resume printing!")
else
- visible_message("[src] beeps, \"Error transmitting message.\"")
+ buzz("Error while printing!")
+
+/obj/machinery/faxmachine/proc/log_history(var/operation, var/text)
+ LAZYADD(fax_history, "[stationdate2text()], [stationtime2text()]: [operation] - [text]")
+ var/obj/item/disk/D = disk_reader?.get_inserted()
+ if(!D)
return
+ var/datum/computer_file/data/hist = new
+ hist.stored_data = json_encode(fax_history)
+ D.write_file(hist, FAX_HISTORY_FILE)
+
+/obj/machinery/faxmachine/proc/add_quick_dial_contact(var/contact_name, var/contact_URI, var/mob/user)
+ if(!length(contact_URI))
+ if(user)
+ to_chat(user, SPAN_WARNING("Please specify a destination URI!"))
+ return FALSE
+
+ if(!length(contact_name))
+ if(user)
+ to_chat(user, SPAN_WARNING("Please specify a valid name for the contact!"))
+ return FALSE
+ LAZYSET(quick_dial, contact_name, contact_URI)
+
+ //Overwrite quick dial
+ var/obj/item/disk/D = try_get_disk()
+ if(D)
+ var/datum/computer_file/data/datfile = new
+ datfile.stored_data = json_encode(quick_dial)
+ D.write_file(datfile, FAX_QUICK_DIAL_FILE)
+
+ if(user)
+ to_chat(user, SPAN_NOTICE("New contact '[contact_name]' with URI '[contact_URI]' added successfully!"))
+ update_ui()
+ return TRUE
- rcvdcopy.forceMove(null) //hopefully this shouldn't cause trouble
- global.adminfaxes += rcvdcopy
-
- //message badmins that a fax has arrived
- if (destination == global.using_map.boss_name)
- message_admins(sender, "[uppertext(destination)] FAX", rcvdcopy, destination, "#006100")
- else if (destination == "Sol Federal Police")
- message_admins(sender, "[uppertext(destination)] FAX", rcvdcopy, destination, "#1f66a0")
- else if (destination == "[global.using_map.boss_short] Supply")
- message_admins(sender, "[uppertext(destination)] FAX", rcvdcopy, destination, "#5f4519")
- else if (destination in global.using_map.map_admin_faxes)
- message_admins(sender, "[uppertext(destination)] FAX", rcvdcopy, destination, "#510b74")
- else
- message_admins(sender, "[uppertext(destination)] FAX", rcvdcopy, "UNKNOWN")
+/obj/machinery/faxmachine/proc/rem_quick_dial_contact(var/contact_name, var/mob/user)
+ if(!length(contact_name))
+ if(user)
+ to_chat(user, SPAN_WARNING("Please select a contact to remove!"))
+ return FALSE
+ LAZYREMOVE(quick_dial, contact_name)
+
+ //Overwrite quick dial
+ var/obj/item/disk/D = try_get_disk()
+ if(D)
+ var/datum/computer_file/data/datfile = new
+ datfile.stored_data = json_encode(quick_dial)
+ D.write_file(datfile, FAX_QUICK_DIAL_FILE)
+
+ if(user)
+ to_chat(user, SPAN_NOTICE("Successfully removed contact '[contact_name]' with URI '[LAZYACCESS(quick_dial, contact_name)]'!"))
+ update_ui()
+ return TRUE
- sendcooldown = 1800
- sleep(50)
- visible_message("[src] beeps, \"Message transmitted successfully.\"")
+////////////////////////////////////////////////////////////////////////////////////////
+// Public Methods
+////////////////////////////////////////////////////////////////////////////////////////
+/decl/public_access/public_method/fax_receive_document
+ name = "Send Fax Message"
+ desc = "Sends the specified document over to the specified network tag."
+ call_proc = /obj/machinery/faxmachine/proc/receive_fax
+ forward_args = TRUE
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Admin Faxes Handling
+////////////////////////////////////////////////////////////////////////////////////////
+
+/**Helper for sending a fax from a fax machine to an admin destination. */
+/proc/send_fax_to_admin(var/mob/sender, var/obj/item/doc, var/network_URI, var/obj/machinery/faxmachine/source_fax)
+ var/obj/item/rcvdcopy = doc.Clone()
+ if(!rcvdcopy)
+ return
+ rcvdcopy.forceMove(null)
+ LAZYADD(global.adminfaxes, rcvdcopy)
+
+ var/list/fax_details = LAZYACCESS(global.using_map?.map_admin_faxes, network_URI)
+ var/dest_display_name = LAZYACCESS(fax_details, "name") || network_URI
+ var/font_colour = LAZYACCESS(fax_details, "color") || "#006100"
+ var/faxname = "[uppertext(dest_display_name)] FAX"
+ var/reply_type = dest_display_name
+ if(!(network_URI in global.using_map.map_admin_faxes))
+ reply_type = "UNKNOWN"
-/obj/machinery/photocopier/faxmachine/proc/message_admins(var/mob/sender, var/faxname, var/obj/item/sent, var/reply_type, font_colour="#006100")
var/msg = "[faxname]: [get_options_bar(sender, 2,1,1)]"
- msg += "(TAKE) (REPLY): "
- msg += "Receiving '[sent.name]' via secure connection ... view message"
+ msg += "(TAKE) (REPLY): "
+ msg += "Receiving '[rcvdcopy.name]' via secure connection ... view message"
for(var/client/C in global.admins)
if(check_rights((R_ADMIN|R_MOD),0,C))
to_chat(C, msg)
sound_to(C, 'sound/machines/dotprinter.ogg')
+ return TRUE
+
+#undef FAX_QUICK_DIAL_FILE
+#undef FAX_HISTORY_FILE
+#undef FAX_COOLDOWN
+#undef FAX_ADMIN_COOLDOWN
\ No newline at end of file
diff --git a/code/modules/paperwork/filingcabinet.dm b/code/modules/paperwork/filingcabinet.dm
index a2e85b6c012..58bb1b3e759 100644
--- a/code/modules/paperwork/filingcabinet.dm
+++ b/code/modules/paperwork/filingcabinet.dm
@@ -1,81 +1,132 @@
-/* Filing cabinets!
- * Contains:
- * Filing Cabinets
- * Security Record Cabinets
- * Medical Record Cabinets
- */
-
-/*
- * Filing Cabinets
- */
-/obj/structure/filingcabinet
- name = "filing cabinet"
- desc = "A large cabinet with drawers."
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "filingcabinet"
- density = 1
- anchored = 1
- atom_flags = ATOM_FLAG_NO_TEMP_CHANGE | ATOM_FLAG_CLIMBABLE
- obj_flags = OBJ_FLAG_ANCHORABLE
- var/list/can_hold = list(
+/////////////////////////////////////////////////////////////////
+// Filling Cabinet
+/////////////////////////////////////////////////////////////////
+/obj/structure/filing_cabinet
+ name = "filing cabinet"
+ desc = "A large cabinet with drawers."
+ icon = 'icons/obj/structures/filling_cabinets.dmi'
+ icon_state = "filingcabinet"
+ material = /decl/material/solid/metal/steel
+ density = TRUE
+ anchored = TRUE
+ atom_flags = ATOM_FLAG_NO_TEMP_CHANGE | ATOM_FLAG_CLIMBABLE
+ obj_flags = OBJ_FLAG_ANCHORABLE
+ tool_interaction_flags = TOOL_INTERACTION_ANCHOR | TOOL_INTERACTION_DECONSTRUCT
+ var/tmp/list/can_hold = list(
/obj/item/paper,
/obj/item/folder,
/obj/item/photo,
/obj/item/paper_bundle,
- /obj/item/forensics/sample)
+ /obj/item/forensics/sample
+ )
-/obj/structure/filingcabinet/chestdrawer
- name = "chest drawer"
- icon_state = "chestdrawer"
-
-/obj/structure/filingcabinet/wallcabinet
- name = "wall-mounted filing cabinet"
- desc = "A filing cabinet installed into a cavity in the wall to save space. Wow!"
- icon_state = "wallcabinet"
- density = 0
- obj_flags = 0
-
-
-/obj/structure/filingcabinet/filingcabinet //not changing the path to avoid unecessary map issues, but please don't name stuff like this in the future -Pete
- icon_state = "tallcabinet"
-
-
-/obj/structure/filingcabinet/Initialize()
- for(var/obj/item/I in loc)
- if(is_type_in_list(I, can_hold))
- I.forceMove(src)
+/obj/structure/filing_cabinet/Initialize(ml, _mat, _reinf_mat)
+ if(ml)
+ for(var/obj/item/I in loc)
+ if(is_type_in_list(I, can_hold))
+ I.forceMove(src)
. = ..()
-/obj/structure/filingcabinet/attackby(obj/item/P, mob/user)
+/obj/structure/filing_cabinet/attackby(obj/item/P, mob/user)
if(is_type_in_list(P, can_hold))
if(!user.unEquip(P, src))
return
add_fingerprint(user)
- to_chat(user, "
You put [P] in [src].")
+ to_chat(user, SPAN_NOTICE("You put [P] in [src]."))
flick("[initial(icon_state)]-open",src)
updateUsrDialog()
- else
- ..()
-
-/obj/structure/filingcabinet/attack_hand(mob/user)
- if(contents.len <= 0)
- to_chat(user, "
\The [src] is empty.")
- return
+ return TRUE
+ return ..()
+/obj/structure/filing_cabinet/interact(mob/user)
user.set_machine(src)
- var/dat = list("
")
+ var/dat = "
"
for(var/obj/item/P in src)
- dat += "| [P.name] |
"
- dat += "
"
- show_browser(user, "[name][jointext(dat,null)]", "window=filingcabinet;size=350x300")
+ dat += "| [P.name] |
"
+ dat += "
"
+ show_browser(user, "[name][dat]", "window=filingcabinet;size=350x300")
-/obj/structure/filingcabinet/Topic(href, href_list)
- if(href_list["retrieve"])
- show_browser(usr, "", "window=filingcabinet") // Close the menu
+/obj/structure/filing_cabinet/attack_hand(mob/user)
+ return interact(user)
- //var/retrieveindex = text2num(href_list["retrieve"])
- var/obj/item/P = locate(href_list["retrieve"])//contents[retrieveindex]
- if(istype(P) && (P.loc == src) && src.Adjacent(usr))
- usr.put_in_hands(P)
+/obj/structure/filing_cabinet/OnTopic(mob/user, href_list, datum/topic_state/state)
+ if(href_list["retrieve"])
+ close_browser(user, "window=filingcabinet")
+ var/obj/item/P = locate(href_list["retrieve"])
+ if(istype(P) && CanPhysicallyInteractWith(user, src))
+ user.put_in_hands(P)
+ flick("[initial(icon_state)]-open", src)
updateUsrDialog()
- flick("[initial(icon_state)]-open",src)
+ . = TOPIC_REFRESH
+
+// Subtypes for mapping!
+/obj/structure/filing_cabinet/chestdrawer
+ name = "chest drawer"
+ icon_state = "chestdrawer"
+
+/obj/structure/filing_cabinet/wall
+ name = "wall-mounted filing cabinet"
+ desc = "A filing cabinet installed into a cavity in the wall to save space. Wow!"
+ icon_state = "wallcabinet"
+ obj_flags = OBJ_FLAG_MOVES_UNSUPPORTED
+ directional_offset = "{'NORTH':{'y':-32}, 'SOUTH':{'y':32}, 'EAST':{'x':-32}, 'WEST':{'x':32}}"
+
+/obj/structure/filing_cabinet/tall
+ icon_state = "tallcabinet"
+
+// Record cabinets fill with paper records on first interaction.
+/obj/structure/filing_cabinet/records
+ name = "security record archive"
+ var/generated = FALSE
+ var/archive_name = "security record"
+
+// We generate records on first interaction, as latejoin,
+// joining crew, etc. are hard to plan for and order around.
+// It's also a fair few atoms to worry about.
+/obj/structure/filing_cabinet/records/attack_hand(mob/user)
+ if(!generated)
+ generate_records()
+ return ..()
+
+/obj/structure/filing_cabinet/records/proc/generate_records()
+ generated = TRUE
+ var/datum/computer_network/network = get_local_network_at(get_turf(src))
+ if(!network)
+ return
+ for(var/datum/computer_file/report/crew_record/record in network.get_crew_records())
+ var/obj/item/paper/record_paperwork = new(src)
+ record_paperwork.name = "[archive_name] - [record.get_name()]"
+ record_paperwork.info = collate_data(record)
+ record_paperwork.update_icon()
+
+/obj/structure/filing_cabinet/records/proc/collate_data(var/datum/computer_file/report/crew_record/record)
+ . = list()
+ . += "Name: [record.get_name()]"
+ . += "Criminal Status: [record.get_criminalStatus()]"
+ . += "Details: [record.get_security_record()]"
+ return jointext(., "
")
+
+/obj/structure/filing_cabinet/records/medical
+ name = "medical record archive"
+ archive_name = "medical record"
+
+/obj/structure/filing_cabinet/records/medical/collate_data(var/datum/computer_file/report/crew_record/record)
+ . = list()
+ . += "Name: [record.get_name()]"
+ . += "Gender: [record.get_sex()]"
+ . += "Species: [record.get_species_name()]"
+ . += "Blood Type: [record.get_bloodtype()]"
+ . += "Details: [record.get_medical_record()]"
+ return jointext(., "
")
+
+/obj/structure/filing_cabinet/records/medical
+ name = "employment record archive"
+ archive_name = "employment record"
+
+/obj/structure/filing_cabinet/records/medical/collate_data(var/datum/computer_file/report/crew_record/record)
+ . = list()
+ . += "Name: [record.get_name()]"
+ . += "Gender: [record.get_sex()]"
+ . += "Species: [record.get_species_name()]"
+ . += "Details: [record.get_employment_record()]"
+ return jointext(., "
")
diff --git a/code/modules/paperwork/folders.dm b/code/modules/paperwork/folders.dm
index aa0c0e51a54..9b5156ed506 100644
--- a/code/modules/paperwork/folders.dm
+++ b/code/modules/paperwork/folders.dm
@@ -1,110 +1,90 @@
+///////////////////////////////////////////////
+// Folder
+///////////////////////////////////////////////
/obj/item/folder
- name = "folder"
- desc = "A folder."
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "folder"
- w_class = ITEM_SIZE_SMALL
- drop_sound = 'sound/foley/paperpickup1.ogg'
+ name = "folder"
+ desc = "A folder."
+ icon = 'icons/obj/items/folders.dmi'
+ icon_state = "folder"
+ w_class = ITEM_SIZE_SMALL
+ throwforce = 0
+ drop_sound = 'sound/foley/paperpickup1.ogg'
pickup_sound = 'sound/foley/paperpickup2.ogg'
+ material = /decl/material/solid/cardboard
+ item_flags = ITEM_FLAG_CAN_TAPE
/obj/item/folder/blue
- desc = "A blue folder."
+ desc = "A blue folder."
icon_state = "folder_blue"
/obj/item/folder/red
- desc = "A red folder."
+ desc = "A red folder."
icon_state = "folder_red"
/obj/item/folder/yellow
- desc = "A yellow folder."
+ desc = "A yellow folder."
icon_state = "folder_yellow"
/obj/item/folder/cyan
- desc = "A cyan folder."
+ desc = "A cyan folder."
icon_state = "folder_cyan"
-/obj/item/folder/on_update_icon()
- overlays.Cut()
- if(contents.len)
- overlays += "folder_paper"
- return
+/obj/item/folder/on_update_icon(var/paper_overlay = TRUE)
+ . = ..()
+ if(paper_overlay && length(contents))
+ add_overlay("folder_paper")
/obj/item/folder/attackby(obj/item/W, mob/user)
if(istype(W, /obj/item/paper) || istype(W, /obj/item/photo) || istype(W, /obj/item/paper_bundle))
if(!user.unEquip(W, src))
return
- to_chat(user, "You put the [W] into \the [src].")
+ to_chat(user, SPAN_NOTICE("You put the [W] into \the [src]."))
+ updateUsrDialog()
update_icon()
- else if(istype(W, /obj/item/pen))
+ return TRUE
+
+ else if(IS_PEN(W))
+ updateUsrDialog()
var/n_name = sanitize_safe(input(usr, "What would you like to label the folder?", "Folder Labelling", null) as text, MAX_NAME_LEN)
- if((loc == usr && usr.stat == 0))
- SetName("folder[(n_name ? text("- '[n_name]'") : null)]")
+ if(!CanPhysicallyInteractWith(user, src))
+ to_chat(user, SPAN_WARNING("You must stay close to \the [src]."))
+ return
+ SetName("folder[(n_name ? text("- '[n_name]'") : null)]")
+ return TRUE
return
/obj/item/folder/attack_self(mob/user)
- var/dat = "[name]"
- for(var/obj/item/paper/P in src)
- dat += "Remove Rename - [P.name]
"
- for(var/obj/item/photo/Ph in src)
- dat += "Remove Rename - [Ph.name]
"
- for(var/obj/item/paper_bundle/Pb in src)
- dat += "Remove Rename - [Pb.name]
"
- show_browser(user, dat, "window=folder")
- onclose(user, "folder")
- add_fingerprint(usr)
- return
+ return interact(user)
-/obj/item/folder/Topic(href, href_list)
- ..()
- if((usr.stat || usr.restrained()))
- return
+/obj/item/folder/interact(mob/user)
+ var/dat = "[name]"
+ for(var/obj/item/I in src)
+ dat += "Remove Rename - [I.name]
"
+
+ user.set_machine(src)
+ show_browser(user, dat, "window=[initial(name)]")
+ onclose(user, initial(name))
+ return TRUE
+
+/obj/item/folder/DefaultTopicState()
+ return global.physical_topic_state
+
+/obj/item/folder/OnTopic(mob/user, href_list, datum/topic_state/state)
+ if(href_list["remove"])
+ var/obj/item/P = locate(href_list["remove"])
+ user.put_in_hands(P)
+ . = TOPIC_REFRESH
+ else
+ . = handle_paper_stack_shared_topics(user, href_list)
- if(src.loc == usr)
-
- if(href_list["remove"])
- var/obj/item/P = locate(href_list["remove"])
- if(P && (P.loc == src) && istype(P))
- usr.put_in_hands(P)
-
- else if(href_list["read"])
- var/obj/item/paper/P = locate(href_list["read"])
- if(P && (P.loc == src) && istype(P))
- if(!(istype(usr, /mob/living/carbon/human) || isghost(usr) || istype(usr, /mob/living/silicon)))
- show_browser(usr, "[P.name][stars(P.show_info(usr))][P.stamps]", "window=[P.name]")
- onclose(usr, "[P.name]")
- else
- show_browser(usr, "[P.name][P.show_info(usr)][P.stamps]", "window=[P.name]")
- onclose(usr, "[P.name]")
- else if(href_list["look"])
- var/obj/item/photo/P = locate(href_list["look"])
- if(P && (P.loc == src) && istype(P))
- P.show(usr)
- else if(href_list["browse"])
- var/obj/item/paper_bundle/P = locate(href_list["browse"])
- if(P && (P.loc == src) && istype(P))
- P.attack_self(usr)
- onclose(usr, "[P.name]")
- else if(href_list["rename"])
- var/obj/item/O = locate(href_list["rename"])
-
- if(O && (O.loc == src))
- if(istype(O, /obj/item/paper))
- var/obj/item/paper/to_rename = O
- to_rename.rename()
-
- else if(istype(O, /obj/item/photo))
- var/obj/item/photo/to_rename = O
- to_rename.rename()
-
- else if(istype(O, /obj/item/paper_bundle))
- var/obj/item/paper_bundle/to_rename = O
- to_rename.rename()
-
- //Update everything
- attack_self(usr)
+ //Update everything
+ if(. & TOPIC_REFRESH)
+ updateUsrDialog()
update_icon()
- return
+///////////////////////////////////////////////
+// Envelope
+///////////////////////////////////////////////
/obj/item/folder/envelope
name = "envelope"
desc = "A thick envelope. You can't see what's inside."
@@ -112,6 +92,7 @@
var/sealed = 1
/obj/item/folder/envelope/on_update_icon()
+ . = ..(paper_overlay = FALSE)
if(sealed)
icon_state = "envelope_sealed"
else
diff --git a/code/modules/paperwork/handlabeler.dm b/code/modules/paperwork/handlabeler.dm
index 6210655fa26..605a5c0b37e 100644
--- a/code/modules/paperwork/handlabeler.dm
+++ b/code/modules/paperwork/handlabeler.dm
@@ -1,66 +1,278 @@
+#define HAND_LABELER_MODE_ADD 0 //Add a new label if possible
+#define HAND_LABELER_MODE_REM 1 //Remove the last label
+#define HAND_LABELER_MODE_REMALL 2 //Remove all labels
+//Mode names
+#define HAND_LABELER_SAFETY_TOGGLE "Safety"
+#define HAND_LABELER_MODE_ADD_NAME "Label"
+#define HAND_LABELER_MODE_REM_NAME "Remove one"
+#define HAND_LABELER_MODE_REMALL_NAME "Remove all"
+
+#define LABEL_MATERIAL_COST 120 //units of matter a single label is worth
+
+//////////////////////////////////////////////////////
+// Hand Labeler
+//////////////////////////////////////////////////////
/obj/item/hand_labeler
- name = "hand labeler"
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "labeler0"
- item_state = "flight"
- var/label = null
- var/labels_left = 30
- var/mode = 0 //off or on.
- material = /decl/material/solid/plastic
-
-/obj/item/hand_labeler/attack()
- return
-
-/obj/item/hand_labeler/afterattack(atom/A, mob/user, proximity)
- if(!proximity)
- return
- if(!mode) //if it's off, give up.
- return
- if(A == loc) // if placing the labeller into something (e.g. backpack)
- return // don't set a label
+ name = "hand labeler"
+ icon = 'icons/obj/items/hand_labeler.dmi'
+ icon_state = ICON_STATE_WORLD
+ material = /decl/material/solid/plastic
+ w_class = ITEM_SIZE_SMALL
+ item_flags = ITEM_FLAG_NO_BLUDGEON
+ matter = list(
+ /decl/material/solid/metal/aluminium = MATTER_AMOUNT_REINFORCEMENT, //These things always got some metal parts
+ )
+ var/label //What the labeler will label its target with
+ var/labels_left = 30
+ var/tmp/max_labels = 30 //Maximum amount of label charges
+ var/safety = TRUE //Whether the safety is on or off. Set to FALSE to allow labeler to interact with things
+ var/mode = HAND_LABELER_MODE_ADD //What operation the labeler is set to do
- if(!labels_left)
- to_chat(user, "No labels left.")
- return
- if(!label || !length(label))
- to_chat(user, "No label text set.")
+/obj/item/hand_labeler/examine(mob/user, distance, infix, suffix)
+ . = ..()
+ if(distance < 1)
+ to_chat(user, safety? "Safety is on." : SPAN_WARNING("Safety is off!"))
+ var/modename
+ switch(mode)
+ if(HAND_LABELER_MODE_ADD)
+ modename = HAND_LABELER_MODE_ADD_NAME
+ if(HAND_LABELER_MODE_REM)
+ modename = HAND_LABELER_MODE_REM_NAME
+ if(HAND_LABELER_MODE_REMALL)
+ modename = HAND_LABELER_MODE_REMALL_NAME
+ to_chat(user, "It's set to '[SPAN_ITALIC(modename)]' mode.")
+ to_chat(user, "It has [get_labels_left()]/[max_labels] label(s).")
+ if(length(label))
+ to_chat(user, "Its label text reads '[SPAN_ITALIC(label)]'.")
+ else
+ to_chat(user, SPAN_NOTICE("You're too far away to tell much more.."))
+
+/obj/item/hand_labeler/attack(mob/living/M, mob/living/user, target_zone, animate)
+ return //No attacking
+
+/obj/item/hand_labeler/afterattack(atom/movable/A, mob/user, proximity)
+ if(safety || !proximity || !istype(A) || A == loc)
return
- if(has_extension(A, /datum/extension/labels))
- var/datum/extension/labels/L = get_extension(A, /datum/extension/labels)
- if(!L.CanAttachLabel(user, label))
+
+ switch(mode)
+ if(HAND_LABELER_MODE_ADD)
+ if(!get_labels_left())
+ to_chat(user, SPAN_WARNING("No labels left."))
+ return
+ if(!length(label))
+ to_chat(user, SPAN_WARNING("No label text set."))
+ return
+ if(A.attach_label(user, src, label))
+ rem_paper_labels(1)
+
+ if(HAND_LABELER_MODE_REM, HAND_LABELER_MODE_REMALL)
+ var/datum/extension/labels/L = get_extension(A, /datum/extension/labels)
+ if(!LAZYLEN(L?.labels))
+ to_chat(user, SPAN_WARNING("\The [A] is not labeled!"))
+ return
+
+ var/nb_removed
+ var/removed = L.labels[L.labels.len]
+ if(mode == HAND_LABELER_MODE_REMALL)
+ L.RemoveAllLabels()
+ nb_removed = length(L.labels)
+ else
+ L.RemoveLabel(user, L.labels[L.labels.len])
+ nb_removed = 1
+
+ user.visible_message(
+ SPAN_NOTICE("[user] removes [nb_removed > 1? "some labels" : "a label"] from \the [A]."),
+ SPAN_NOTICE("You remove [nb_removed > 1? "[nb_removed] labels" : "the '[removed]' label"] from \the [A].")
+ )
+ playsound(loc, 'sound/items/poster_ripped.ogg', 50, TRUE)
+
+ //Update stuff
+ A.queue_icon_update() //Ask them to update their icons if possible
+ update_icon()
+
+/obj/item/hand_labeler/proc/show_action_radial_menu(var/mob/user)
+ //#TODO: Cache some of that stuff..
+ var/image/btn_power = image('icons/screen/radial.dmi', icon_state = safety? "radial_power" : "radial_power_off")
+ btn_power.plane = FLOAT_PLANE
+ btn_power.layer = FLOAT_LAYER
+ btn_power.name = "Turn Safety [safety? "Off": "On"]"
+ var/image/btn_set_label = new()
+ btn_set_label.appearance = src
+ btn_set_label.plane = FLOAT_PLANE
+ btn_set_label.layer = FLOAT_LAYER
+ btn_set_label.name = "Set Label Text"
+ var/image/btn_rem_one = new()
+ btn_rem_one.appearance = src
+ btn_rem_one.plane = FLOAT_PLANE
+ btn_rem_one.layer = FLOAT_LAYER
+ btn_rem_one.name = "Remove One"
+ var/image/btn_rem_all = new()
+ btn_rem_all.appearance = src
+ btn_rem_all.plane = FLOAT_PLANE
+ btn_rem_all.layer = FLOAT_LAYER
+ btn_rem_all.name = "Remove All"
+
+ var/list/choices = list(
+ HAND_LABELER_SAFETY_TOGGLE = btn_power,
+ HAND_LABELER_MODE_ADD_NAME = btn_set_label,
+ HAND_LABELER_MODE_REM_NAME = btn_rem_one,
+ HAND_LABELER_MODE_REMALL_NAME = btn_rem_all,
+ )
+ return show_radial_menu(user, user, choices, use_labels = TRUE)
+
+/obj/item/hand_labeler/attack_self(mob/user)
+ var/choice = show_action_radial_menu(user)
+ switch(choice)
+ if(HAND_LABELER_SAFETY_TOGGLE)
+ safety = !safety
+ playsound(user, 'sound/machines/click.ogg', 30, TRUE)
+ to_chat(loc, SPAN_NOTICE("You toggle the safety [safety? "on" : "off"]!"))
+
+ if(HAND_LABELER_MODE_ADD_NAME)
+ to_chat(user, SPAN_NOTICE("You switch to labeling mode."))
+ var/str = sanitize_safe(input(user,"Label text?", "Set label", label), MAX_LNAME_LEN)
+ if(!str || !length(str))
+ return
+ label = str
+ mode = HAND_LABELER_MODE_ADD
+ to_chat(user, SPAN_NOTICE("You set the label text to '[str]'."))
+
+ if(HAND_LABELER_MODE_REMALL_NAME, HAND_LABELER_MODE_REM_NAME)
+ mode = (choice == HAND_LABELER_MODE_REMALL_NAME)? HAND_LABELER_MODE_REMALL : HAND_LABELER_MODE_REM
+ to_chat(user, SPAN_NOTICE("You switch to remove [mode == HAND_LABELER_MODE_REMALL? "all labels" : "one label"] mode."))
+ playsound(loc, 'sound/effects/pop.ogg', 10)
+ if(prob(5))
+ spark_at(src, amount = 2)
+
+ update_icon()
+ return TRUE
+
+/obj/item/hand_labeler/attackby(obj/item/W, mob/user)
+
+ //Allow refilling with paper sheets too
+ if(istype(W, /obj/item/paper))
+ var/obj/item/paper/P = W
+ if(!P.is_blank())
+ to_chat(user, SPAN_WARNING("\The [P] is not blank. You can't use that for refilling \the [src]."))
+ return
+
+ var/incoming_amt = LAZYACCESS(P.matter, /decl/material/solid/paper)
+ var/current_amt = LAZYACCESS(matter, /decl/material/solid/paper)
+ var/label_added = incoming_amt / LABEL_MATERIAL_COST
+
+ if(incoming_amt < LABEL_MATERIAL_COST)
+ to_chat(user, SPAN_WARNING("\The [P] does not contains enough paper."))
+ return
+ if(((incoming_amt + current_amt) / LABEL_MATERIAL_COST) > max_labels)
+ to_chat(user, SPAN_WARNING("There's not enough room in \the [src] for the [label_added] label(s) \the [P] is worth."))
+ return
+ if(!user.do_skilled(2 SECONDS, SKILL_LITERACY, src) || (QDELETED(W) || QDELETED(src)))
+ return
+
+ to_chat(user, SPAN_NOTICE("You slice \the [P] into [label_added] small strips and insert them into \the [src]'s paper feed."))
+ add_paper_labels(label_added)
+ qdel(W)
+ update_icon()
+ return TRUE
+
+ //Allow reloading from stacks much faster
+ else if(istype(W, /obj/item/stack/material))
+ var/obj/item/stack/material/ST = W
+ var/decl/material/M = ST.material
+ var/max_accepted_labels = max_labels - get_labels_left()
+ var/max_accepted_units = max_accepted_labels * LABEL_MATERIAL_COST
+ var/available_units = ST.get_amount() * SHEET_MATERIAL_AMOUNT
+ var/added_labels = 0
+
+ if(available_units > max_accepted_units)
+ //Take only what's needed
+ var/needed_sheets = CEILING(max_accepted_units / SHEET_MATERIAL_AMOUNT)
+ var/leftover_units = max_accepted_units % SHEET_MATERIAL_AMOUNT
+ ST.use(needed_sheets)
+ //Drop the extra as shards
+ if(leftover_units)
+ M.place_cuttings(get_turf(user), leftover_units)
+ to_chat(user, SPAN_NOTICE("Some leftover [ST.singular_name] cuttings fall to the ground..."))
+ add_paper_labels(max_accepted_labels)
+ added_labels = max_accepted_labels
+ to_chat(user, SPAN_NOTICE("You use [max_accepted_units/SHEET_MATERIAL_AMOUNT] [ST.plural_name] to refill \the [src] with [added_labels] label(s)."))
+
+ else if(available_units > LABEL_MATERIAL_COST)
+ //Take all that's available
+ ST.use(CEILING(available_units/SHEET_MATERIAL_AMOUNT))
+ added_labels = round(available_units / LABEL_MATERIAL_COST)
+ add_paper_labels(added_labels)
+ to_chat(user, SPAN_NOTICE("You use [CEILING(available_units/SHEET_MATERIAL_AMOUNT)] [ST.plural_name] to refill \the [src] with [added_labels] label(s)."))
+ else
+ //Abort because not enough materials for even a single label
+ to_chat(user, SPAN_WARNING("There's not enough [ST.plural_name] in \the [ST] to refil \the [src]!"))
return
- A.attach_label(user, src, label)
-/atom/proc/attach_label(var/user, var/atom/labeler, var/label_text)
- to_chat(user, "The label refuses to stick to [name].")
+ update_icon()
+ return TRUE
+ return ..()
+
+/**Gets the amount of labels we can apply with our current supply of paper. */
+/obj/item/hand_labeler/proc/get_labels_left()
+ return labels_left
+
+/**Adds a paper label to the labeler if there's room for it. */
+/obj/item/hand_labeler/proc/add_paper_labels(var/amount)
+ if(get_labels_left() >= max_labels || amount <= 0)
+ return FALSE
+ labels_left = clamp((labels_left + amount), 0, max_labels)
+ update_icon()
+ return TRUE
+
+/**Remove a paper label from the labeler if there's any. */
+/obj/item/hand_labeler/proc/rem_paper_labels(var/amount)
+ if(get_labels_left() <= 0 || amount <= 0)
+ return FALSE
+ labels_left = clamp((labels_left - amount), 0, max_labels)
+ update_icon()
+ return TRUE
-/mob/observer/attach_label(var/user, var/atom/labeler, var/label_text)
- to_chat(user, "\The [labeler] passes through \the [src].")
+/obj/item/hand_labeler/dump_contents()
+ . = ..()
+ //Dump label paper left
+ if(labels_left > 0)
+ var/decl/material/M = GET_DECL(/decl/material/solid/paper)
+ var/turf/T = get_turf(src)
+ var/total_sheets = round((labels_left * LABEL_MATERIAL_COST) / SHEET_MATERIAL_AMOUNT)
+ var/leftovers = round((labels_left * LABEL_MATERIAL_COST) % SHEET_MATERIAL_AMOUNT)
+ M.create_object(T, total_sheets)
+ if(leftovers > 0)
+ M.place_cuttings(T, leftovers)
-/obj/machinery/portable_atmospherics/hydroponics/attach_label(var/user)
+////////////////////////////////////////////////////////////
+// Attach Label Overrides
+////////////////////////////////////////////////////////////
+/atom/proc/attach_label(var/mob/user, var/atom/labeler, var/label_text)
+ to_chat(user, SPAN_WARNING("The label refuses to stick to [name]."))
+ return FALSE
+
+/mob/observer/attach_label(mob/user, atom/labeler, label_text)
+ to_chat(user, SPAN_DANGER("\The [labeler] passes through \the [src]!"))
+ return FALSE
+
+/obj/machinery/portable_atmospherics/hydroponics/attach_label(mob/user, atom/labeler, label_text)
if(!mechanical)
- to_chat(user, "How are you going to label that?")
+ to_chat(user, SPAN_WARNING("How are you going to label that?"))
return
- ..()
+ . = ..()
update_icon()
-/obj/attach_label(var/user, var/atom/labeler, var/label_text)
+/obj/attach_label(mob/user, atom/labeler, label_text)
if(!simulated)
return
var/datum/extension/labels/L = get_or_create_extension(src, /datum/extension/labels)
- L.AttachLabel(user, label_text)
+ return L.AttachLabel(user, label_text)
-/obj/item/hand_labeler/attack_self(mob/user)
- mode = !mode
- icon_state = "labeler[mode]"
- if(mode)
- to_chat(user, "You turn on \the [src].")
- //Now let them chose the text.
- var/str = sanitize_safe(input(user,"Label text?","Set label",""), MAX_LNAME_LEN)
- if(!str || !length(str))
- to_chat(user, "Invalid text.")
- return
- label = str
- to_chat(user, "You set the text to '[str]'.")
- else
- to_chat(user, "You turn off \the [src].")
+#undef HAND_LABELER_MODE_ADD
+#undef HAND_LABELER_MODE_REM
+#undef HAND_LABELER_MODE_REMALL
+#undef HAND_LABELER_SAFETY_TOGGLE
+#undef HAND_LABELER_MODE_ADD_NAME
+#undef HAND_LABELER_MODE_REM_NAME
+#undef HAND_LABELER_MODE_REMALL_NAME
+#undef LABEL_MATERIAL_COST
\ No newline at end of file
diff --git a/code/modules/paperwork/helpers.dm b/code/modules/paperwork/helpers.dm
new file mode 100644
index 00000000000..520d67dfef7
--- /dev/null
+++ b/code/modules/paperwork/helpers.dm
@@ -0,0 +1,35 @@
+/**Handles topic interactions shared by folders and clipboard.*/
+/proc/handle_paper_stack_shared_topics(var/mob/user, var/list/href_list)
+ if(href_list["rename"])
+ var/obj/item/O = locate(href_list["rename"])
+ if(istype(O, /obj/item/paper))
+ var/obj/item/paper/to_rename = O
+ to_rename.rename()
+ . = TOPIC_REFRESH
+
+ else if(istype(O, /obj/item/photo))
+ var/obj/item/photo/to_rename = O
+ to_rename.rename()
+ . = TOPIC_REFRESH
+
+ else if(istype(O, /obj/item/paper_bundle))
+ var/obj/item/paper_bundle/to_rename = O
+ to_rename.rename()
+ . = TOPIC_REFRESH
+
+ else if(href_list["examine"])
+ var/obj/item/P = locate(href_list["examine"])
+ if(istype(P, /obj/item/paper))
+ var/obj/item/paper/PP = P
+ PP.attack_self(user)
+ . = TOPIC_HANDLED
+
+ else if(istype(P, /obj/item/photo))
+ var/obj/item/photo/PP = P
+ PP.attack_self(user)
+ . = TOPIC_HANDLED
+
+ else if(istype(P, /obj/item/paper_bundle))
+ var/obj/item/paper_bundle/PP = P
+ PP.attack_self(user)
+ . = TOPIC_HANDLED
diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm
index de0e0114ed4..5900215c4c1 100644
--- a/code/modules/paperwork/paper.dm
+++ b/code/modules/paperwork/paper.dm
@@ -5,63 +5,76 @@
* Paper
* also scraps of paper
*/
-
/obj/item/paper
- name = "sheet of paper"
- gender = NEUTER
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "paper"
- item_state = "paper"
- randpixel = 8
- throwforce = 0
- w_class = ITEM_SIZE_TINY
- throw_range = 1
- throw_speed = 1
- layer = ABOVE_OBJ_LAYER
- slot_flags = SLOT_HEAD
- body_parts_covered = SLOT_HEAD
- attack_verb = list("bapped")
- material = /decl/material/solid/wood
-
- drop_sound = 'sound/foley/paperpickup1.ogg'
- pickup_sound = 'sound/foley/paperpickup2.ogg'
-
- var/info // What's actually written on the paper.
- var/info_links // A different version of the paper which includes html links at fields and EOF
- var/stamps // The (text for the) stamps on the paper.
- var/fields // Amount of user created fields
- var/free_space = MAX_PAPER_MESSAGE_LEN
- var/list/stamped
- var/list/ico[0] //Icons and
- var/list/offset_x[0] //offsets stored for later
- var/list/offset_y[0] //usage by the photocopier
- var/rigged = 0
- var/spam_flag = 0
- var/last_modified_ckey
- var/age = 0
+ name = "sheet of paper"
+ icon = 'icons/obj/bureaucracy.dmi'
+ icon_state = "paper"
+ item_state = "paper"
+ layer = ABOVE_OBJ_LAYER
+ slot_flags = SLOT_HEAD
+ body_parts_covered = SLOT_HEAD
+ randpixel = 8
+ throwforce = 0
+ throw_range = 1
+ throw_speed = 1
+ w_class = ITEM_SIZE_TINY
+ attack_verb = list("bapped")
+ material = /decl/material/solid/paper
+ drop_sound = 'sound/foley/paperpickup1.ogg'
+ pickup_sound = 'sound/foley/paperpickup2.ogg'
+ item_flags = ITEM_FLAG_CAN_TAPE
+ //#TODO: Fonts probably should be stored in the pens or something?
+ var/tmp/deffont = "Verdana"
+ var/tmp/signfont = "Times New Roman"
+ var/tmp/crayonfont = "Comic Sans MS"
+ var/tmp/fancyfont = "Segoe Script"
+ var/scan_file_type = /datum/computer_file/data/text
+ var/persist_on_init = TRUE
+ var/age = 0
+ var/fields = 0 // Amount of user created fields
+ var/free_space = MAX_PAPER_MESSAGE_LEN
+ var/rigged = FALSE
+ var/tmp/is_honking = FALSE
+ var/is_crumpled = FALSE //Whether the paper is currently crumpled
+ var/info // What's actually written on the paper.
+ var/info_links // A different version of the paper which includes html links at fields and EOF (aka the "write" link)
+ var/stamp_text // The (text for the) stamps on the paper.
var/list/metadata
+ var/list/applied_stamps //List of stamp overlays.
+ var/last_modified_ckey
- var/const/deffont = "Verdana"
- var/const/signfont = "Times New Roman"
- var/const/crayonfont = "Comic Sans MS"
- var/const/fancyfont = "Segoe Script"
-
- var/scan_file_type = /datum/computer_file/data/text
-
- var/persist_on_init = TRUE
-
-/obj/item/paper/Initialize(mapload, text, title, list/md = null)
- . = ..(mapload)
- set_content(text ? text : info, title)
- metadata = md
+/obj/item/paper/Initialize(mapload, material_key, var/_text, var/_title, var/list/md = null)
+ . = ..(mapload, material_key)
+ set_content(_text ? _text : info, _title)
+ if(md)
+ LAZYDISTINCTADD(metadata, md) //Merge them
if(!mapload && persist_on_init)
SSpersistence.track_value(src, /decl/persistence_handler/paper)
-/obj/item/paper/create_matter()
- matter = list(/decl/material/solid/wood = round(SHEET_MATERIAL_AMOUNT * 0.2))
+/obj/item/paper/GetCloneArgs()
+ return list(null, material, info, name)
+
+/obj/item/paper/PopulateClone(obj/item/paper/clone)
+ clone = ..()
+ clone.fields = fields
+ clone.last_modified_ckey = last_modified_ckey
+ clone.rigged = rigged
+ clone.is_crumpled = is_crumpled
+ clone.stamp_text = stamp_text
+ clone.applied_stamps = LAZYLEN(applied_stamps)? listDeepClone(applied_stamps) : null
+ clone.metadata = LAZYLEN(metadata)? listDeepClone(metadata, TRUE) : null
+ return clone
+
+/obj/item/paper/Clone()
+ var/obj/item/paper/clone = ..()
+ if(clone)
+ clone.updateinfolinks()
+ return clone
+
+/obj/item/paper/get_matter_amount_modifier()
+ return 0.2
/obj/item/paper/proc/set_content(text,title)
- set waitfor = FALSE
if(title)
SetName(title)
info = html_encode(text)
@@ -69,14 +82,25 @@
update_icon()
update_space(info)
updateinfolinks()
+ updateUsrDialog()
/obj/item/paper/on_update_icon()
- if(icon_state == "paper_talisman")
- return
- else if(info)
- icon_state = "paper_words"
+ . = ..()
+ if(is_crumpled)
+ icon_state = "scrap"
+ return //No overlays on crumpled paper
else
- icon_state = "paper"
+ icon_state = initial(icon_state)
+ update_contents_overlays()
+
+ //The appearence is the key, the type is the value
+ for(var/image/key in applied_stamps)
+ add_overlay(key)
+
+/**Applies the overlay displayed when the paper contains some text. */
+/obj/item/paper/proc/update_contents_overlays()
+ if(length(info))
+ add_overlay("paper_words")
/obj/item/paper/proc/update_space(var/new_text)
if(new_text)
@@ -84,77 +108,65 @@
/obj/item/paper/examine(mob/user, distance)
. = ..()
- if(name != "sheet of paper")
+ if(name != initial(name))
to_chat(user, "It's titled '[name]'.")
+
if(distance <= 1)
- show_content(usr)
+ interact(user, readonly = TRUE)
else
- to_chat(user, "You have to go closer if you want to read it.")
+ to_chat(user, SPAN_NOTICE("You have to go closer if you want to read it."))
-/obj/item/paper/proc/show_content(mob/user, forceshow)
- var/show_info = user.handle_reading_literacy(user, info, FALSE, (forceshow || get_dist(src, user) <= 1))
+/obj/item/paper/interact(mob/user, var/forceshow = FALSE, var/readonly = FALSE)
+ var/show_info = user.handle_reading_literacy(user, readonly? info : info_links, FALSE, (forceshow || get_dist(src, user) <= 1))
if(show_info)
- show_browser(user, "[name][show_info][stamps]", "window=[name]")
+ user.set_machine(src)
+ show_browser(user, "[name][show_info][stamp_text]", "window=[name]")
onclose(user, "[name]")
-
-/obj/item/paper/verb/rename()
- set name = "Rename paper"
- set category = "Object"
- set src in usr
-
- if((MUTATION_CLUMSY in usr.mutations) && prob(50))
- to_chat(usr, "You cut yourself on the paper.")
- return
- var/n_name = sanitize_safe(input(usr, "What would you like to label the paper?", "Paper Labelling", null) as text, MAX_NAME_LEN)
-
- // We check loc one level up, so we can rename in clipboards and such. See also: /obj/item/photo/rename()
- if(!n_name || !CanInteract(usr, global.deep_inventory_topic_state))
- return
- n_name = usr.handle_writing_literacy(usr, n_name)
- if(n_name)
- SetName(n_name)
- add_fingerprint(usr)
+ return TRUE
/obj/item/paper/attack_self(mob/user)
if(user.a_intent == I_HURT)
- if(icon_state == "scrap")
- user.show_message("\The [src] is already crumpled.")
+ if(is_crumpled)
+ user.show_message(SPAN_WARNING("\The [src] is already crumpled."))
return
//crumple dat paper
- info = stars(info,85)
+ crumple()
user.visible_message("\The [user] crumples \the [src] into a ball!")
- icon_state = "scrap"
- return
- user.examinate(src)
+ return TRUE
+
+ interact(user, readonly = FALSE) //Allow us writing on paper since we're holding it somwhere
+
if(rigged && (global.current_holiday?.name == "April Fool's Day"))
- if(spam_flag == 0)
- spam_flag = 1
- playsound(loc, 'sound/items/bikehorn.ogg', 50, 1)
+ if(!is_honking)
+ is_honking = TRUE
+ playsound(loc, 'sound/items/bikehorn.ogg', 50, TRUE)
spawn(20)
- spam_flag = 0
+ is_honking = FALSE
+ return TRUE
/obj/item/paper/attack_ai(mob/living/silicon/ai/user)
- show_content(user)
+ interact(user, readonly = TRUE)
+ return TRUE
/obj/item/paper/attack(mob/living/carbon/M, mob/living/carbon/user)
if(user.zone_sel.selecting == BP_EYES)
- user.visible_message("You show the paper to [M]. ", \
- " [user] holds up a paper and shows it to [M]. ")
+ user.visible_message(SPAN_NOTICE("You show the paper to [M]."), \
+ SPAN_NOTICE("[user] holds up a paper and shows it to [M]."))
M.examinate(src)
else if(user.zone_sel.selecting == BP_MOUTH) // lipstick wiping
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H == user)
- to_chat(user, "You wipe off the lipstick with [src].")
+ to_chat(user, SPAN_NOTICE("You wipe off the lipstick with [src]."))
H.lip_style = null
H.update_body()
else
- user.visible_message("[user] begins to wipe [H]'s lipstick off with \the [src].", \
- "You begin to wipe off [H]'s lipstick.")
+ user.visible_message(SPAN_WARNING("[user] begins to wipe [H]'s lipstick off with \the [src]."), \
+ SPAN_NOTICE("You begin to wipe off [H]'s lipstick."))
if(do_after(user, 10, H) && do_after(H, 10, check_holding = 0)) //user needs to keep their active hand, H does not.
- user.visible_message("[user] wipes [H]'s lipstick off with \the [src].", \
- "You wipe off [H]'s lipstick.")
+ user.visible_message(SPAN_NOTICE("[user] wipes [H]'s lipstick off with \the [src]."), \
+ SPAN_NOTICE("You wipe off [H]'s lipstick."))
H.lip_style = null
H.update_body()
@@ -201,19 +213,19 @@
addtofield(i, "write", 1)
info_links = info_links + "write"
-
/obj/item/paper/proc/clearpaper()
info = null
- stamps = null
+ stamp_text = null
free_space = MAX_PAPER_MESSAGE_LEN
- stamped = list()
- overlays.Cut()
+ LAZYCLEARLIST(applied_stamps)
+ is_crumpled = FALSE
updateinfolinks()
update_icon()
/obj/item/paper/proc/get_signature(var/obj/item/pen/P, mob/user)
- if(P && istype(P, /obj/item/pen))
- return P.get_signature(user)
+ if(P && IS_PEN(P))
+ var/decl/tool_archetype/pen/parch = GET_DECL(TOOL_PEN)
+ return parch.get_signature(user, P)
return (user && user.real_name) ? user.real_name : "Anonymous"
/obj/item/paper/proc/parsepencode(t, obj/item/pen/P, mob/user, iscrayon, isfancy)
@@ -236,12 +248,13 @@
t = replacetext(t, "\[cell\]", "")
t = replacetext(t, "\[logo\]", "")
+ var/pen_color = P? P.get_tool_property(TOOL_PEN, TOOL_PROP_COLOR) : "black"
if(iscrayon)
- t = "[t]"
+ t = "[t]"
else if(isfancy)
- t = "[t]"
+ t = "[t]"
else
- t = "[t]"
+ t = "[t]"
t = pencode2html(t)
@@ -256,7 +269,6 @@
return t
-
/obj/item/paper/proc/burnpaper(obj/item/flame/P, mob/user)
var/class = "warning"
@@ -277,66 +289,54 @@
qdel(src)
else
- to_chat(user, "You must hold \the [P] steady to burn \the [src].")
+ to_chat(user, SPAN_WARNING("You must hold \the [P] steady to burn \the [src]."))
+/obj/item/paper/CouldNotUseTopic(mob/user)
+ to_chat(user, SPAN_WARNING("You can't do that!"))
-/obj/item/paper/Topic(href, href_list)
- ..()
- if(!usr || (usr.stat || usr.restrained()))
- return
+/obj/item/paper/OnTopic(mob/user, href_list, datum/topic_state/state)
if(href_list["write"])
var/id = href_list["write"]
- //var/t = strip_html_simple(input(usr, "What text do you wish to add to " + (id=="end" ? "the end of the paper" : "field "+id) + "?", "[name]", null),8192) as message
-
if(free_space <= 0)
- to_chat(usr, "There isn't enough space left on \the [src] to write anything.")
- return
-
- var/obj/item/I = usr.get_active_hand() // Check to see if he still got that darn pen, also check what type of pen
- var/iscrayon = 0
- var/isfancy = 0
- if(!istype(I, /obj/item/pen))
- var/obj/item/rig/r = usr.get_equipped_item(slot_back_str)
- if(istype(r))
- var/obj/item/rig_module/device/pen/m = locate(/obj/item/rig_module/device/pen) in r.installed_modules
- if(!r.offline && m)
- I = m.device
- else
- return
- else
- return
-
- var/obj/item/pen/P = I
- if(!P.active)
- P.toggle()
-
- if(P.iscrayon)
- iscrayon = TRUE
-
- if(P.isfancy)
- isfancy = TRUE
+ to_chat(user, SPAN_INFO("There isn't enough space left on \the [src] to write anything."))
+ return TOPIC_NOACTION
+
+ //Try to find a usable pen on the user, if not abort
+ var/obj/item/I = user.get_accessible_pen()
+ if(!IS_PEN(I))
+ to_chat(user, SPAN_WARNING("You need something to write with!"))
+ return TOPIC_NOACTION
+
+ //If we got a pen that's not in our hands, make sure to move it over
+ if(user.get_active_hand() != I && user.get_empty_hand_slot() && user.put_in_hands(I))
+ to_chat(user, SPAN_NOTICE("You grab your trusty [I]!"))
+ else if(user.get_active_hand() != I)
+ to_chat(user, SPAN_WARNING("You'd use your trusty [I], but your hands are full!"))
+ return TOPIC_NOACTION
+
+ var/pen_flags = I.get_tool_property(TOOL_PEN, TOOL_PROP_PEN_FLAG)
+ if(!(pen_flags & PEN_FLAG_ACTIVE))
+ var/decl/tool_archetype/pen/parch = GET_DECL(TOOL_PEN)
+ parch.toggle_active(usr, I)
+ var/iscrayon = pen_flags & PEN_FLAG_CRAYON
+ var/isfancy = pen_flags & PEN_FLAG_FANCY
var/t = sanitize(input("Enter what you want to write:", "Write", null, null) as message, free_space, extra = 0, trim = 0)
-
if(!t)
- return
-
- // if paper is not in usr, then it must be near them, or in a clipboard or folder, which must be in or near usr
- if(src.loc != usr && !src.Adjacent(usr) && !((istype(src.loc, /obj/item/clipboard) || istype(src.loc, /obj/item/folder)) && (src.loc.loc == usr || src.loc.Adjacent(usr)) ) )
- return
+ return TOPIC_NOACTION
var/last_fields_value = fields
-
- t = parsepencode(t, I, usr, iscrayon, isfancy) // Encode everything from pencode to html
-
+ t = parsepencode(t, I, user, iscrayon, isfancy) // Encode everything from pencode to html
if(fields > MAX_FIELDS)
- to_chat(usr, "Too many fields. Sorry, you can't do this.")
+ to_chat(user, SPAN_WARNING("Too many fields. Sorry, you can't do this."))
fields = last_fields_value
- return
+ return TOPIC_NOACTION
- var/processed_text = usr.handle_writing_literacy(usr, t)
+ var/processed_text = user.handle_writing_literacy(user, t)
+ if(length(t))
+ playsound(src, pick('sound/effects/pen1.ogg','sound/effects/pen2.ogg'), 30)
if(id!="end")
addtofield(text2num(id), processed_text) // He wants to edit a field, let him.
@@ -344,147 +344,223 @@
info += processed_text // Oh, he wants to edit to the end of the file, let him.
updateinfolinks()
- last_modified_ckey = usr.ckey
-
+ last_modified_ckey = user.ckey
update_space(t)
- var/processed_info_links = usr.handle_reading_literacy(usr, info_links, TRUE)
- if(processed_info_links)
- show_browser(usr, "[name][processed_info_links][stamps]", "window=[name]") // Update the window
- playsound(src, pick('sound/effects/pen1.ogg','sound/effects/pen2.ogg'), 10)
- update_icon()
+ . = TOPIC_REFRESH
+ if(. & TOPIC_REFRESH)
+ updateUsrDialog()
+ update_icon()
+ return
+
+ return ..()
/obj/item/paper/attackby(obj/item/P, mob/user)
- ..()
- var/clown = 0
- if(user.mind && (user.mind.assigned_role == "Clown"))
- clown = 1
-
- if(istype(P, /obj/item/ducttape))
- var/obj/item/ducttape/tape = P
- tape.stick(src, user)
- return
+ if(istype(P, /obj/item/stack/tape_roll/duct_tape))
+ var/obj/item/stack/tape_roll/duct_tape/tape = P
+ return tape.stick(src, user)
- if(istype(P, /obj/item/paper) || istype(P, /obj/item/photo))
- if(!can_bundle())
- return
- var/obj/item/paper/other = P
- if(istype(other) && !other.can_bundle())
- return
- if (istype(P, /obj/item/paper/carbon))
- var/obj/item/paper/carbon/C = P
- if (!C.iscopy && !C.copied)
- to_chat(user, "Take off the carbon copy first.")
- add_fingerprint(user)
- return
- var/obj/item/paper_bundle/B = new(src.loc)
- if (name != "paper")
- B.SetName(name)
- else if (P.name != "paper" && P.name != "photo")
- B.SetName(P.name)
-
- if(!user.unEquip(P, B) || !user.unEquip(src, B))
+ else if(istype(P, /obj/item/paper) || istype(P, /obj/item/photo))
+ var/obj/item/paper_bundle/B = try_bundle_with(P, user)
+ if(!B)
return
user.put_in_hands(B)
+ to_chat(user, SPAN_NOTICE("You clip \the [P] and [(name == initial(name)) ? "\the " : ""][name] together."))
+ return TRUE
- to_chat(user, "You clip the [P.name] to [(src.name == "paper") ? "the paper" : src.name].")
-
- B.pages.Add(src)
- B.pages.Add(P)
- B.update_icon()
-
- else if(istype(P, /obj/item/pen))
- if(icon_state == "scrap")
- to_chat(usr, "\The [src] is too crumpled to write on.")
+ else if(IS_PEN(P))
+ if(is_crumpled)
+ to_chat(user, SPAN_WARNING("\The [src] is too crumpled to write on."))
return
var/obj/item/pen/robopen/RP = P
if ( istype(RP) && RP.mode == 2 )
RP.RenamePaper(user,src)
else
- var/processed_info_links = user.handle_reading_literacy(user, info_links, length(info))
- if(processed_info_links)
- show_browser(user, "[name][processed_info_links][stamps]", "window=[name]")
- return
+ interact(user, readonly = FALSE)
+ return TRUE
else if(istype(P, /obj/item/stamp) || istype(P, /obj/item/clothing/ring/seal))
- if((!in_range(src, usr) && loc != user && !( istype(loc, /obj/item/clipboard) ) && loc.loc != user && user.get_active_hand() != P))
- return
-
- stamps += (stamps=="" ? "
" : "
") + "This paper has been stamped with the [P.name]."
-
- var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
- var/x
- var/y
- if(istype(P, /obj/item/stamp/captain) || istype(P, /obj/item/stamp/boss))
- x = rand(-2, 0)
- y = rand(-1, 2)
- else
- x = rand(-2, 2)
- y = rand(-3, 2)
- offset_x += x
- offset_y += y
- stampoverlay.pixel_x = x
- stampoverlay.pixel_y = y
-
- if(istype(P, /obj/item/stamp/clown))
- if(!clown)
- to_chat(user, "You are totally unable to use the stamp. HONK!")
- return
-
- if(!ico)
- ico = new
- ico += "paper_[P.icon_state]"
- stampoverlay.icon_state = "paper_[P.icon_state]"
-
- if(!stamped)
- stamped = new
- stamped += P.type
- overlays += stampoverlay
-
- playsound(src, 'sound/effects/stamp.ogg', 50, 1)
- to_chat(user, "You stamp the paper with your [P.name].")
+ apply_custom_stamp(
+ image('icons/obj/bureaucracy.dmi', icon_state = "paper_[P.icon_state]", pixel_x = rand(-2, 2), pixel_y = rand(-2, 2)),
+ "with \the [P]")
+ playsound(src, 'sound/effects/stamp.ogg', 50, TRUE)
+ to_chat(user, SPAN_NOTICE("You stamp the paper with your [P.name]."))
+ return TRUE
else if(istype(P, /obj/item/flame))
burnpaper(P, user)
+ return TRUE
else if(istype(P, /obj/item/paper_bundle))
- if(!can_bundle())
+ var/obj/item/paper_bundle/B = P
+ B.merge(src, user)
+ return TRUE
+ return ..()
+
+/obj/item/paper/proc/try_bundle_with(var/obj/item/paper/other, var/mob/user)
+ if(!can_bundle_with(other))
+ if(user)
+ to_chat(user, SPAN_WARNING("You can't bundle those!"))
+ return
+
+ var/obj/item/paper_bundle/B = new(loc)
+ if(user)
+ if(!user.canUnEquip(src))
+ to_chat(user, SPAN_WARNING("You can't unequip \the [src]!"))
+ return
+ if(!user.canUnEquip(other))
+ to_chat(user, SPAN_WARNING("You can't unequip \the [other]!"))
return
- var/obj/item/paper_bundle/attacking_bundle = P
- attacking_bundle.insert_sheet_at(user, (attacking_bundle.pages.len)+1, src)
- attacking_bundle.update_icon()
+ user.unEquip(src, B)
+ user.unEquip(other, B)
- add_fingerprint(user)
+ if (name != initial(name))
+ B.SetName(name)
+ else if (other.name != initial(other.name))
+ B.SetName(other.name)
+
+ B.insert_sheet_at(user, src)
+ B.insert_sheet_at(user, other)
+ return B
/obj/item/paper/proc/can_bundle()
return TRUE
-/obj/item/paper/proc/show_info(var/mob/user)
- return info
+/obj/item/paper/proc/can_bundle_with(var/obj/item/other)
+ if(istype(other, /obj/item/paper))
+ var/obj/item/paper/P = other
+ return can_bundle() && P.can_bundle()
+ else if(istype(other, /obj/item/photo))
+ return can_bundle()
+ else if(istype(other, /obj/item/paper_bundle))
+ var/obj/item/paper_bundle/B = other
+ return can_bundle() && !B.is_full()
+ return FALSE
+
+/obj/item/paper/DefaultTopicState()
+ return global.paper_topic_state
+
+/**Whether the paper can be considered blank, for purposes of refilling machines and etc. */
+/obj/item/paper/proc/is_blank()
+ return !length(info) && !length(stamp_text) && !is_crumpled && !LAZYLEN(applied_stamps)
+
+/**Stamp the paper with the specified values.
+ * stamper_name: what is stamped. Or what comes after the sentence "This paper has been stamped "
+*/
+/obj/item/paper/proc/apply_custom_stamp(var/image/stamp, var/stamper_name)
+ LAZYADD(applied_stamps, stamp)
+ stamp_text += "[length(stamp_text)? "
" : "
"]This paper has been stamped [length(stamper_name)? stamper_name : "by the generic stamp"]."
+ update_icon()
+
+/**Merge the paper with other papers or bundles inside "location" */
+/obj/item/paper/proc/merge_with_existing(var/atom/location, var/mob/user)
+ if(!location || !can_bundle())
+ return
+ for(var/obj/item/I in location)
+ if(istype(I, /obj/item/paper))
+ return try_bundle_with(I, user)
+
+ if(istype(I, /obj/item/paper_bundle))
+ var/obj/item/paper_bundle/B = I
+ if(B.is_full())
+ continue
+ if(B.merge(src, user))
+ return B
+
+/obj/item/paper/proc/crumple()
+ info = stars(info,85)
+ is_crumpled = TRUE
+ update_icon()
+
+/obj/item/paper/verb/rename()
+ set name = "Rename paper"
+ set category = "Object"
+ set src in usr
+ if(usr.incapacitated())
+ to_chat(usr, SPAN_WARNING("You can't do that in your current state!"))
+ return
-//For supply.
+ if((MUTATION_CLUMSY in usr.mutations) && prob(50))
+ to_chat(usr, SPAN_WARNING("You cut yourself on the paper."))
+ return
+ var/n_name = sanitize_safe(input(usr, "What would you like to name the paper?", "Paper Naming", name) as text, MAX_NAME_LEN)
+
+ // We check loc one level up, so we can rename in clipboards and such. See also: /obj/item/photo/rename()
+ if(!n_name || !CanInteract(usr, global.deep_inventory_topic_state))
+ return
+ n_name = usr.handle_writing_literacy(usr, n_name)
+ if(n_name)
+ SetName(length(n_name) > 0? n_name : initial(name))
+ add_fingerprint(usr)
+
+/obj/item/paper/dropped(mob/user)
+ . = ..()
+ if(CanUseTopic(user, DefaultTopicState()))
+ updateUsrDialog()
+ else
+ close_browser(user, name)
+
+//
+//Topic state for paper since we can use it within clipboards and folders
+//
+var/global/datum/topic_state/default/paper_state/paper_topic_state = new
+/datum/topic_state/default/paper_state/can_use_topic(var/src_object, var/mob/user)
+ . = ..()
+ if(. == STATUS_INTERACTIVE)
+ return
+
+ //Check inside held objects
+ for(var/atom/movable/AM in user.get_held_items())
+ if(src_object in AM)
+ return user.shared_nano_interaction() //Have to check this again, since we ignore all the distance stuff that was already done
+
+///////////////////////////////////////////////////
+// Paper Templates
+///////////////////////////////////////////////////
/obj/item/paper/manifest
- name = "supply manifest"
- var/order_total = 0
- var/is_copy = 1
-/*
- * Premade paper
- */
+ name = "supply manifest"
+ metadata = list(
+ "order_total" = 0,
+ "is_copy" = TRUE,
+ )
+
/obj/item/paper/court
name = "Judgement"
info = "For crimes as specified, the offender is sentenced to:
\n
\n"
+/obj/item/paper/aromatherapy_disclaimer
+ name = "aromatherapy disclaimer"
+ info = "The manufacturer and the retailer make no claims of the contained products' effacy.
Use at your own risk."
+
+///////////////////////////////////////////////////
+// Colored Paper
+///////////////////////////////////////////////////
+/obj/item/paper/blue
+ name = "blue sheet of paper"
+ color = "#ccffff"
+
+/obj/item/paper/green
+ name = "green sheet of paper"
+ color = "#ccffaa"
+
+/obj/item/paper/yellow
+ name = "yellow sheet of paper"
+ color = "#ffffcc"
+
+/obj/item/paper/pink
+ name = "pink sheet of paper"
+ color = "#ffccff"
+
+///////////////////////////////////////////////////
+// Crumpled Paper
+///////////////////////////////////////////////////
/obj/item/paper/crumpled
- name = "paper scrap"
+ name = "paper scrap"
icon_state = "scrap"
-/obj/item/paper/crumpled/on_update_icon()
+/obj/item/paper/crumpled/update_contents_overlays()
return
/obj/item/paper/crumpled/bloody
icon_state = "scrap_bloodied"
-
-/obj/item/paper/aromatherapy_disclaimer
- name = "aromatherapy disclaimer"
- info = "The manufacturer and the retailer make no claims of the contained products' effacy.
Use at your own risk."
\ No newline at end of file
diff --git a/code/modules/paperwork/paper_bundle.dm b/code/modules/paperwork/paper_bundle.dm
index e27719a8d35..50e1635c527 100644
--- a/code/modules/paperwork/paper_bundle.dm
+++ b/code/modules/paperwork/paper_bundle.dm
@@ -1,77 +1,178 @@
+#define MAX_PHOTO_OVERLAYS 10 //Maximum amount of photo overlays displayed on the paper bundle
+#define MAX_PAPER_UNDERLAYS 20 //Maximum amount of paper underlays displayed under the bundle icon
+
+///////////////////////////////////////////////////////////////////////////
+// Paper Bundle
+///////////////////////////////////////////////////////////////////////////
/obj/item/paper_bundle
- name = "paper bundle"
- gender = NEUTER
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "paper"
- item_state = "paper"
- randpixel = 8
- throwforce = 0
- w_class = ITEM_SIZE_SMALL
- throw_range = 2
- throw_speed = 1
- layer = ABOVE_OBJ_LAYER
- attack_verb = list("bapped")
- var/page = 1 // current page
- var/list/pages = list() // Ordered list of pages as they are to be displayed. Can be different order than src.contents.
+ name = "paper bundle"
+ icon = 'icons/obj/bureaucracy.dmi'
+ icon_state = "paper"
+ item_state = "paper"
+ layer = ABOVE_OBJ_LAYER
+ randpixel = 8
+ throwforce = 0
+ throw_range = 2
+ throw_speed = 1
+ w_class = ITEM_SIZE_SMALL
+ attack_verb = list("bapped")
+ drop_sound = 'sound/foley/paperpickup1.ogg'
+ pickup_sound = 'sound/foley/paperpickup2.ogg'
+ item_flags = ITEM_FLAG_CAN_TAPE
+ health = 10
+ max_health = 10
+ var/tmp/cur_page = 1 // current page
+ var/tmp/max_pages = 100 //Maximum number of papers that can be in the bundle
+ var/list/pages // Ordered list of pages as they are to be displayed. Can be different order than src.contents.
+ var/static/list/cached_overlays //Cached images used by all paper bundles for generating the overlays and underlays
+
+/**Creates frequently used images globally, so we can re-use them. */
+/obj/item/paper_bundle/proc/cache_overlays()
+ if(LAZYLEN(cached_overlays))
+ return
+ LAZYSET(cached_overlays, "clip", image('icons/obj/bureaucracy.dmi', "clip"))
+ LAZYSET(cached_overlays, "paper", image('icons/obj/bureaucracy.dmi', "paper"))
+ LAZYSET(cached_overlays, "photo", image('icons/obj/bureaucracy.dmi', "photo"))
+ LAZYSET(cached_overlays, "refill", image('icons/obj/bureaucracy.dmi', "paper_refill_label"))
+/obj/item/paper_bundle/Destroy()
+ LAZYCLEARLIST(pages) //Get rid of refs
+ return ..()
/obj/item/paper_bundle/attackby(obj/item/W, mob/user)
- ..()
- if(!istype(W))
- return
- var/obj/item/paper/paper = W
- if(istype(paper) && !paper.can_bundle())
- return //non-paper or bundlable paper only
- if (istype(W, /obj/item/paper/carbon))
- var/obj/item/paper/carbon/C = W
- if (!C.iscopy && !C.copied)
- to_chat(user, "Take off the carbon copy first.")
- add_fingerprint(user)
- return
+
// adding sheets
if(istype(W, /obj/item/paper) || istype(W, /obj/item/photo))
- insert_sheet_at(user, pages.len+1, W)
+ var/obj/item/paper/paper = W
+ if(istype(paper) && !paper.can_bundle())
+ return //non-paper or bundlable paper only
+ merge(W, user, cur_page)
+ return TRUE
+
+ // merging bundles
+ else if(istype(W, /obj/item/paper_bundle) && merge(W, user, cur_page))
+ to_chat(user, SPAN_NOTICE("You add \the [W.name] to \the [name]."))
+ return TRUE
// burning
else if(istype(W, /obj/item/flame))
burnpaper(W, user)
+ return TRUE
- // merging bundles
- else if(istype(W, /obj/item/paper_bundle))
- for(var/obj/O in W)
- O.forceMove(src)
- O.add_fingerprint(user)
- pages.Add(O)
-
- to_chat(user, "You add \the [W.name] to [(src.name == "paper bundle") ? "the paper bundle" : src.name].")
- qdel(W)
+ else if(istype(W, /obj/item/stack/tape_roll/duct_tape))
+ var/obj/P = pages[cur_page]
+ . = P.attackby(W, user)
+ update_icon()
+ updateUsrDialog()
+ return TRUE
+
+ else if(IS_PEN(W) || istype(W, /obj/item/stamp))
+ close_browser(user, "window=[name]")
+ var/obj/P = pages[cur_page]
+ . = P.attackby(W, user)
+ update_icon()
+ updateUsrDialog()
+ return .
+
+ return ..()
+
+/**Check if the bundle should break itself down, and does it if needed. */
+/obj/item/paper_bundle/proc/reevaluate_existence(var/mob/user)
+ if(LAZYLEN(pages) < 2)
+ break_bundle(user)
+
+/**Insert the given item into the bundle, optionally at the index specified, or otherwise at the end. */
+/obj/item/paper_bundle/proc/insert_sheet_at(var/mob/user, var/obj/item/sheet, var/index = null)
+ if (user && !user.unEquip(sheet, src))
+ return
+ else if(!user)
+ sheet.forceMove(src)
+
+ if(isnull(index))
+ LAZYDISTINCTADD(pages, sheet)
+ index = length(pages)
else
- if(istype(W, /obj/item/ducttape))
- return 0
- if(istype(W, /obj/item/pen))
- show_browser(user, "", "window=[name]") //Closes the dialog
- var/obj/P = pages[page]
- P.attackby(W, user)
+ LAZYINSERT(pages, sheet, index)
+
+ //Make sure the cur_page stays valid, and pointing at the right index
+ cur_page = clamp((index <= cur_page)? (cur_page + 1) : cur_page, 1, length(pages))
+ if(user)
+ to_chat(user, SPAN_NOTICE("You add \the [sheet] as the [get_ordinal_string(index)] page in \the [name]."))
+ updateUsrDialog()
update_icon()
- attack_self(user) //Update the browsed page.
- add_fingerprint(user)
- return
+ return TRUE
+
+/obj/item/paper_bundle/proc/remove_sheet(var/obj/item/I, var/mob/user, var/skip_qdel = FALSE)
+ var/found = -1
+ for(var/i = 1 to length(pages))
+ if(pages[i] == I)
+ found = i
+ break
+ if(found == -1)
+ return
+ return remove_sheet_at(found, user, skip_qdel)
+
+/**Indiscriminatly remove sheets from the bundle */
+/obj/item/paper_bundle/proc/remove_sheets(var/amount, var/mob/user, var/delete_pages = TRUE)
+ if(LAZYLEN(pages) <= 0 || amount > LAZYLEN(pages))
+ return //Not a user error
+
+ for(var/i = 1 to amount)
+ var/obj/item/I = pages[pages.len]
+ pages -= I
+ if(delete_pages)
+ qdel(I)
+ else
+ I.dropInto(loc)
+ reevaluate_existence(user)
+
+ if(!QDELETED(src))
+ cur_page = 1 //Reset current page
+ updateUsrDialog()
+ update_icon()
+ return TRUE
-/obj/item/paper_bundle/proc/insert_sheet_at(mob/user, var/index, obj/item/sheet)
- if (!user.unEquip(sheet, src))
+/obj/item/paper_bundle/proc/remove_sheet_at(var/index, var/mob/user, var/skip_qdel = FALSE)
+ var/obj/item/I = LAZYACCESS(pages, index)
+ if(!I)
return
- var/bundle_name = "paper bundle"
- var/sheet_name = istype(sheet, /obj/item/photo) ? "photo" : "sheet of paper"
- bundle_name = (bundle_name == name) ? "the [bundle_name]" : name
- sheet_name = (sheet_name == sheet.name) ? "the [sheet_name]" : sheet.name
- to_chat(user, "You add [sheet_name] to [bundle_name].")
- pages.Insert(index, sheet)
- if(index <= page)
- page++
+ LAZYREMOVE(pages, I)
+ if(user)
+ user.put_in_hands(I)
+ to_chat(user, SPAN_NOTICE("You remove the [I.name] from the bundle."))
+ else
+ I.dropInto(loc)
+
+ if(!skip_qdel)
+ reevaluate_existence(user)
+ if(QDELETED(src))
+ return TRUE
+
+ //Make sure the cur_page stays valid, and pointing at the right index
+ cur_page = clamp((cur_page >= index)? (cur_page - 1) : cur_page, 1, length(pages))
+
+ updateUsrDialog()
+ update_icon()
+ return TRUE
+
+/**Delete the bundle and drop all pages */
+/obj/item/paper_bundle/proc/break_bundle(var/mob/user)
+ if(user && user == loc)
+ user.drop_from_inventory(src)
-/obj/item/paper_bundle/proc/burn_callback(obj/item/flame/P, mob/user, span_class)
+ close_browser(user, "window=[name]")
+ for(var/obj/item/I in src)
+ if(user && user.get_empty_hand_slot())
+ user.put_in_hands(I)
+ else
+ I.add_fingerprint(user)
+ I.dropInto(loc)
+ qdel(src)
+ return TRUE
+
+/obj/item/paper_bundle/proc/burn_callback(var/obj/item/flame/P, var/mob/user, var/span_class)
if(QDELETED(P) || QDELETED(user))
return
if(!Adjacent(user) || user.get_active_hand() != P || !P.lit)
@@ -83,8 +184,8 @@
new /obj/effect/decal/cleanable/ash(loc)
qdel(src)
-/obj/item/paper_bundle/proc/burnpaper(obj/item/flame/P, mob/user)
- if(!P.lit || user.restrained())
+/obj/item/paper_bundle/proc/burnpaper(var/obj/item/flame/P, var/mob/user)
+ if(!P.lit || user.incapacitated())
return
var/span_class = istype(P, /obj/item/flame/lighter/zippo) ? "rose" : "warning"
var/decl/pronouns/G = user.get_pronouns()
@@ -96,143 +197,349 @@
/obj/item/paper_bundle/examine(mob/user, distance)
. = ..()
if(distance <= 1)
- src.show_content(user)
+ interact(user)
else
- to_chat(user, SPAN_WARNING("It is too far away."))
+ to_chat(user, SPAN_WARNING("It's too far away."))
-/obj/item/paper_bundle/proc/show_content(mob/user)
+/obj/item/paper_bundle/interact(mob/user)
var/dat
- var/obj/item/W = pages[page]
-
- // first
- if(page == 1)
- dat+= ""
- dat+= ""
- dat+= "
"
- // last
- else if(page == pages.len)
- dat+= ""
- dat+= ""
- dat+= "
"
- // middle pages
+ var/obj/item/W = pages[cur_page]
+
+ //Header
+ dat = ""
+ dat += "| "
+ if(cur_page > 1)
+ dat += "First"
+ else
+ dat += "First"
+ dat += " | "
+
+ dat += ""
+ if(cur_page > 1)
+ dat += "Previous"
+ else
+ dat += "Previous"
+ dat += " | "
+
+ dat += "[cur_page]/[length(pages)] Remove | "
+
+ dat += ""
+ if(cur_page < pages.len)
+ dat += "Next"
else
- dat+= ""
- dat+= ""
- dat+= "
"
+ dat += "Next"
+ dat += " | "
- if(istype(pages[page], /obj/item/paper))
+ dat += ""
+ if(cur_page < pages.len)
+ dat += "Last"
+ else
+ dat += "Last"
+ dat += " | "
+ dat += "
"
+
+ //Contents
+ if(istype(W, /obj/item/paper))
var/obj/item/paper/P = W
- dat+= "[P.name][P.show_info(user)][P.stamps]"
+ dat += "[P.name][P.info][P.stamp_text]"
show_browser(user, dat, "window=[name]")
- else if(istype(pages[page], /obj/item/photo))
+ onclose(user, name)
+
+ else if(istype(W, /obj/item/photo))
var/obj/item/photo/P = W
- dat += "[P.name]"
- dat += " 
Written on the back:
[P.scribble]" : null ]"
+ dat += {"
+
[P.name]
+

Written on the back:
[P.scribble]" : null ]
+ "}
send_rsc(user, P.img, "tmp_photo.png")
- show_browser(user, JOINTEXT(dat), "window=[name]")
+ show_browser(user, dat, "window=[name]")
+ onclose(user, name)
+ user.set_machine(src)
+ return TRUE
/obj/item/paper_bundle/attack_self(mob/user)
- src.show_content(user)
- add_fingerprint(user)
- update_icon()
- return
-
-/obj/item/paper_bundle/Topic(href, href_list)
- if(..())
- return 1
- if((src in usr.contents) || (istype(src.loc, /obj/item/folder) && (src.loc in usr.contents)))
- usr.set_machine(src)
- var/obj/item/in_hand = usr.get_active_hand()
- if(href_list["next_page"])
- if(in_hand && (istype(in_hand, /obj/item/paper) || istype(in_hand, /obj/item/photo)))
- insert_sheet_at(usr, page+1, in_hand)
- else if(page != pages.len)
- page++
- playsound(src.loc, "pageturn", 50, 1)
- if(href_list["prev_page"])
- if(in_hand && (istype(in_hand, /obj/item/paper) || istype(in_hand, /obj/item/photo)))
- insert_sheet_at(usr, page, in_hand)
- else if(page > 1)
- page--
- playsound(src.loc, "pageturn", 50, 1)
- if(href_list["remove"])
- var/obj/item/W = pages[page]
- usr.put_in_hands(W)
- pages.Remove(pages[page])
-
- to_chat(usr, "
You remove the [W.name] from the bundle.")
-
- if(pages.len <= 1)
- var/obj/item/paper/P = src[1]
- usr.drop_from_inventory(src)
- usr.put_in_hands(P)
- qdel(src)
-
- return
+ return interact(user)
- if(page > pages.len)
- page = pages.len
-
- update_icon()
+/obj/item/paper_bundle/OnTopic(mob/user, href_list, datum/topic_state/state)
+ . = ..()
- src.attack_self(usr)
- updateUsrDialog()
- else
- to_chat(usr, "
You need to hold it in hands!")
+ //Handle page turning
+ if(href_list["next_page"] && (cur_page < LAZYLEN(pages)))
+ cur_page = clamp(cur_page + 1, 1, length(pages))
+ playsound(src.loc, "pageturn", 50, 1)
+ . = TOPIC_REFRESH
-/obj/item/paper_bundle/verb/rename()
- set name = "Rename bundle"
- set category = "Object"
- set src in usr
+ if(href_list["prev_page"] && (cur_page > 1))
+ cur_page = clamp(cur_page - 1, 1, length(pages))
+ playsound(src.loc, "pageturn", 50, 1)
+ . = TOPIC_REFRESH
- var/n_name = sanitize_safe(input(usr, "What would you like to label the bundle?", "Bundle Labelling", null) as text, MAX_NAME_LEN)
- if((loc == usr || loc.loc && loc.loc == usr) && usr.stat == 0)
- SetName("[(n_name ? text("[n_name]") : "paper")]")
- add_fingerprint(usr)
- return
+ if(href_list["first_page"] && (cur_page > 1))
+ cur_page = 1
+ playsound(src.loc, "pageturn", 50, TRUE)
+ spawn(5)
+ playsound(src.loc, "pageturn", 20, TRUE)
+ . = TOPIC_REFRESH
+ if(href_list["last_page"] && (cur_page < LAZYLEN(pages)))
+ cur_page = length(pages)
+ playsound(src.loc, "pageturn", 50, TRUE)
+ spawn(5)
+ playsound(src.loc, "pageturn", 20, TRUE)
+ . = TOPIC_REFRESH
-/obj/item/paper_bundle/verb/remove_all()
- set name = "Loose bundle"
- set category = "Object"
- set src in usr
+ if(href_list["jump_to"])
+ var/newpage = input(user, "Page: ", "Which page?", cur_page) as num
+ if(!CanPhysicallyInteractWith(user, src))
+ to_chat(user, SPAN_WARNING("You must stay close to \the [src]."))
+ else if(newpage > 0 && newpage <= length(pages))
+ cur_page = newpage
+ playsound(src.loc, "pageturn", 50, TRUE)
+ . = TOPIC_REFRESH
- to_chat(usr, "
You loosen the bundle.")
- for(var/obj/O in src)
- O.dropInto(usr.loc)
- O.reset_plane_and_layer()
- O.add_fingerprint(usr)
- qdel(src)
+ if(href_list["remove"] && remove_sheet_at(cur_page, user) && !QDELETED(src))
+ . = TOPIC_REFRESH
+ if(. & TOPIC_REFRESH)
+ update_icon()
+ updateUsrDialog()
/obj/item/paper_bundle/on_update_icon()
+ . = ..()
+ if(!LAZYLEN(cached_overlays))
+ cache_overlays()
+ underlays.Cut()
+
var/obj/item/paper/P = pages[1]
+ icon = P.icon
icon_state = P.icon_state
- overlays = P.overlays
- underlays.Cut()
- var/i = 0
- var/photo
- for(var/obj/O in src)
- var/image/img = image('icons/obj/bureaucracy.dmi')
- if(istype(O, /obj/item/paper))
- img.icon_state = O.icon_state
- img.pixel_x -= min(1*i, 2)
- img.pixel_y -= min(1*i, 2)
- default_pixel_x = min(0.5*i, 1)
- default_pixel_y = min( 1*i, 2)
+ set_overlays(P.overlays)
+
+ var/paper_count = 0
+ var/photo_count = 0
+
+ for(var/obj/O in pages)
+ if(istype(O, /obj/item/paper) && (paper_count < MAX_PAPER_UNDERLAYS))
+ //We can't even see them, so don't bother create appearences form each paper's icon, and use a generic one
+ var/mutable_appearance/img = new(cached_overlays["paper"])
+ img.color = O.color
+ img.pixel_x -= min(paper_count, 2)
+ img.pixel_y -= min(paper_count, 2)
+ default_pixel_x = min(0.5 * paper_count, 1)
+ default_pixel_y = min(paper_count, 2)
reset_offsets(0)
underlays += img
- i++
- else if(istype(O, /obj/item/photo))
+ paper_count++
+
+ else if(istype(O, /obj/item/photo) && (photo_count < MAX_PHOTO_OVERLAYS))
var/obj/item/photo/Ph = O
- img = Ph.tiny
- photo = 1
- overlays += img
- if(i>1)
- desc = "[i] papers clipped to each other."
+ if(photo_count < 1)
+ add_overlay(Ph.tiny)
+ else
+ add_overlay(cached_overlays["photo"]) //We can't even see them, so don't bother create new unique appearences
+ photo_count++
+
+ //Break if we have nothing else to do
+ if((paper_count >= MAX_PAPER_UNDERLAYS) && (photo_count >= MAX_PHOTO_OVERLAYS))
+ break
+
+ if(paper_count > 1)
+ desc = "[paper_count] papers clipped to each other."
else
desc = "A single sheet of paper."
- if(photo)
+
+ if(photo_count > 1)
+ desc += "\nThere are [photo_count] photos attached to it."
+ else if(photo_count > 0)
desc += "\nThere is a photo attached to it."
- overlays += image('icons/obj/bureaucracy.dmi', "clip")
- return
+
+ add_overlay(cached_overlays["clip"])
+
+/**
+ * Merge another bundle or paper into us.
+ */
+/obj/item/paper_bundle/proc/merge(var/obj/item/I, var/mob/user, var/at_hint = null)
+
+ //Merge lone paper
+ if(istype(I, /obj/item/paper) || istype(I, /obj/item/photo))
+ var/obj/item/paper/P = I
+ if(is_full() || (istype(P) && !P.can_bundle())) //Only paper check if they can be bundled apparently
+ return FALSE
+ //Merge the thing
+ insert_sheet_at(user, I, at_hint)
+ if(user)
+ if(!user.unEquip(I, src))
+ to_chat(user, SPAN_WARNING("You can't unequip \the [I]!"))
+ return
+ I.add_fingerprint(user)
+ else
+ I.forceMove(src)
+
+ //Update
+ update_icon()
+ updateUsrDialog()
+ return TRUE
+
+ //Merge paper bundle
+ else if(istype(I, /obj/item/paper_bundle))
+ var/obj/item/paper_bundle/B = I
+ if(LAZYLEN(B.pages) <= 0)
+ return FALSE
+
+ var/cur_num_pages = LAZYLEN(pages)
+ if(cur_num_pages == max_pages)
+ return FALSE
+
+ //Merge the paper lists
+ var/num_added = min(max_pages, LAZYLEN(pages) + LAZYLEN(B.pages)) - cur_num_pages
+ var/insert_index = ((!isnull(at_hint) && (at_hint >= 0) && (at_hint <= length(pages)))? at_hint : length(pages) + 1)
+ LAZYINSERT(pages, B.pages, insert_index)
+ if(length(pages) > max_pages)
+ pages.Cut(max_pages)
+ B.pages.Cut(1, num_added + 1)
+
+ //Make sure to move all the things we grabbed from the old stack
+ for(var/obj/item/O in pages)
+ if(O.loc != src)
+ O.forceMove(src)
+ if(user)
+ O.add_fingerprint(user)
+
+ //Update
+ update_icon()
+ updateUsrDialog()
+
+ //If old stack now empty, delete it
+ if(LAZYLEN(B.pages) <= 0)
+ qdel(B)
+ else
+ B.cur_page = 1 //Make sure the other bundle won't runtime
+ B.update_icon()
+ return TRUE
+
+ CRASH("Tried to merge \a [I] ([I?.type]), which has an unhandled type, into a paper bundle!")
+
+/**Attempts to merge all mergeable papers in the specified location into src. */
+/obj/item/paper_bundle/proc/merge_all_in_loc(var/atom/location, var/mob/user)
+ if(is_full())
+ return
+
+ for(var/obj/item/I in location)
+ if(is_full())
+ break
+ if(!QDELETED(I) && (istype(I, /obj/item/paper_bundle) || istype(I, /obj/item/paper) || istype(I, /obj/item/photo)))
+ merge(I)
+
+//Make sure we can be interacted with when inside a folder
+/obj/item/paper_bundle/DefaultTopicState()
+ return global.paper_topic_state
+
+//We don't contain any matter, since we're not really a material thing..
+/obj/item/paper_bundle/create_matter()
+ UNSETEMPTY(matter)
+
+/obj/item/paper_bundle/proc/get_amount_papers()
+ return LAZYLEN(pages)
+
+/obj/item/paper_bundle/proc/is_full()
+ return LAZYLEN(pages) >= max_pages
+
+/**Whether all the papers in the pile are blank /obj/item/paper */
+/obj/item/paper_bundle/proc/is_blank()
+ for(var/obj/item/I in pages)
+ if(!istype(I, /obj/item/paper))
+ return FALSE
+ var/obj/item/paper/P = I
+ if(!P.is_blank())
+ return FALSE
+ return TRUE
+
+/obj/item/paper_bundle/get_alt_interactions(mob/user)
+ . = ..()
+ LAZYADD(., /decl/interaction_handler/rename/paper_bundle)
+ LAZYADD(., /decl/interaction_handler/unbundle/paper_bundle)
+
+/obj/item/paper_bundle/PopulateClone(obj/item/paper_bundle/clone)
+ clone = ..()
+ for(var/obj/item/I in pages)
+ clone.merge(I.Clone())
+ return clone
+
+/obj/item/paper_bundle/verb/rename()
+ set name = "Rename Bundle"
+ set category = "Object"
+ set src in usr
+ if(!CanPhysicallyInteractWith(usr, src))
+ to_chat(usr, SPAN_WARNING("You cannot do this currently!"))
+ return
+
+ var/n_name = sanitize_safe(input(usr, "What would you like to name \the [src]? (Leave empty to reset name.)", "Renaming", name) as text, MAX_NAME_LEN)
+ if(CanPhysicallyInteractWith(usr, src) && !QDELETED(src))
+ SetName("[length(n_name) ? "[n_name]" : initial(name)]")
+ add_fingerprint(usr)
+
+///////////////////////////////////////////////////////////////////////////
+// Paper Refill
+///////////////////////////////////////////////////////////////////////////
+/obj/item/paper_bundle/refill
+ name = "paper refill"
+ desc = "A bundle of blank sheets of paper."
+ var/tmp/bundle_size = 30 //Amount of paper sheets in this bundle
+
+/obj/item/paper_bundle/refill/Initialize(ml, material_key)
+ . = ..()
+ if(. != INITIALIZE_HINT_QDEL)
+ setup_contents()
+
+/obj/item/paper_bundle/refill/proc/setup_contents()
+ for(var/i=1 to bundle_size)
+ var/obj/item/paper/P = new /obj/item/paper(src)
+ LAZYADD(pages, P)
+ update_icon()
+
+/obj/item/paper_bundle/refill/attack_self(mob/user)
+ return break_bundle(user)
+
+/obj/item/paper_bundle/refill/interact(mob/user)
+ return //We don't show the menu
+
+/obj/item/paper_bundle/refill/insert_sheet_at(mob/user, obj/item/sheet, index)
+ return //Don't let us insert
+
+/obj/item/paper_bundle/refill/merge(obj/item/I, mob/user, at_hint)
+ return //Don't merge
+
+/obj/item/paper_bundle/refill/break_bundle(mob/user)
+ user?.unEquip(src)
+ var/turf/T = get_turf(src)
+ for(var/i = 1 to bundle_size)
+ new /obj/item/paper(T)
+ qdel(src)
+
+/obj/item/paper_bundle/refill/on_update_icon()
+ . = ..()
+ add_overlay(cached_overlays["refill"])
+
+///////////////////////////////////////////////////////////////////////////
+// Interaction Rename
+///////////////////////////////////////////////////////////////////////////
+/decl/interaction_handler/rename/paper_bundle
+ name = "Rename Bundle"
+ expected_target_type = /obj/item/paper_bundle
+
+/decl/interaction_handler/rename/paper_bundle/invoked(obj/item/paper_bundle/target, mob/user)
+ target.rename()
+
+///////////////////////////////////////////////////////////////////////////
+// Interaction Break
+///////////////////////////////////////////////////////////////////////////
+/decl/interaction_handler/unbundle/paper_bundle
+ name = "Unbundle"
+ expected_target_type = /obj/item/paper_bundle
+
+/decl/interaction_handler/unbundle/paper_bundle/invoked(obj/item/paper_bundle/target, mob/user)
+ to_chat(user, SPAN_NOTICE("You loosen \the [target]."))
+ target.break_bundle(user)
+
+#undef MAX_PHOTO_OVERLAYS
+#undef MAX_PAPER_UNDERLAYS
diff --git a/code/modules/paperwork/paper_plane.dm b/code/modules/paperwork/paper_plane.dm
new file mode 100644
index 00000000000..52786419cb4
--- /dev/null
+++ b/code/modules/paperwork/paper_plane.dm
@@ -0,0 +1,97 @@
+///////////////////////////////////////////////////
+// Paper Plane
+///////////////////////////////////////////////////
+/obj/item/paper_plane
+ name = "paper plane"
+ desc = "A sheet of paper folded into a plane."
+ icon = 'icons/obj/bureaucracy.dmi'
+ icon_state = "paper_plane"
+ item_state = "paper"
+ layer = ABOVE_OBJ_LAYER
+ does_spin = FALSE
+ throwforce = 0
+ throw_range = 20
+ throw_speed = 1
+ w_class = ITEM_SIZE_TINY
+ item_flags = ITEM_FLAG_CAN_TAPE
+ attack_verb = list("stabbed", "pricked")
+ material = /decl/material/solid/paper
+ var/obj/item/paper/my_paper //The sheet of paper this paper_plane is made of
+
+/obj/item/paper_plane/proc/set_paper(var/obj/item/paper/_paper)
+ if(my_paper)
+ return FALSE
+ _paper.forceMove(src)
+ my_paper = _paper
+ if(my_paper.material)
+ set_material(my_paper.material.type)
+ color = my_paper.color
+ update_icon()
+ return TRUE
+
+/obj/item/paper_plane/proc/unfold(var/mob/user)
+ if(user)
+ if(!user.unEquip(src))
+ return
+ user.visible_message(SPAN_NOTICE("\The [user] unfolds \the [src]."), SPAN_NOTICE("You unfold \the [src]."))
+ if(my_paper)
+ user.put_in_active_hand(my_paper)
+ else if(my_paper)
+ my_paper.dropInto(loc)
+ my_paper = null
+ qdel(src)
+ return TRUE
+
+/obj/item/paper_plane/throw_impact(atom/hit_atom, datum/thrownthing/TT)
+ . = ..()
+ if(istype(hit_atom, /mob/living/carbon))
+ var/mob/living/carbon/C = hit_atom
+ //Only hurt if received right into the eyes
+ if(TT.target_zone == BP_EYES && !(BP_EYES in C.get_covered_body_parts()))
+ C.apply_damage(1, BRUTE, BP_EYES, 0, src, 0)
+ C.apply_effects(2, 0, 0, 0, 1, 0, 15)
+ take_damage(TT.speed * w_class, BRUTE)
+
+/obj/item/paper_plane/attack_self(mob/user)
+ if(user.a_intent == I_HURT)
+ return crumple(user)
+ else
+ return unfold(user)
+
+/obj/item/paper_plane/proc/crumple(var/mob/user)
+ if(user)
+ user.visible_message(SPAN_WARNING("\The [user] crumples \the [src]."))
+
+ if(my_paper)
+ my_paper.crumple()
+ unfold(user)
+ else
+ //If no paper, just make one
+ if(user)
+ user.unEquip(src)
+ var/obj/item/paper/P = new(loc)
+ P.crumple()
+ qdel(src)
+ return TRUE
+
+///////////////////////////////////////////////////
+// Alt Interactions
+///////////////////////////////////////////////////
+/decl/interaction_handler/make_paper_plane
+ name = "Fold Into Paper Plane"
+ expected_target_type = /obj/item/paper
+
+/decl/interaction_handler/make_paper_plane/is_possible(obj/item/paper/target, mob/user, obj/item/prop)
+ return ..() && !target.is_crumpled
+
+/decl/interaction_handler/make_paper_plane/invoked(obj/item/paper/target, mob/user)
+ user.visible_message(SPAN_NOTICE("\The [user] folds \the [target] into a plane."), SPAN_NOTICE("You fold \the [target] into a plane."))
+ var/obj/item/paper_plane/PP = new
+ user.unEquip(target, PP)
+ PP.set_paper(target)
+ user.put_in_hands(PP)
+ return TRUE
+
+/obj/item/paper/get_alt_interactions(mob/user)
+ . = ..()
+ LAZYDISTINCTADD(., /decl/interaction_handler/make_paper_plane)
\ No newline at end of file
diff --git a/code/modules/paperwork/paper_sticky.dm b/code/modules/paperwork/paper_sticky.dm
index ea745dc6064..9572cf6b23c 100644
--- a/code/modules/paperwork/paper_sticky.dm
+++ b/code/modules/paperwork/paper_sticky.dm
@@ -1,95 +1,99 @@
+////////////////////////////////////////////////
+// Sticky Note Pad
+////////////////////////////////////////////////
/obj/item/sticky_pad
- name = "sticky note pad"
- desc = "A pad of densely packed sticky notes."
- color = COLOR_YELLOW
- icon = 'icons/obj/stickynotes.dmi'
- icon_state = "pad_full"
- item_state = "paper"
- w_class = ITEM_SIZE_SMALL
- material = /decl/material/solid/wood
-
- var/papers = 50
- var/written_text
- var/written_by
- var/paper_type = /obj/item/paper/sticky
+ name = "sticky note pad"
+ desc = "A pad of densely packed sticky notes."
+ color = COLOR_YELLOW
+ icon = 'icons/obj/stickynotes.dmi'
+ icon_state = "pad_full"
+ item_state = "paper"
+ w_class = ITEM_SIZE_SMALL
+ material = /decl/material/solid/paper
+ var/papers = 50
+ var/tmp/max_papers = 50
+ var/paper_type = /obj/item/paper/sticky
+ var/obj/item/paper/top //The instanciated paper on the top of the pad, if there's one
+
+/obj/item/sticky_pad/Initialize(ml, material_key)
+ . = ..()
+ update_top_paper()
/obj/item/sticky_pad/proc/update_matter()
matter = list(
- /decl/material/solid/wood = round((papers * SHEET_MATERIAL_AMOUNT) * 0.2)
+ /decl/material/solid/paper = round((papers * SHEET_MATERIAL_AMOUNT) * 0.2)
)
/obj/item/sticky_pad/create_matter()
update_matter()
/obj/item/sticky_pad/on_update_icon()
+ . = ..()
if(papers <= 15)
icon_state = "pad_empty"
else if(papers <= 50)
icon_state = "pad_used"
else
icon_state = "pad_full"
- if(written_text)
+
+ if(top?.info)
icon_state = "[icon_state]_writing"
/obj/item/sticky_pad/attackby(var/obj/item/thing, var/mob/user)
- if(istype(thing, /obj/item/pen))
-
- if(jobban_isbanned(user, "Graffiti"))
- to_chat(user, SPAN_WARNING("You are banned from leaving persistent information across rounds."))
- return
-
- var/writing_space = MAX_MESSAGE_LEN - length(written_text)
- if(writing_space <= 0)
- to_chat(user, SPAN_WARNING("There is no room left on \the [src]."))
- return
- var/text = sanitize_safe(input("What would you like to write?") as text, writing_space)
- if(!text || thing.loc != user || (!Adjacent(user) && loc != user) || user.incapacitated())
- return
- user.visible_message(SPAN_NOTICE("\The [user] jots a note down on \the [src]."))
- written_by = user.ckey
- if(written_text)
- written_text = "[written_text] [text]"
- else
- written_text = text
+ if(IS_PEN(thing) || istype(thing, /obj/item/stamp))
+ . = top?.attackby(thing, user)
update_icon()
- return
- ..()
+ return .
+ return ..()
/obj/item/sticky_pad/examine(mob/user)
. = ..()
to_chat(user, SPAN_NOTICE("It has [papers] sticky note\s left."))
to_chat(user, SPAN_NOTICE("You can click it on grab intent to pick it up."))
+/obj/item/sticky_pad/dragged_onto(mob/user)
+ user.put_in_hands(top)
+ . = ..()
+
/obj/item/sticky_pad/attack_hand(var/mob/user)
if(user.a_intent == I_GRAB)
- ..()
- else
- var/obj/item/paper/paper = new paper_type(get_turf(src))
- paper.set_content(written_text, "sticky note")
- paper.last_modified_ckey = written_by
- paper.color = color
- written_text = null
- user.put_in_hands(paper)
- to_chat(user, SPAN_NOTICE("You pull \the [paper] off \the [src]."))
+ return ..()
+ else if(top)
+ user.put_in_active_hand(top)
+ top = null
papers--
+ update_top_paper()
+ to_chat(user, SPAN_NOTICE("You pull \the [top] off \the [src]."))
+
if(papers <= 0)
qdel(src)
else
- update_matter()
+ update_top_paper()
update_icon()
+ return TRUE
+
+/**Creates the paper the user can write on, if there's any paper left. */
+/obj/item/sticky_pad/proc/update_top_paper()
+ if(!top && papers > 0)
+ top = new paper_type(src)
+ top.set_color(color)
/obj/item/sticky_pad/random/Initialize()
. = ..()
color = pick(COLOR_YELLOW, COLOR_LIME, COLOR_CYAN, COLOR_ORANGE, COLOR_PINK)
+////////////////////////////////////////////////
+// Sticky Note Sheet
+////////////////////////////////////////////////
/obj/item/paper/sticky
- name = "sticky note"
- desc = "Note to self: buy more sticky notes."
- icon = 'icons/obj/stickynotes.dmi'
- color = COLOR_YELLOW
- slot_flags = 0
- layer = ABOVE_WINDOW_LAYER
+ name = "sticky note"
+ desc = "Note to self: buy more sticky notes."
+ icon = 'icons/obj/stickynotes.dmi'
+ color = COLOR_YELLOW
+ slot_flags = 0
+ layer = ABOVE_WINDOW_LAYER
persist_on_init = FALSE
+ item_flags = ITEM_FLAG_CAN_TAPE
/obj/item/paper/sticky/Initialize()
. = ..()
@@ -105,9 +109,9 @@
events_repository.unregister(/decl/observ/moved, src, src)
. = ..()
-/obj/item/paper/sticky/on_update_icon()
- if(icon_state != "scrap")
- icon_state = info ? "paper_words" : "paper"
+/obj/item/paper/sticky/update_contents_overlays()
+ if(length(info))
+ add_overlay("sticky_words")
// Copied from duct tape.
/obj/item/paper/sticky/attack_hand()
@@ -120,7 +124,7 @@
/obj/item/paper/sticky/afterattack(var/A, var/mob/user, var/flag, var/params)
- if(!in_range(user, A) || istype(A, /obj/machinery/door) || istype(A, /obj/item/storage) || icon_state == "scrap")
+ if(!in_range(user, A) || istype(A, /obj/machinery/door) || istype(A, /obj/item/storage) || is_crumpled)
return
var/turf/target_turf = get_turf(A)
diff --git a/code/modules/paperwork/paperbin.dm b/code/modules/paperwork/paperbin.dm
index 4e90023a5ec..3edbc900983 100644
--- a/code/modules/paperwork/paperbin.dm
+++ b/code/modules/paperwork/paperbin.dm
@@ -1,17 +1,25 @@
+/////////////////////////////////////////////////////////////////
+// Paper Bin
+/////////////////////////////////////////////////////////////////
/obj/item/paper_bin
- name = "paper bin"
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "paper_bin1"
- item_state = "sheet-metal"
- randpixel = 0
- throwforce = 1
- w_class = ITEM_SIZE_NORMAL
- throw_speed = 3
- throw_range = 7
- layer = BELOW_OBJ_LAYER
- var/amount = 30 //How much paper is in the bin.
- var/list/papers = new/list() //List of papers put in the bin for reference.
+ name = "paper bin"
+ icon = 'icons/obj/items/paper_bin.dmi'
+ icon_state = "paper_bin1"
+ item_state = "sheet-metal"
+ randpixel = 0
+ layer = BELOW_OBJ_LAYER
+ throwforce = 1
+ w_class = ITEM_SIZE_NORMAL
+ throw_speed = 3
+ throw_range = 7
+ material = /decl/material/solid/plastic
+ var/amount = 30 //How much paper is in the bin.
+ var/tmp/max_amount = 30 //How much paper fits in the bin
+ var/list/papers //List of papers put in the bin for reference.
+/obj/item/paper_bin/Destroy()
+ LAZYCLEARLIST(papers) //Gets rid of any refs
+ return ..()
/obj/item/paper_bin/handle_mouse_drop(atom/over, mob/user)
if((loc == user || in_range(src, user)) && user.get_empty_hand_slot())
@@ -20,82 +28,145 @@
. = ..()
/obj/item/paper_bin/attack_hand(mob/user)
+ if(!Adjacent(user))
+ to_chat(user, SPAN_WARNING("You're too far!"))
+ return
+
if(ishuman(user))
var/mob/living/carbon/human/H = user
var/obj/item/organ/external/temp = GET_EXTERNAL_ORGAN(H, H.get_active_held_item_slot())
if(temp && !temp.is_usable())
- to_chat(user, "
You try to move your [temp.name], but cannot!")
- return
- var/response = ""
- if(!papers.len > 0)
- response = alert(user, "Do you take regular paper, or Carbon copy paper?", "Paper type request", "Regular", "Carbon-Copy", "Cancel")
- if (response != "Regular" && response != "Carbon-Copy")
- add_fingerprint(user)
+ to_chat(user, SPAN_NOTICE("You try to move your [temp.name], but cannot!"))
return
- if(amount >= 1)
- amount--
- if(amount==0)
- update_icon()
-
- var/obj/item/paper/P
- if(papers.len > 0) //If there's any custom paper on the stack, use that instead of creating a new paper.
- P = papers[papers.len]
- papers.Remove(P)
- else
- if(response == "Regular")
+
+ if(LAZYLEN(papers) < 1 && amount < 1)
+ to_chat(user, SPAN_WARNING("[src] is empty!"))
+ return
+
+ var/obj/item/paper/P
+ if(LAZYLEN(papers) > 0) //If there's any custom paper on the stack, use that instead of creating a new paper.
+ P = papers[papers.len]
+ LAZYREMOVE(papers, P)
+ else
+ var/paper_kind = input(user, "What kind of paper?") in list("Regular", "Green", "Blue", "Pink", "Yellow", "Carbon-Copy", "Cancel")
+ switch(paper_kind)
+ if("Regular")
P = new /obj/item/paper
- if(global.current_holiday?.name == "April Fool's Day")
- if(prob(30))
- P.info = "
HONK HONK HONK HONK HONK HONK HONK
HOOOOOOOOOOOOOOOOOOOOOONK
APRIL FOOLS"
- P.rigged = 1
- P.updateinfolinks()
- else if (response == "Carbon-Copy")
+ if("Green")
+ P = new /obj/item/paper/green
+ if("Blue")
+ P = new /obj/item/paper/blue
+ if("Pink")
+ P = new /obj/item/paper/pink
+ if("Yellow")
+ P = new /obj/item/paper/yellow
+ if("Carbon-Copy")
P = new /obj/item/paper/carbon
- user.put_in_hands(P)
- to_chat(user, "
You take [P] out of the [src].")
- else
- to_chat(user, "
[src] is empty!")
+ else
+ return
+
+ if(!istype(P, /obj/item/paper/carbon) && global.current_holiday?.name == "April Fool's Day" && prob(30))
+ P.rigged = TRUE
+ P.set_content("
HONK HONK HONK HONK HONK HONK HONK
HOOOOOOOOOOOOOOOOOOOOOONK
APRIL FOOLS")
+ user.put_in_hands(P)
+ to_chat(user, SPAN_NOTICE("You take \the [P] out of \the [src]."))
+ amount--
+ update_icon()
add_fingerprint(user)
return
+/obj/item/paper_bin/attackby(obj/item/I, mob/user)
+ if(istype(I, /obj/item/paper))
+ if(amount >= max_amount)
+ to_chat(user, SPAN_WARNING("\The [src] is full!"))
+ return
+ if(!user.unEquip(I, src))
+ return
+ add_paper(I)
+ to_chat(user, SPAN_NOTICE("You put [I] in [src]."))
+ return TRUE
-/obj/item/paper_bin/attackby(obj/item/i, mob/user)
- if(istype(i, /obj/item/paper))
- if(!user.unEquip(i, src))
+ else if(istype(I, /obj/item/paper_bundle))
+ if(amount >= max_amount)
+ to_chat(user, SPAN_WARNING("\The [src] is full!"))
return
- to_chat(user, "
You put [i] in [src].")
- papers.Add(i)
- update_icon()
- amount++
- else if(istype(i, /obj/item/paper_bundle))
- to_chat(user, "
You loosen \the [i] and add its papers into \the [src].")
- var/was_there_a_photo = 0
- for(var/obj/item/bundleitem in i) //loop through items in bundle
+ var/obj/item/paper_bundle/B = I
+ var/was_there_a_photo = FALSE
+ for(var/obj/item/bundleitem in I) //loop through items in bundle
if(istype(bundleitem, /obj/item/paper)) //if item is paper, add into the bin
- papers.Add(bundleitem)
- update_icon()
- amount++
+ LAZYREMOVE(B.pages, bundleitem)
+ add_paper(bundleitem)
else if(istype(bundleitem, /obj/item/photo)) //if item is photo, drop it on the ground
- was_there_a_photo = 1
+ was_there_a_photo = TRUE
bundleitem.dropInto(user.loc)
bundleitem.reset_plane_and_layer()
- qdel(i)
+ to_chat(user, SPAN_NOTICE("You loosen \the [I] and add its papers into \the [src]."))
+ B.reevaluate_existence()
if(was_there_a_photo)
- to_chat(user, "
The photo cannot go into \the [src].")
+ to_chat(user, SPAN_NOTICE("The photo cannot go into \the [src]."))
+ return TRUE
+ return ..()
/obj/item/paper_bin/examine(mob/user, distance)
. = ..()
- if(distance <= 1)
- if(amount)
- to_chat(user, "
There " + (amount > 1 ? "are [amount] papers" : "is one paper") + " in the bin.")
- else
- to_chat(user, "
There are no papers in the bin.")
-
+ if(distance > 1)
+ return
+ if(amount)
+ to_chat(user, SPAN_NOTICE("There [(amount > 1 ? "are [amount] papers" : "is one paper")] in the bin."))
+ else
+ to_chat(user, SPAN_NOTICE("There are no papers in the bin."))
+ to_chat(user, SPAN_NOTICE("It can contain at most [max_amount] papers."))
/obj/item/paper_bin/on_update_icon()
- if(amount < 1)
+ . = ..()
+ if(amount <= 0)
icon_state = "paper_bin0"
- else
+ else if(amount <= (max_amount / 3))
icon_state = "paper_bin1"
+ else if(amount >= max_amount)
+ icon_state = "paper_bin3"
+ else
+ icon_state = "paper_bin2"
+
+/obj/item/paper_bin/dump_contents()
+ . = ..()
+ //Dump all stored papers too
+ for(var/i=1 to amount)
+ var/obj/item/paper/P = new /obj/item/paper(loc)
+ P.merge_with_existing(loc, usr)
+ LAZYCLEARLIST(papers)
+
+/obj/item/paper_bin/proc/add_paper(var/obj/item/paper/P)
+ if(amount >= max_amount)
+ return
+ //Add only non-blank papers into the paper list
+ if(P.is_blank())
+ qdel(P)
+ else
+ LAZYDISTINCTADD(papers, P)
+ P.forceMove(src)
+ amount++
+ update_icon()
+ return TRUE
+
+/obj/item/paper_bin/get_alt_interactions(mob/user)
+ . = ..()
+ LAZYADD(., /decl/interaction_handler/paper_bin_dump_contents)
+
+/////////////////////////////////////////////////////////////////
+// Empty Bin Interaction
+/////////////////////////////////////////////////////////////////
+/decl/interaction_handler/paper_bin_dump_contents
+ name = "Dump Contents"
+ expected_target_type = /obj/item/paper_bin
+
+/decl/interaction_handler/paper_bin_dump_contents/is_possible(var/obj/item/paper_bin/target, mob/user, obj/item/prop)
+ return ..() && target.amount > 0
+
+/decl/interaction_handler/paper_bin_dump_contents/invoked(var/obj/item/paper_bin/bin, mob/user)
+ to_chat(user, SPAN_NOTICE("You start emptying \the [bin]..."))
+ if(do_after(user, 2 SECONDS) && !QDELETED(bin))
+ bin.dump_contents()
+ to_chat(user, SPAN_NOTICE("You emptied \the [bin]."))
diff --git a/code/modules/paperwork/papershredder.dm b/code/modules/paperwork/papershredder.dm
index dc13cb371cf..582bcd4e5d3 100644
--- a/code/modules/paperwork/papershredder.dm
+++ b/code/modules/paperwork/papershredder.dm
@@ -1,148 +1,251 @@
+//////////////////////////////////////////////////////////////////
+// Paper Shredder
+//////////////////////////////////////////////////////////////////
/obj/machinery/papershredder
- name = "paper shredder"
- desc = "For those documents you don't want seen."
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "papershredder0"
- density = 1
- anchored = 1
- atom_flags = ATOM_FLAG_NO_TEMP_CHANGE | ATOM_FLAG_CLIMBABLE
- obj_flags = OBJ_FLAG_ANCHORABLE
- var/max_paper = 10
- var/paperamount = 0
-
- var/list/shred_amounts = list(
- /obj/item/photo = 1,
- /obj/item/shreddedp = 1,
- /obj/item/paper = 1,
- /obj/item/newspaper = 3,
- /obj/item/card/id = 3,
- /obj/item/paper_bundle = 3,
- /obj/item/forensics/sample/print = 1
- )
+ name = "paper shredder"
+ desc = "For those documents you don't want seen."
+ icon = 'icons/obj/machines/paper_shredder.dmi'
+ icon_state = "papershredder0"
+ density = TRUE
+ anchored = TRUE
+ atom_flags = ATOM_FLAG_NO_TEMP_CHANGE | ATOM_FLAG_CLIMBABLE
+ obj_flags = OBJ_FLAG_ANCHORABLE
+ idle_power_usage = 0
+ stat_immune = NOSCREEN | NOINPUT
+ waterproof = FALSE
+ construct_state = /decl/machine_construction/default/panel_closed
+ required_interaction_dexterity = DEXTERITY_SIMPLE_MACHINES
+ uncreated_component_parts = list(
+ /obj/item/stock_parts/power/apc = 1,
+ )
+ var/list/shredder_bin //List of shreded material type to matter amount
+ var/cached_total_matter = 0 //Total of all the matter units we put in the shredder so far
+ var/tmp/max_total_matter = SHEET_MATERIAL_AMOUNT * 10 //Maximum amount of matter that can be stored inside the bin
+
+/obj/machinery/papershredder/Initialize()
+ . = ..()
+ update_icon()
-/obj/machinery/papershredder/attackby(var/obj/item/W, var/mob/user)
+/obj/machinery/papershredder/on_component_failure(obj/item/stock_parts/component)
+ . = ..()
- if(istype(W, /obj/item/storage))
- empty_bin(user, W)
+ if(istype(component, /obj/item/stock_parts/circuitboard))
+ spark_at(get_turf(src), 30, FALSE, src)
+ empty_bin(violent = TRUE)
+
+/**Shreds the given object. */
+/obj/machinery/papershredder/proc/shred(var/obj/item/I, var/mob/user)
+ if(inoperable())
+ to_chat(user, SPAN_WARNING("\The [src] doesn't seem to work."))
return
- else
- var/paper_result
- for(var/shred_type in shred_amounts)
- if(istype(W, shred_type))
- paper_result = shred_amounts[shred_type]
- if(paper_result)
- if(paperamount == max_paper)
- to_chat(user, "
\The [src] is full; please empty it before you continue.")
- return
- paperamount += paper_result
- qdel(W)
- playsound(src.loc, 'sound/items/pshred.ogg', 75, 1)
- if(paperamount > max_paper)
- to_chat(user, "
\The [src] was too full, and shredded paper goes everywhere!")
- for(var/i=(paperamount-max_paper);i>0;i--)
- var/obj/item/shreddedp/SP = get_shredded_paper()
- SP.dropInto(loc)
- SP.throw_at(get_edge_target_turf(src,pick(global.alldirs)),1,5)
- paperamount = max_paper
- update_icon()
- return
- ..()
- return
-
-/obj/machinery/papershredder/verb/empty_contents()
- set name = "Empty bin"
- set category = "Object"
- set src in range(1)
-
- if(usr.incapacitated())
+ if(is_bin_full())
+ visible_message(SPAN_WARNING("The \"bin full\" warning light is flashing on \the [src]!"))
return
+ //#TODO: Uncomment this once the bug is fixed, so we check for available power before actually working
+ // if(!powered() || can_use_power_oneoff(60) <= 0)
+ // to_chat(user, SPAN_WARNING("\The [src] seems to be lacking power..."))
+ // return
+ use_power_oneoff(60)
+ user.unEquip(I, src)
+
+ //If the material is too hard damage the shredder
+ var/decl/material/M = I.material
+ if(M.hardness > MAT_VALUE_FLEXIBLE && M.hardness < MAT_VALUE_RIGID)
+ audible_message(SPAN_WARNING("You hear a loud mechanical grinding!"))
+ take_damage(1, BRUTE, TRUE)
+ spark_at(get_turf(src), 1, FALSE, src)
+ . = TRUE
+
+ else if(M.hardness >= MAT_VALUE_RIGID)
+ audible_message(SPAN_DANGER("You hear rattling and then a loud bang!"))
+ use_power_oneoff(200)
+ take_damage(25, BRUTE, TRUE)
+ set_broken(TRUE, MACHINE_BROKEN_GENERIC)
+ . = FALSE
- if(!paperamount)
- to_chat(usr, "
\The [src] is empty.")
- return
+ else
+ visible_message(SPAN_NOTICE("\The [src] happily consumes \the [I]."))
+ . = TRUE
+
+ if(.)
+ //Move over the matter
+ for(var/key in I.matter)
+ if(I.matter[key] < 1)
+ continue
+ var/new_matter = LAZYACCESS(shredder_bin, key) + I.matter[key]
+ cached_total_matter += new_matter
+ LAZYSET(shredder_bin, key, new_matter)
+
+ I.physically_destroyed()
+ playsound(get_turf(src), 'sound/items/pshred.ogg', 50, TRUE)
+ else
+ I.dropInto(get_turf(user))
+ playsound(get_turf(src), 'sound/effects/metalscrape1.ogg', 40, TRUE)
+ update_icon()
- empty_bin(usr)
+/obj/machinery/papershredder/proc/is_bin_full()
+ return cached_total_matter >= max_total_matter
-/obj/machinery/papershredder/proc/empty_bin(var/mob/living/user, var/obj/item/storage/empty_into)
-
- if(empty_into) // If the user tries to empty the bin into something
+/obj/machinery/papershredder/proc/is_bin_empty()
+ return !(length(shredder_bin) > 0 && cached_total_matter)
- if(paperamount == 0) // Can't empty what is already empty
- to_chat(user, "
\The [src] is empty.")
- return
+/obj/machinery/papershredder/proc/can_shred(var/obj/item/I, var/mob/user = null)
+ if(!istype(I))
+ if(user)
+ to_chat(user, SPAN_WARNING("\The [I] cannot be shredded by \the [src]!"))
+ return
- if(empty_into && !istype(empty_into)) // Make sure we can store paper in the thing
- to_chat(user, "
You cannot put shredded paper into the [empty_into].")
- return
+ //Being one of those types bypasses the checks
+ if(istype(I, /obj/item/paper) || istype(I, /obj/item/paper_bundle) || istype(I, /obj/item/folder) || istype(I, /obj/item/newspaper) || istype(I, /obj/item/photo))
+ return TRUE
- // Move papers one by one as they fit; stop when we are empty or can't fit any more
- while(paperamount > 0)
+ //Generic tests for random objects
+ if(I.w_class > ITEM_SIZE_TINY)
+ if(user)
+ to_chat(user, SPAN_WARNING("\The [I] is too big to be inserted into \the [src]!"))
+ return
- var/obj/item/shred_temp = get_shredded_paper()
+ var/decl/material/M = I.material
+ if(!istype(M) || M.hardness >= MAT_VALUE_HARD)
+ if(user)
+ to_chat(user, SPAN_WARNING("\The [I] is obviously not shreddable."))
+ return
+ return TRUE
- if(empty_into.can_be_inserted(shred_temp, user, 0))
- empty_into.handle_item_insertion(shred_temp)
- else
- qdel(shred_temp)
- paperamount++
- break
+/obj/machinery/papershredder/attackby(var/obj/item/W, var/mob/user)
+ if(!has_extension(W, /datum/extension/tool)) //Silently skip tools
+ var/trying_to_smack = !(W.item_flags & ITEM_FLAG_NO_BLUDGEON) && user && user.a_intent == I_HURT
+ if(istype(W, /obj/item/storage))
+ empty_bin(user, W)
+ return TRUE
+
+ else if(!trying_to_smack && can_shred(W))
+ shred(W, user)
+ return TRUE
+ return ..()
+
+/**Creates shredded products, and empty the matter bin */
+/obj/machinery/papershredder/proc/create_shredded()
+ for(var/key in shredder_bin)
+ var/decl/material/M = GET_DECL(key)
+ var/amt_per_shard = atom_info_repository.get_matter_for(M.shard_type, key, 1)
+ if(shredder_bin[key] > amt_per_shard)
+ LAZYADD(., M.place_cuttings(src, shredder_bin[key]))
+
+ //Anything leftover we just assume the machine ate or something
+ cached_total_matter = 0
+ LAZYCLEARLIST(shredder_bin)
+
+/**Empties the paper bin into the given container, and/or on the floor. If violent is on, and there's no container passed, we're going to throw around the trash. */
+/obj/machinery/papershredder/proc/empty_bin(var/mob/living/user, var/obj/item/storage/empty_into, var/violent = FALSE)
+ if(is_bin_empty())
+ if(user)
+ to_chat(user, SPAN_NOTICE("\The [src] is empty."))
+ return
+ if(empty_into && !istype(empty_into)) // Make sure we can store paper in the thing
+ if(user)
+ to_chat(user, SPAN_NOTICE("You cannot put shredded paper into the [empty_into]."))
+ return
+
+ //If we got a container put what we can into it
+ var/list/shredded = create_shredded()
+ if(empty_into)
+ for(var/obj/item/I in shredded)
+ if(empty_into.can_be_inserted(I, user, !isnull(user)))
+ empty_into.handle_item_insertion(I, TRUE)
+ LAZYREMOVE(shredded, I)
// Report on how we did
- if(paperamount == 0)
- to_chat(user, "
You empty \the [src] into \the [empty_into].")
- if(paperamount > 0)
- to_chat(user, "
\The [empty_into] will not fit any more shredded paper.")
+ if(user)
+ if(length(shredded) < 1)
+ to_chat(user, SPAN_NOTICE("You empty \the [src] into \the [empty_into]."))
+ else
+ to_chat(user, SPAN_NOTICE("\The [empty_into] will not fit any more shredded paper."))
- else // Just dump the paper out on the floor
- while(paperamount > 0)
- get_shredded_paper()
+ //Drop the leftovers
+ if(LAZYLEN(shredded))
+ var/turf/T = get_turf(user? user : src)
+ for(var/obj/item/I in shredded)
+ I.dropInto(T)
+ if(violent)
+ I.throw_at(get_edge_target_turf(src, pick(global.alldirs)), I.throw_range, I.throw_speed)
update_icon()
-
-/obj/machinery/papershredder/proc/get_shredded_paper()
- if(paperamount)
- paperamount--
- return new /obj/item/shreddedp(get_turf(src))
+ return TRUE
/obj/machinery/papershredder/on_update_icon()
- icon_state = "papershredder[max(0,min(5,FLOOR(paperamount/2)))]"
+ cut_overlays()
+ var/ratio = ((cached_total_matter * 5) / max_total_matter)
+ icon_state = "papershredder[clamp(0, CEILING(ratio), 5)]"
+ if(!is_unpowered())
+ add_overlay("papershredder_power")
+ if(is_broken() || is_bin_full())
+ add_overlay("papershredder_bad")
+
+/obj/machinery/papershredder/get_alt_interactions(mob/user)
+ . = ..()
+ LAZYADD(., /decl/interaction_handler/empty/paper_shredder)
+
+//////////////////////////////////////////////////////////////////
+// Empty Bin Interaction
+//////////////////////////////////////////////////////////////////
+/decl/interaction_handler/empty/paper_shredder
+ name = "Empty Bin"
+ expected_target_type = /obj/machinery/papershredder
+
+/decl/interaction_handler/empty/paper_shredder/is_possible(obj/machinery/papershredder/target, mob/user, obj/item/prop)
+ return ..() && !target.is_bin_empty()
+
+/decl/interaction_handler/empty/paper_shredder/invoked(obj/machinery/papershredder/target, mob/user)
+ target.empty_bin(user)
+
+//////////////////////////////////////////////////////////////////
+// Shredded Paper
+//////////////////////////////////////////////////////////////////
+/obj/item/shreddedp
+ name = "shredded"
+ icon = 'icons/obj/bureaucracy.dmi'
+ icon_state = "shredp"
+ randpixel = 5
+ throw_range = 3
+ throw_speed = 2
+ throwforce = 0
+ w_class = ITEM_SIZE_TINY
+ material = /decl/material/solid/paper
+ material_alteration = MAT_FLAG_ALTERATION_COLOR | MAT_FLAG_ALTERATION_NAME
+
+/obj/item/shreddedp/get_matter_amount_modifier()
+ return 0.2
+
+/obj/item/shreddedp/set_material(new_material)
+ . = ..()
+ if(material)
+ SetName("[initial(name)] [material.solid_name]")
/obj/item/shreddedp/attackby(var/obj/item/W, var/mob/user)
if(istype(W, /obj/item/flame/lighter))
burnpaper(W, user)
- else
- ..()
+ return TRUE
+ return ..()
/obj/item/shreddedp/proc/burnpaper(var/obj/item/flame/lighter/P, var/mob/user)
- if(user.restrained())
+ if(!CanPhysicallyInteractWith(user, src) && material?.fuel_value)
return
if(!P.lit)
- to_chat(user, "
\The [P] is not lit.")
+ to_chat(user, SPAN_WARNING("\The [P] is not lit."))
return
var/decl/pronouns/G = user.get_pronouns()
- user.visible_message("
\The [user] holds \the [P] up to \the [src]. It looks like [G.he] [G.is] trying to burn it!", \
- "
You hold \the [P] up to \the [src], burning it slowly.")
+ user.visible_message(\
+ SPAN_WARNING("\The [user] holds \the [P] up to \the [src]. It looks like [G.he] [G.is] trying to burn it!"), \
+ SPAN_WARNING("You hold \the [P] up to \the [src], burning it slowly."))
if(!do_after(user,20, src))
- to_chat(user, "
You must hold \the [P] steady to burn \the [src].")
+ to_chat(user, SPAN_WARNING("You must hold \the [P] steady to burn \the [src]."))
return
- user.visible_message("
\The [user] burns right through \the [src], turning it to ash. It flutters through the air before settling on the floor in a heap.", \
- "
You burn right through \the [src], turning it to ash. It flutters through the air before settling on the floor in a heap.")
- FireBurn()
+ user.visible_message( \
+ SPAN_DANGER("\The [user] burns right through \the [src], turning it to ash. It flutters through the air before settling on the floor in a heap."), \
+ SPAN_DANGER("You burn right through \the [src], turning it to ash. It flutters through the air before settling on the floor in a heap."))
+ fire_act()
-/obj/item/shreddedp/proc/FireBurn()
+/obj/item/shreddedp/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume)
new /obj/effect/decal/cleanable/ash(get_turf(src))
- qdel(src)
-
-/obj/item/shreddedp
- name = "shredded paper"
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "shredp"
- randpixel = 5
- throwforce = 0
- w_class = ITEM_SIZE_TINY
- throw_range = 3
- throw_speed = 1
-
-/obj/item/shreddedp/Initialize()
- . = ..()
- if(prob(65)) color = pick("#bababa","#7f7f7f")
+ physically_destroyed()
diff --git a/code/modules/paperwork/pen/chameleon_pen.dm b/code/modules/paperwork/pen/chameleon_pen.dm
index c773da9b4b0..3bd6e1cbd44 100644
--- a/code/modules/paperwork/pen/chameleon_pen.dm
+++ b/code/modules/paperwork/pen/chameleon_pen.dm
@@ -1,5 +1,6 @@
-/obj/item/pen/chameleon
- var/signature = ""
+/obj/item/pen/chameleon/Initialize(ml, material_key)
+ . = ..()
+ set_tool_property(TOOL_PEN, TOOL_PROP_PEN_SIG, "Anonymous") //Always default to anonymous for this pen, since it should never uses the user's real_name
/obj/item/pen/chameleon/attack_self(mob/user)
/*
@@ -13,13 +14,8 @@
if(new_signature)
signature = new_signature
*/
- signature = sanitize(input("Enter new signature. Leave blank for 'Anonymous'", "New Signature", signature))
-
-/obj/item/pen/proc/get_signature(var/mob/user)
- return (user && user.real_name) ? user.real_name : "Anonymous"
-
-/obj/item/pen/chameleon/get_signature(var/mob/user)
- return signature ? signature : "Anonymous"
+ var/signature = sanitize(input("Enter new signature. Leave blank for 'Anonymous'", "New Signature", get_tool_property(TOOL_PEN, TOOL_PROP_PEN_SIG)))
+ set_tool_property(TOOL_PEN, TOOL_PROP_PEN_SIG, signature ? signature : "Anonymous")
/obj/item/pen/chameleon/verb/set_colour()
set name = "Change Pen Colour"
@@ -31,30 +27,22 @@
if(selected_type)
switch(selected_type)
if("Yellow")
- colour = COLOR_YELLOW
- color_description = "yellow ink"
+ set_medium_color(COLOR_YELLOW, "yellow")
if("Green")
- colour = COLOR_LIME
- color_description = "green ink"
+ set_medium_color(COLOR_LIME, "green")
if("Pink")
- colour = COLOR_PINK
- color_description = "pink ink"
+ set_medium_color(COLOR_PINK, "pink")
if("Blue")
- colour = COLOR_BLUE
- color_description = "blue ink"
+ set_medium_color(COLOR_BLUE, "blue")
if("Orange")
- colour = COLOR_ORANGE
- color_description = "orange ink"
+ set_medium_color(COLOR_ORANGE, "orange")
if("Cyan")
- colour = COLOR_CYAN
- color_description = "cyan ink"
+ set_medium_color(COLOR_CYAN, "cyan")
if("Red")
- colour = COLOR_RED
- color_description = "red ink"
+ set_medium_color(COLOR_RED, "red")
if("Invisible")
- colour = COLOR_WHITE
- color_description = "transluscent ink"
+ set_medium_color(COLOR_WHITE, "transluscent")
else
- colour = COLOR_BLACK
- color_description = "black ink"
- to_chat(usr, "
You select the [lowertext(selected_type)] ink container.")
\ No newline at end of file
+ set_medium_color(COLOR_BLACK, "black")
+
+ to_chat(usr, SPAN_INFO("You select the [lowertext(selected_type)] [medium_name] container."))
\ No newline at end of file
diff --git a/code/modules/paperwork/pen/crayon.dm b/code/modules/paperwork/pen/crayon.dm
index c70a5bc3876..d3ba803decc 100644
--- a/code/modules/paperwork/pen/crayon.dm
+++ b/code/modules/paperwork/pen/crayon.dm
@@ -1,19 +1,129 @@
/obj/item/pen/crayon
- name = "crayon"
- desc = "A colourful crayon. Please refrain from eating it or putting it in your nose."
- icon = 'icons/obj/items/crayons.dmi'
- icon_state = "crayonred"
- w_class = ITEM_SIZE_TINY
- attack_verb = list("attacked", "coloured")
- colour = "#ff0000" //RGB
- color_description = "red crayon"
- iscrayon = TRUE
-
- var/shadeColour = "#220000" //RGB
- var/uses = 30 //0 for unlimited uses
- var/instant = 0
- var/colourName = "red" //for updateIcon purposes
-
-/obj/item/pen/crayon/Initialize()
- name = "[colourName] crayon"
+ name = "crayon"
+ icon = 'icons/obj/items/crayons.dmi'
+ icon_state = "crayonred"
+ w_class = ITEM_SIZE_TINY
+ attack_verb = list("attacked", "coloured", "crayon'd")
+ stroke_colour = "#ff0000" //RGB
+ stroke_colour_name = "red"
+ medium_name = "crayon"
+ pen_flag = PEN_FLAG_ACTIVE | PEN_FLAG_CRAYON | PEN_FLAG_DEL_EMPTY
+ pen_quality = TOOL_QUALITY_BAD //Writing with those things is awkward
+ max_uses = 30
+ var/shade_colour = "#220000" //RGB
+
+/obj/item/pen/crayon/make_pen_description()
+ desc = "A colourful [stroke_colour_name] [istype(material)?"[material.name] ":null][medium_name]. Please refrain from eating it or putting it in your nose."
+
+/obj/item/pen/crayon/set_medium_color(_color, _color_name, var/_shade_colour)
+ . = ..(_color, _color_name)
+ shade_colour = _shade_colour
+ set_tool_property(TOOL_PEN, TOOL_PROP_PEN_SHADE_COLOR, shade_colour)
+
+/obj/item/pen/crayon/afterattack(turf/target, mob/user, proximity)
+ if(!proximity)
+ return
+
+ if(istype(target) && target.is_floor())
+ var/drawtype = input("Choose what you'd like to draw.", "Crayon scribbles") in list("graffiti","rune","letter","arrow")
+ var/draw_message = "drawing"
+ switch(drawtype)
+ if("letter")
+ drawtype = input("Choose the letter.", "Crayon scribbles") in list(global.alphabet)
+ draw_message = "drawing a letter"
+ if("graffiti")
+ draw_message = "drawing graffiti"
+ if("rune")
+ draw_message = "drawing a rune"
+ if("arrow")
+ drawtype = input("Choose the arrow.", "Crayon scribbles") in list("left", "right", "up", "down")
+ draw_message = "drawing an arrow"
+
+ if(do_tool_interaction(TOOL_PEN, user, target, 5 SECONDS, draw_message, "drawing on", fuel_expenditure = 1))
+ new /obj/effect/decal/cleanable/crayon(target, stroke_colour, shade_colour, drawtype)
+ target.add_fingerprint(user) // Adds their fingerprints to the floor the crayon is drawn on.
+ return
+
+/obj/item/pen/crayon/attack(mob/living/M, mob/user)
+ if(istype(M) && M == user)
+ var/decl/tool_archetype/pen/parch = GET_DECL(TOOL_PEN)
+ playsound(src, 'sound/weapons/bite.ogg')
+ to_chat(M, SPAN_NOTICE("You take a bite of the crayon and swallow it."))
+ M.adjust_nutrition(1)
+ var/uses = get_tool_property(TOOL_PEN, TOOL_PROP_USES)
+ M.reagents.add_reagent(/decl/material/liquid/pigment, min(5,uses)/3)
+ if(parch.decrement_uses(user, src, 5) <= 0)
+ to_chat(M, SPAN_WARNING("You ate your crayon!"))
+ return
. = ..()
+
+/obj/item/pen/crayon/red
+ icon_state = "crayonred"
+ stroke_colour = "#da0000"
+ shade_colour = "#810c0c"
+ stroke_colour_name = "red"
+
+/obj/item/pen/crayon/orange
+ icon_state = "crayonorange"
+ stroke_colour = "#ff9300"
+ stroke_colour_name = "orange"
+ shade_colour = "#a55403"
+
+/obj/item/pen/crayon/yellow
+ icon_state = "crayonyellow"
+ stroke_colour = "#fff200"
+ shade_colour = "#886422"
+ stroke_colour_name = "yellow"
+
+/obj/item/pen/crayon/green
+ icon_state = "crayongreen"
+ stroke_colour = "#a8e61d"
+ shade_colour = "#61840f"
+ stroke_colour_name = "green"
+
+/obj/item/pen/crayon/blue
+ icon_state = "crayonblue"
+ stroke_colour = "#00b7ef"
+ shade_colour = "#0082a8"
+ stroke_colour_name = "blue"
+
+/obj/item/pen/crayon/purple
+ icon_state = "crayonpurple"
+ stroke_colour = "#da00ff"
+ shade_colour = "#810cff"
+ stroke_colour_name = "purple"
+
+/obj/item/pen/crayon/mime
+ icon_state = "crayonmime"
+ stroke_colour = "#ffffff"
+ shade_colour = "#000000"
+ stroke_colour_name = "mime"
+ max_uses = -1 //Infinite
+
+/obj/item/pen/crayon/mime/make_pen_description()
+ desc = "A very sad-looking crayon."
+
+/obj/item/pen/crayon/mime/attack_self(mob/user) //inversion
+ if(stroke_colour != "#ffffff" && shade_colour != "#000000")
+ set_medium_color("#ffffff", stroke_colour_name, "#000000")
+ to_chat(user, "You will now draw in white and black with this crayon.")
+ else
+ set_medium_color("#000000", stroke_colour_name, "#ffffff")
+ to_chat(user, "You will now draw in black and white with this crayon.")
+ return
+
+/obj/item/pen/crayon/rainbow
+ icon_state = "crayonrainbow"
+ stroke_colour = "#fff000"
+ shade_colour = "#000fff"
+ stroke_colour_name = "rainbow"
+ max_uses = -1
+
+/obj/item/pen/crayon/rainbow/make_pen_description()
+ desc = "A very colourful [istype(material)?"[material.name] ":null][medium_name]. Please refrain from eating it or putting it in your nose."
+
+/obj/item/pen/crayon/rainbow/attack_self(mob/user)
+ stroke_colour = input(user, "Please select the main colour.", "Crayon colour") as color
+ shade_colour = input(user, "Please select the shade colour.", "Crayon colour") as color
+ set_medium_color(stroke_colour, stroke_colour_name, shade_colour)
+ return
diff --git a/code/modules/paperwork/pen/fancy.dm b/code/modules/paperwork/pen/fancy.dm
index 8723b47e503..d57b4409ba3 100644
--- a/code/modules/paperwork/pen/fancy.dm
+++ b/code/modules/paperwork/pen/fancy.dm
@@ -1,14 +1,22 @@
/obj/item/pen/fancy
- name = "fancy pen"
- desc = "A high quality traditional fountain pen with an internal reservoir and an extra fine gold-platinum nib. Guaranteed never to leak."
- icon = 'icons/obj/items/pens/pen_fancy.dmi'
- throwforce = 1 //pointy
- colour = "#1c1713" //dark ashy brownish
- material = /decl/material/solid/metal/steel
- isfancy = TRUE
+ name = "fountain pen"
+ icon = 'icons/obj/items/pens/pen_fancy.dmi'
+ sharp = 1 //pointy
+ stroke_colour = "#1c1713" //dark ashy brownish
+ stroke_colour_name = "dark ashy brownish"
+ material = /decl/material/solid/metal/steel
+ pen_flag = PEN_FLAG_ACTIVE | PEN_FLAG_FANCY
+ pen_quality = TOOL_QUALITY_GOOD
+
+/obj/item/pen/fancy/make_pen_description()
+ desc = "A high quality [istype(material)?"[material.name] ":null]traditional [stroke_colour_name] [medium_name] fountain pen with an internal reservoir and an extra fine gold-platinum nib. Guaranteed never to leak."
/obj/item/pen/fancy/quill
- name = "dire goose quill"
+ name = "dire goose quill"
+ icon = 'icons/obj/items/pens/pen_quill.dmi'
+ sharp = 0
+ material = /decl/material/solid/skin/feathers
+ pen_quality = TOOL_QUALITY_BEST
+
+/obj/item/pen/fancy/quill/make_pen_description()
desc = "A quill fashioned from a feather of the dire goose makes an excellent writing instrument, as well as a valuable trophy."
- matter = null
- icon = 'icons/obj/items/pens/pen_quill.dmi'
diff --git a/code/modules/paperwork/pen/multi_pen.dm b/code/modules/paperwork/pen/multi_pen.dm
index 8a64bc13861..f2c7cb636e6 100644
--- a/code/modules/paperwork/pen/multi_pen.dm
+++ b/code/modules/paperwork/pen/multi_pen.dm
@@ -1,20 +1,31 @@
/obj/item/pen/multi
- name = "multicoloured pen"
- desc = "It's a pen with multiple colors of ink!"
- var/selectedColor = 1
- var/colors = list("black","blue","red","green")
- var/color_descriptions = list("black ink", "blue ink", "red ink", "green ink")
- var/color_icons = list(
+ name = "multicoloured pen"
+ desc = "It's a pen with multiple colors of ink!"
+ pen_quality = TOOL_QUALITY_MEDIOCRE
+ var/colour_idx = 1
+ var/stroke_colours = list("black", "blue", "red", "green")
+ var/stroke_colour_names = list("black", "blue", "red", "green")
+ var/colour_icons = list(
'icons/obj/items/pens/pen.dmi',
'icons/obj/items/pens/pen_blue.dmi',
'icons/obj/items/pens/pen_red.dmi',
'icons/obj/items/pens/pen_green.dmi',
)
+/obj/item/pen/multi/Initialize(ml, material_key)
+ . = ..()
+ change_colour(colour_idx)
+
+/obj/item/pen/multi/make_pen_description()
+ desc = "It's [istype(material)?"[ADD_ARTICLE(material.name)]":"a"] pen with multiple colors of ink! It's currently set to [stroke_colour_name] [medium_name]."
+
+/obj/item/pen/multi/proc/change_colour(var/new_idx)
+ colour_idx = new_idx
+ if(colour_idx > length(stroke_colours))
+ colour_idx = 1
+ icon = colour_icons[colour_idx]
+ set_medium_color(stroke_colours[colour_idx], stroke_colour_names[colour_idx])
+
/obj/item/pen/multi/attack_self(mob/user)
- if(++selectedColor > length(colors))
- selectedColor = 1
- colour = colors[selectedColor]
- color_description = color_descriptions[selectedColor]
- icon = color_icons[selectedColor]
- to_chat(user, "
Changed color to '[colour].'")
+ change_colour((++colour_idx))
+ to_chat(user, SPAN_NOTICE("Changed color to '[stroke_colour_name] [medium_name].'"))
diff --git a/code/modules/paperwork/pen/pen.dm b/code/modules/paperwork/pen/pen.dm
index ff280211223..0085a6b9a4f 100644
--- a/code/modules/paperwork/pen/pen.dm
+++ b/code/modules/paperwork/pen/pen.dm
@@ -1,49 +1,35 @@
/obj/item/pen
- desc = "It's a normal black ink pen."
- name = "pen"
- icon = 'icons/obj/items/pens/pen.dmi'
- icon_state = ICON_STATE_WORLD
- slot_flags = SLOT_LOWER_BODY | SLOT_EARS
- throwforce = 0
- w_class = ITEM_SIZE_TINY
- throw_speed = 7
- throw_range = 15
- material = /decl/material/solid/plastic
-
- var/colour = "black" //what colour the ink is!
- var/color_description = "black ink"
- var/active = TRUE
- var/iscrayon = FALSE
- var/isfancy = FALSE
+ name = "pen"
+ desc = ""
+ icon = 'icons/obj/items/pens/pen.dmi'
+ icon_state = ICON_STATE_WORLD
+ slot_flags = SLOT_LOWER_BODY | SLOT_EARS
+ w_class = ITEM_SIZE_TINY
+ throwforce = 0
+ throw_speed = 7
+ throw_range = 15
+ material = /decl/material/solid/plastic
+ var/pen_flag = PEN_FLAG_ACTIVE //Properties/state of the pen used.
+ var/stroke_colour = "black" //What colour the ink is! Can be hexadecimal colour or a colour name string.
+ var/stroke_colour_name = "black" //Human readable name of the stroke colour. Used in text strings, and to identify the nearest colour to the stroke colour.
+ var/medium_name = "ink" //Whatever the pen uses to leave its mark. Used in text strings.
+ var/max_uses = -1 //-1 for unlimited uses.
+ var/pen_quality = TOOL_QUALITY_DEFAULT //What will be set as tool quality for the pen
/obj/item/pen/Initialize(ml, material_key)
. = ..()
- set_extension(src, /datum/extension/tool, list(TOOL_DRILL = TOOL_QUALITY_WORST))
-
-/obj/item/pen/blue
- name = "blue pen"
- desc = "It's a normal blue ink pen."
- icon = 'icons/obj/items/pens/pen_blue.dmi'
- colour = "blue"
- color_description = "blue ink"
-
-/obj/item/pen/red
- name = "red pen"
- desc = "It's a normal red ink pen."
- icon = 'icons/obj/items/pens/pen_red.dmi'
- colour = "red"
- color_description = "red ink"
+ set_extension(src, /datum/extension/tool,
+ list(
+ TOOL_DRILL = TOOL_QUALITY_WORST,
+ TOOL_PEN = pen_quality),
-/obj/item/pen/green
- name = "green pen"
- desc = "It's a normal green ink pen."
- icon = 'icons/obj/items/pens/pen_green.dmi'
- colour = "green"
-
-/obj/item/pen/invisible
- desc = "It's an invisble pen marker."
- colour = "white"
- color_description = "transluscent ink"
+ list(
+ TOOL_PEN = list(
+ TOOL_PROP_COLOR_NAME = stroke_colour_name,
+ TOOL_PROP_COLOR = stroke_colour,
+ TOOL_PROP_PEN_FLAG = pen_flag,
+ TOOL_PROP_USES = max_uses)))
+ make_pen_description()
/obj/item/pen/attack(atom/A, mob/user, target_zone)
if(ismob(A))
@@ -52,13 +38,53 @@
var/mob/living/carbon/human/H = M
var/obj/item/organ/external/head/head = H.get_organ(BP_HEAD, /obj/item/organ/external/head)
if(istype(head))
- head.write_on(user, color_description)
+ head.write_on(user, "[stroke_colour_name] [medium_name]")
else
to_chat(user, SPAN_WARNING("You stab [M] with \the [src]."))
admin_attack_log(user, M, "Stabbed using \a [src]", "Was stabbed with \a [src]", "used \a [src] to stab")
+
else if(istype(A, /obj/item/organ/external/head))
var/obj/item/organ/external/head/head = A
- head.write_on(user, color_description)
+ head.write_on(user, "[stroke_colour_name] [medium_name]")
/obj/item/pen/proc/toggle()
- return
\ No newline at end of file
+ if(pen_flag & PEN_FLAG_ACTIVE)
+ pen_flag &= ~PEN_FLAG_ACTIVE
+ else
+ pen_flag |= PEN_FLAG_ACTIVE
+ playsound(src, 'sound/items/penclick.ogg', 5, 0, -4)
+ set_tool_property(TOOL_PEN, TOOL_PROP_PEN_FLAG, pen_flag)
+ update_icon()
+
+/obj/item/pen/proc/set_medium_color(var/_color, var/_color_name)
+ stroke_colour = _color
+ stroke_colour_name = _color_name
+ set_tool_property(TOOL_PEN, TOOL_PROP_COLOR, stroke_colour)
+ set_tool_property(TOOL_PEN, TOOL_PROP_COLOR_NAME, stroke_colour_name)
+ make_pen_description()
+
+/obj/item/pen/proc/make_pen_description()
+ desc = "Its [ADD_ARTICLE(stroke_colour_name)] [medium_name] [istype(material)? material.name : ""] pen."
+
+/obj/item/pen/blue
+ name = "blue pen"
+ icon = 'icons/obj/items/pens/pen_blue.dmi'
+ stroke_colour = "blue"
+ stroke_colour_name = "blue"
+
+/obj/item/pen/red
+ name = "red pen"
+ icon = 'icons/obj/items/pens/pen_red.dmi'
+ stroke_colour = "red"
+ stroke_colour_name = "red"
+
+/obj/item/pen/green
+ name = "green pen"
+ icon = 'icons/obj/items/pens/pen_green.dmi'
+ stroke_colour = "green"
+ stroke_colour_name = "green"
+
+/obj/item/pen/invisible
+ name = "pen"
+ stroke_colour = "white"
+ stroke_colour_name = "transluscent"
diff --git a/code/modules/paperwork/pen/reagent_pen.dm b/code/modules/paperwork/pen/reagent_pen.dm
index a762e25f4e9..1b531b72cce 100644
--- a/code/modules/paperwork/pen/reagent_pen.dm
+++ b/code/modules/paperwork/pen/reagent_pen.dm
@@ -1,10 +1,16 @@
/obj/item/pen/reagent
- atom_flags = ATOM_FLAG_OPEN_CONTAINER
+ atom_flags = ATOM_FLAG_OPEN_CONTAINER
origin_tech = "{'materials':2,'esoteric':5}"
+ sharp = 1
+ pen_quality = TOOL_QUALITY_MEDIOCRE
/obj/item/pen/reagent/Initialize()
. = ..()
+ initialize_reagents()
+
+/obj/item/pen/reagent/initialize_reagents(populate = TRUE)
create_reagents(30)
+ . = ..()
/obj/item/pen/reagent/attack(mob/living/M, mob/living/user, var/target_zone)
@@ -32,9 +38,10 @@
* Sleepy Pens
*/
/obj/item/pen/reagent/sleepy
- desc = "It's a black ink pen with a sharp point and a carefully engraved \"Waffle Co.\"."
origin_tech = "{'materials':2,'esoteric':5}"
-/obj/item/pen/reagent/sleepy/Initialize()
- . = ..()
- reagents.add_reagent(/decl/material/liquid/paralytics, 15)
+/obj/item/pen/reagent/sleepy/make_pen_description()
+ desc = "It's \a [stroke_colour_name] [medium_name] pen with a sharp point and a carefully engraved \"Waffle Co.\"."
+
+/obj/item/pen/reagent/sleepy/populate_reagents()
+ reagents.add_reagent(/decl/material/liquid/paralytics, round(reagents.maximum_volume/2))
diff --git a/code/modules/paperwork/pen/retractable_pen.dm b/code/modules/paperwork/pen/retractable_pen.dm
index 9af09e7ff62..3e728611f78 100644
--- a/code/modules/paperwork/pen/retractable_pen.dm
+++ b/code/modules/paperwork/pen/retractable_pen.dm
@@ -1,41 +1,37 @@
/obj/item/pen/retractable
- desc = "It's a retractable pen."
- active = FALSE
- icon = 'icons/obj/items/pens/pen_retractable.dmi'
+ desc = "It's a retractable pen."
+ icon = 'icons/obj/items/pens/pen_retractable.dmi'
+ pen_flag = PEN_FLAG_TOGGLEABLE
/obj/item/pen/retractable/blue
- colour = "blue"
- color_description = "blue ink"
+ stroke_colour = "blue"
+ stroke_colour_name = "blue"
icon = 'icons/obj/items/pens/pen_retractable_blue.dmi'
/obj/item/pen/retractable/red
- colour = "red"
- color_description = "red ink"
+ stroke_colour = "red"
+ stroke_colour_name = "red"
icon = 'icons/obj/items/pens/pen_retractable_red.dmi'
/obj/item/pen/retractable/green
- colour = "green"
- color_description = "green ink"
+ stroke_colour = "green"
+ stroke_colour_name = "green"
icon = 'icons/obj/items/pens/pen_retractable_green.dmi'
/obj/item/pen/retractable/Initialize()
. = ..()
- desc = "It's a retractable [color_description] pen."
+ desc = "It's a retractable [stroke_colour_name] [medium_name] pen."
/obj/item/pen/retractable/on_update_icon()
+ . = ..()
icon_state = get_world_inventory_state()
- if(active)
+ if(pen_flag & PEN_FLAG_ACTIVE)
icon_state = "[icon_state]-on"
/obj/item/pen/retractable/attack(atom/A, mob/user, target_zone)
- if(!active)
+ if(!(pen_flag & PEN_FLAG_ACTIVE))
toggle()
..()
/obj/item/pen/retractable/attack_self(mob/user)
toggle()
-
-/obj/item/pen/retractable/toggle()
- active = !active
- playsound(src, 'sound/items/penclick.ogg', 5, 0, -4)
- update_icon()
diff --git a/code/modules/paperwork/photocopier.dm b/code/modules/paperwork/photocopier.dm
index 59801c90a61..920e940173e 100644
--- a/code/modules/paperwork/photocopier.dm
+++ b/code/modules/paperwork/photocopier.dm
@@ -1,222 +1,295 @@
+////////////////////////////////////////////////////////////////////////////////////////
+// Photocopier Board
+////////////////////////////////////////////////////////////////////////////////////////
+/obj/item/stock_parts/circuitboard/photocopier
+ name = "circuitboard (photocopier)"
+ build_path = /obj/machinery/photocopier
+ board_type = "machine"
+ origin_tech = "{'engineering':1, 'programming':1}"
+ req_components = list(
+ /obj/item/stock_parts/printer/buildable = 1,
+ /obj/item/stock_parts/manipulator = 2,
+ /obj/item/stock_parts/scanning_module = 1,
+ )
+ additional_spawn_components = list(
+ /obj/item/stock_parts/console_screen = 1,
+ /obj/item/stock_parts/keyboard = 1,
+ /obj/item/stock_parts/power/apc/buildable = 1
+ )
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Photocopier
+////////////////////////////////////////////////////////////////////////////////////////
/obj/machinery/photocopier
- name = "photocopier"
- icon = 'icons/obj/bureaucracy.dmi'
- icon_state = "photocopier"
- var/insert_anim = "photocopier_animation"
- anchored = 1
- density = 1
- idle_power_usage = 30
- active_power_usage = 200
- power_channel = EQUIP
- atom_flags = ATOM_FLAG_NO_TEMP_CHANGE | ATOM_FLAG_CLIMBABLE
- obj_flags = OBJ_FLAG_ANCHORABLE
- var/obj/item/copyitem = null //what's in the copier!
- var/copies = 1 //how many copies to print!
- var/toner = 30 //how much toner is left! woooooo~
- var/maxcopies = 10 //how many copies can be copied at once- idea shamelessly stolen from bs12's copier!
+ name = "photocopier"
+ icon = 'icons/obj/machines/photocopier.dmi'
+ icon_state = "photocopier"
+ anchored = TRUE
+ density = TRUE
+ idle_power_usage = 30
+ active_power_usage = 200
+ atom_flags = ATOM_FLAG_NO_TEMP_CHANGE | ATOM_FLAG_CLIMBABLE
+ obj_flags = OBJ_FLAG_ANCHORABLE
+ construct_state = /decl/machine_construction/default/panel_closed
+ maximum_component_parts = list(
+ /obj/item/stock_parts/printer = 1,
+ /obj/item/stock_parts = 10,
+ )
+ uncreated_component_parts = null
+ var/tmp/insert_anim = "photocopier_animation"
+ var/obj/item/scanner_item //what's in the scanner
+ var/obj/item/stock_parts/printer/printer //What handles the printing queue
+ var/tmp/max_copies = 10 //how many copies can be copied at once- idea shamelessly stolen from bs12's copier!
+ var/tmp/busy = FALSE //Whether we should allow people to mess with the settings and contents
+ var/accept_refill = FALSE //Whether we should handle attackby paper to be sent to the paper bin, or to the scanner slot
+ var/total_printing = 0 //The total number of pages we are printing in the current run
+
+/obj/machinery/photocopier/Initialize(mapload, d=0, populate_parts = TRUE)
+ . = ..()
+ if(.!= INITIALIZE_HINT_QDEL && populate_parts && printer)
+ //Mapped photocopiers shall spawn with ink and paper
+ printer.make_full()
+
+/obj/machinery/photocopier/Destroy()
+ scanner_item = null
+ printer = null
+ return ..()
+
+/obj/machinery/photocopier/RefreshParts()
+ . = ..()
+ printer = get_component_of_type(/obj/item/stock_parts/printer) //Cache the printer component
+ if(printer)
+ printer.show_queue_ctrl = FALSE //Make sure we don't let users mess with the print queue
+ printer.register_on_printed_page( CALLBACK(src, /obj/machinery/photocopier/proc/update_ui))
+ printer.register_on_finished_queue(CALLBACK(src, /obj/machinery/photocopier/proc/update_ui))
+ printer.register_on_print_error( CALLBACK(src, /obj/machinery/photocopier/proc/update_ui))
+ printer.register_on_status_changed(CALLBACK(src, /obj/machinery/photocopier/proc/update_ui))
+
+/obj/machinery/photocopier/on_update_icon()
+ cut_overlays()
+ //Set the icon first
+ if(scanner_item)
+ icon_state = "photocopier_paper"
+ else
+ icon_state = initial(icon_state)
+
+ //If powered and working add the flashing lights
+ if(inoperable())
+ return
+ //Warning lights
+ if(scanner_item)
+ add_overlay("photocopier_ready")
+ if(!printer?.has_enough_to_print())
+ add_overlay("photocopier_bad")
+
+/obj/machinery/photocopier/proc/update_ui()
+ SSnano.update_uis(src)
+ update_icon()
+
+/obj/machinery/photocopier/proc/queue_copies(var/copy_amount, var/mob/user)
+ if(!scanner_item)
+ if(user)
+ to_chat(user, SPAN_WARNING("Insert something to copy first!"))
+ return FALSE
+
+ //First, check we have enough to print the copies
+ var/required_toner = 0
+ var/required_paper = 1
+
+ //Compile the total amount needed for printing the whole bundle if applicable
+ if(istype(scanner_item, /obj/item/paper_bundle))
+ var/obj/item/paper_bundle/B = scanner_item
+ for(var/obj/item/I in B.pages)
+ required_toner += istype(I, /obj/item/photo)? TONER_USAGE_PHOTO : TONER_USAGE_PAPER
+ required_paper = length(B.pages)
+ else if(istype(scanner_item, /obj/item/photo))
+ required_toner = TONER_USAGE_PHOTO
+ else
+ required_toner = TONER_USAGE_PAPER
+
+ if(!printer?.has_enough_to_print(required_toner, required_paper * copy_amount))
+ buzz("Warning: Not enough paper or toner!")
+ return FALSE
+
+ //If we have enough go ahead
+ var/list/obj/item/scanned_item = scan_item(scanner_item) //Generate the copies we'll queue for printing
+ for(var/i=1 to copy_amount)
+ for(var/obj/item/page in scanned_item)
+ printer.queue_job(page)
+
+ //Play the scanner animation
+ flick(insert_anim, src)
+
+ //Actually start printing out the copies we created when queueing
+ start_processing_queue()
+ return TRUE
+
+/obj/machinery/photocopier/proc/start_processing_queue()
+ if(!printer)
+ return FALSE
+ audible_message(SPAN_NOTICE("\The [src] whirrs into action."))
+ total_printing = printer.get_amount_queued()
+ printer.start_printing_queue()
+
+ use_power_oneoff(active_power_usage)
+ update_icon()
+ SSnano.update_uis(src)
+ return TRUE
+
+/obj/machinery/photocopier/proc/stop_processing_queue()
+ if(!printer)
+ return FALSE
+ total_printing = 0
+ printer.stop_printing_queue()
+ printer.clear_job_queue()
+
+ update_use_power(POWER_USE_IDLE)
+ update_icon()
+ SSnano.update_uis(src)
+ return TRUE
/obj/machinery/photocopier/interface_interact(mob/user)
- interact(user)
+ ui_interact(user)
return TRUE
-/obj/machinery/photocopier/interact(mob/user)
- user.set_machine(src)
-
- var/dat = "Photocopier
"
- if(copyitem)
- dat += "
Remove Item"
- if(toner)
- dat += "
Copy"
- dat += "Printing: [copies] copies."
- dat += "
- "
- dat += "
+"
- else if(toner)
- dat += "Please insert something to copy.
"
- if(istype(user,/mob/living/silicon))
- dat += "
Print photo from database"
- dat += "Current toner level: [toner]"
- if(!toner)
- dat +="
Please insert a new toner cartridge!"
- show_browser(user, dat, "window=copier")
- onclose(user, "copier")
- return
+/obj/machinery/photocopier/proc/get_name_copy_item()
+ if(istype(scanner_item, /obj/item/paper))
+ return "Sheet of paper"
+ else if(istype(scanner_item, /obj/item/paper_bundle))
+ return "Paper bundle"
+ else if(istype(scanner_item, /obj/item/photo))
+ return "Photo"
-/obj/machinery/photocopier/OnTopic(user, href_list, state)
- if(href_list["copy"])
- for(var/i = 0, i < copies, i++)
- if(toner <= 0)
- break
- if (istype(copyitem, /obj/item/paper))
- copy(copyitem, 1)
- sleep(15)
- else if (istype(copyitem, /obj/item/photo))
- photocopy(copyitem)
- sleep(15)
- else if (istype(copyitem, /obj/item/paper_bundle))
- var/obj/item/paper_bundle/B = bundlecopy(copyitem)
- sleep(15*B.pages.len)
- else
- to_chat(user, "
\The [copyitem] can't be copied by \the [src].")
- break
+/obj/machinery/photocopier/ui_data(mob/user, ui_key)
+ . = ..()
+ //Printer header stuff
+ if(printer)
+ LAZYADD(., printer.ui_data(user))
+
+ //Photocopier stuff
+ LAZYSET(., "src", "\ref[src]")
+ LAZYSET(., "is_sillicon_mode", issilicon(user))
+ LAZYSET(., "copies_max", max_copies)
+ LAZYSET(., "is_operational", operable())
+ LAZYSET(., "total_printing", total_printing)
+ LAZYSET(., "loaded_item_name", get_name_copy_item())
- use_power_oneoff(active_power_usage)
- return TOPIC_REFRESH
+/obj/machinery/photocopier/ui_interact(mob/user, ui_key, datum/nanoui/ui, force_open, datum/nanoui/master_ui, datum/topic_state/state)
+ var/list/data = ui_data(user, ui_key)
+ ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if (!ui)
+ ui = new(user, src, ui_key, "photocopier.tmpl", name, 640, 480)
+ ui.add_template("stock_parts_printer_shared", "stock_parts_printer.tmpl") //printer info header
+ ui.set_initial_data(data)
+ ui.open()
- if(href_list["remove"])
- OnRemove(user)
- return TOPIC_REFRESH
+/obj/machinery/photocopier/DefaultTopicState()
+ return global.physical_topic_state
- if(href_list["min"])
- if(copies > 1)
- copies--
+/obj/machinery/photocopier/OnTopic(user, href_list, state)
+ //We don't plug in the printer's own OnTopic here since we don't want to allow the user control over it
+
+ if(href_list["eject"])
+ eject_item(user)
return TOPIC_REFRESH
- else if(href_list["add"])
- if(copies < maxcopies)
- copies++
+ if(href_list["copy_amount"])
+ queue_copies(sanitize_integer(text2num(href_list["copy_amount"]), 1, max_copies, 1))
return TOPIC_REFRESH
if(href_list["aipic"])
- if(!istype(user,/mob/living/silicon)) return
-
- if(toner >= 5)
+ if(!istype(user,/mob/living/silicon))
+ return TOPIC_NOACTION
+ if(printer?.has_enough_to_print(TONER_USAGE_PHOTO))
var/mob/living/silicon/tempAI = user
var/obj/item/camera/siliconcam/camera = tempAI.silicon_camera
-
if(!camera)
- return
+ return TOPIC_NOACTION
var/obj/item/photo/selection = camera.selectpicture()
if (!selection)
- return
+ return TOPIC_NOACTION
- var/obj/item/photo/p = photocopy(selection)
+ var/obj/item/photo/p = selection.Clone()
if (p.desc == "")
p.desc += "Copied by [tempAI.name]"
else
p.desc += " - Copied by [tempAI.name]"
- toner -= 5
- sleep(15)
+ printer.queue_job(p)
+ start_processing_queue()
+ else
+ to_chat(user, SPAN_WARNING("Not enough toner and/or paper to print!"))
+ return TOPIC_NOACTION
return TOPIC_REFRESH
-/obj/machinery/photocopier/proc/OnRemove(mob/user)
- if(copyitem)
- user.put_in_hands(copyitem)
- to_chat(user, "
You take \the [copyitem] out of \the [src].")
- copyitem = null
+/obj/machinery/photocopier/proc/insert_item(var/obj/item/I, var/mob/user)
+ if(!scanner_item)
+ if(!user.unEquip(I, src))
+ return
+ scanner_item = I
+ to_chat(user, SPAN_NOTICE("You insert \the [I] into \the [src]."))
+ SSnano.update_uis(src)
+ update_icon()
+ return TRUE
+ else
+ to_chat(user, SPAN_NOTICE("There is already something in \the [src]."))
+
+/obj/machinery/photocopier/proc/eject_item(var/mob/user)
+ if(!scanner_item)
+ return
+ user.put_in_hands(scanner_item)
+ to_chat(user, SPAN_NOTICE("You take \the [scanner_item] out of \the [src]."))
+ scanner_item = null
+ SSnano.update_uis(src)
+ update_icon()
+ return TRUE
/obj/machinery/photocopier/attackby(obj/item/O, mob/user)
- if(istype(O, /obj/item/paper) || istype(O, /obj/item/photo) || istype(O, /obj/item/paper_bundle))
- if(!copyitem)
- if(!user.unEquip(O, src))
- return
- copyitem = O
- to_chat(user, "
You insert \the [O] into \the [src].")
- flick(insert_anim, src)
- updateUsrDialog()
- else
- to_chat(user, "
There is already something in \the [src].")
- else if(istype(O, /obj/item/toner))
- if(toner <= 10) //allow replacing when low toner is affecting the print darkness
- if(!user.unEquip(O, src))
- return
- to_chat(user, "
You insert the toner cartridge into \the [src].")
- var/obj/item/toner/T = O
- toner += T.toner_amount
- qdel(O)
- updateUsrDialog()
- else
- to_chat(user, "
This cartridge is not yet ready for replacement! Use up the rest of the toner.")
- else ..()
-
-/obj/machinery/photocopier/explosion_act(severity)
- ..()
- if(!QDELETED(src) && (severity == 2 || prob(50)) && toner)
- new /obj/effect/decal/cleanable/blood/oil(get_turf(src))
- toner = 0
-
-/obj/machinery/photocopier/proc/copy(var/obj/item/paper/copy, var/need_toner=1)
- var/obj/item/paper/c = new copy.type(loc, copy.text, copy.name, copy.metadata )
-
- c.color = COLOR_WHITE
-
- if(toner > 10) //lots of toner, make it dark
- c.info = "
"
- else //no toner? shitty copies for you!
- c.info = ""
- var/copied = copy.info
- copied = replacetext(copied, ""//
- c.SetName(copy.name) // -- Doohl
- c.fields = copy.fields
- c.stamps = copy.stamps
- c.stamped = copy.stamped
- c.ico = copy.ico
- c.offset_x = copy.offset_x
- c.offset_y = copy.offset_y
- var/list/temp_overlays = copy.overlays //Iterates through stamps
- var/image/img //and puts a matching
- for (var/j = 1, j <= min(temp_overlays.len, copy.ico.len), j++) //gray overlay onto the copy
- if (findtext(copy.ico[j], "cap") || findtext(copy.ico[j], "cent"))
- img = image('icons/obj/bureaucracy.dmi', "paper_stamp-circle")
- else if (findtext(copy.ico[j], "deny"))
- img = image('icons/obj/bureaucracy.dmi', "paper_stamp-x")
- else
- img = image('icons/obj/bureaucracy.dmi', "paper_stamp-dots")
- img.pixel_x = copy.offset_x[j]
- img.pixel_y = copy.offset_y[j]
- c.overlays += img
- c.updateinfolinks()
- if(need_toner)
- toner--
- if(toner == 0)
- visible_message("A red light on \the [src] flashes, indicating that it is out of toner.")
- c.update_icon()
- return c
-
-/obj/machinery/photocopier/proc/photocopy(var/obj/item/photo/photocopy, var/need_toner=1)
- var/obj/item/photo/p = photocopy.copy()
- p.dropInto(loc)
-
- if(toner > 10) //plenty of toner, go straight greyscale
- p.img.MapColors(rgb(77,77,77), rgb(150,150,150), rgb(28,28,28), rgb(0,0,0))//I'm not sure how expensive this is, but given the many limitations of photocopying, it shouldn't be an issue.
- p.update_icon()
- else //not much toner left, lighten the photo
- p.img.MapColors(rgb(77,77,77), rgb(150,150,150), rgb(28,28,28), rgb(100,100,100))
- p.update_icon()
- if(need_toner)
- toner -= 5 //photos use a lot of ink!
- if(toner < 0)
- toner = 0
- visible_message("A red light on \the [src] flashes, indicating that it is out of toner.")
-
- return p
-
-//If need_toner is 0, the copies will still be lightened when low on toner, however it will not be prevented from printing. TODO: Implement print queues for fax machines and get rid of need_toner
-/obj/machinery/photocopier/proc/bundlecopy(var/obj/item/paper_bundle/bundle, var/need_toner=1)
- var/obj/item/paper_bundle/p = new /obj/item/paper_bundle (src)
- for(var/obj/item/W in bundle.pages)
- if(toner <= 0 && need_toner)
- toner = 0
- visible_message("A red light on \the [src] flashes, indicating that it is out of toner.")
- break
-
- if(istype(W, /obj/item/paper))
- W = copy(W)
- else if(istype(W, /obj/item/photo))
- W = photocopy(W)
- W.forceMove(p)
- p.pages += W
-
- p.dropInto(loc)
- p.update_icon()
- p.icon_state = "paper_words"
- p.SetName(bundle.name)
- return p
-
-/obj/item/toner
- name = "toner cartridge"
- icon = 'icons/obj/items/tonercartridge.dmi'
- icon_state = "tonercartridge"
- var/toner_amount = 30
+ if(istype(construct_state, /decl/machine_construction/default/panel_closed) && (istype(O, /obj/item/paper) || istype(O, /obj/item/photo) || istype(O, /obj/item/paper_bundle)))
+ insert_item(O, user)
+ return TRUE
+ return..() //Components attackby will handle refilling with paper and toner
+
+/**Creates a clone of the specified item. Returns a list of cloned items. */
+/obj/machinery/photocopier/proc/scan_item(var/obj/item/I)
+ LAZYADD(., I.Clone())
+
+/**Check if the amount of toner and paper are available */
+/obj/machinery/photocopier/proc/has_enough_to_print(var/req_toner = TONER_USAGE_PAPER, var/req_paper = 1)
+ return printer?.has_enough_to_print(req_toner, req_paper)
+
+/obj/machinery/photocopier/get_alt_interactions(mob/user)
+ . = ..()
+ LAZYADD(., /decl/interaction_handler/empty/photocopier_paper_bin)
+ LAZYADD(., /decl/interaction_handler/remove/photocopier_scanner_item)
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Empty paper bin
+////////////////////////////////////////////////////////////////////////////////////////
+/decl/interaction_handler/empty/photocopier_paper_bin
+ name = "Empty Paper Bin"
+ expected_target_type = /obj/machinery/photocopier
+
+/decl/interaction_handler/empty/photocopier_paper_bin/is_possible(obj/machinery/photocopier/target, mob/user, obj/item/prop)
+ return (target.printer?.get_amount_paper() > 0) && ..()
+
+/decl/interaction_handler/empty/photocopier_paper_bin/invoked(obj/machinery/photocopier/target, mob/user)
+ if(target.printer?.get_amount_paper() <= 0)
+ return
+ var/obj/item/paper_bundle/B = target.printer?.remove_paper(user)
+ if(B)
+ user.put_in_hands(B)
+ target.update_icon()
+ SSnano.update_uis(target)
+
+////////////////////////////////////////////////////////////////////////////////////////
+// Remove item from scanner
+////////////////////////////////////////////////////////////////////////////////////////
+/decl/interaction_handler/remove/photocopier_scanner_item
+ name = "Remove Item From Scanner"
+ expected_target_type = /obj/machinery/photocopier
+
+/decl/interaction_handler/remove/photocopier_scanner_item/is_possible(obj/machinery/photocopier/target, mob/user, obj/item/prop)
+ return target.scanner_item && ..()
+
+/decl/interaction_handler/remove/photocopier_scanner_item/invoked(obj/machinery/photocopier/target, mob/user)
+ target.eject_item(user)
diff --git a/code/modules/paperwork/photography.dm b/code/modules/paperwork/photography.dm
index efe347a7b6f..cf521a15454 100644
--- a/code/modules/paperwork/photography.dm
+++ b/code/modules/paperwork/photography.dm
@@ -10,72 +10,124 @@
* film *
*******/
/obj/item/camera_film
- name = "film cartridge"
- icon = 'icons/obj/photography.dmi'
- desc = "A camera film cartridge. Insert it into a camera to reload it."
- icon_state = "film"
- item_state = "electropack"
- w_class = ITEM_SIZE_TINY
+ name = "film cartridge"
+ icon = 'icons/obj/photography.dmi'
+ desc = "A camera film cartridge. Insert it into a camera to reload it."
+ icon_state = "film"
+ item_state = "electropack"
+ w_class = ITEM_SIZE_TINY
+ throwforce = 0
+ throw_range = 10
+ material = /decl/material/solid/plastic
+ var/tmp/max_uses = 10
+ var/uses_left = 10
+
+/obj/item/camera_film/Initialize(ml, material_key)
+ set_extension(src, /datum/extension/base_icon_state, icon_state)
+ . = ..()
+ update_icon()
+
+/obj/item/camera_film/on_update_icon()
+ var/datum/extension/base_icon_state/bis = get_extension(src, /datum/extension/base_icon_state)
+ if(uses_left > 1)
+ icon_state = "[bis.base_icon_state]"
+ SetName(initial(name))
+ else
+ icon_state = "[bis.base_icon_state]-empty"
+ SetName("spent [initial(name)]")
+/obj/item/camera_film/proc/use()
+ if(uses_left < 1)
+ return FALSE
+ uses_left--
+ update_icon()
+ return TRUE
+
+/obj/item/camera_film/examine(mob/user, distance, infix, suffix)
+ . = ..()
+ if(uses_left < 1)
+ to_chat(user, SPAN_WARNING("This cartridge is completely spent!"))
+ else
+ to_chat(user, "[uses_left] uses left.")
+
+/obj/item/camera_film/proc/get_remaining()
+ return uses_left
/********
* photo *
********/
-var/global/photo_count = 0
-
/obj/item/photo
- name = "photo"
- icon = 'icons/obj/photography.dmi'
- icon_state = "photo"
- item_state = "paper"
- randpixel = 10
- w_class = ITEM_SIZE_TINY
- var/id
- var/icon/img //Big photo image
- var/scribble //Scribble on the back.
- var/image/tiny
- var/photo_size = 3
-
-/obj/item/photo/Initialize()
+ name = "photo"
+ icon = 'icons/obj/photography.dmi'
+ icon_state = "photo"
+ item_state = "paper"
+ randpixel = 10
+ w_class = ITEM_SIZE_TINY
+ item_flags = ITEM_FLAG_CAN_TAPE
+ material = /decl/material/solid/plastic
+ var/id //Unique id used to name the photo resource to upload to the client, and for synthetic photo synchronization
+ var/icon/img //The actual real photo image
+ var/image/tiny //A thumbnail of the image that's displayed on the actual world icon of the photo
+ var/scribble //User written text on the backside of the photo
+ var/photo_size = 3 //Square size of the pictured scene in turfs
+
+/obj/item/photo/Initialize(ml, material_key, var/icon/_img, var/_scribble)
. = ..()
- id = photo_count++
+ id = sequential_id("obj/item/photo")
+ if(_img)
+ img = _img
+ if(length(_scribble))
+ scribble = _scribble
+ update_icon()
+
+/obj/item/photo/GetCloneArgs()
+ return list(null, material, img, scribble)
+
+/obj/item/photo/PopulateClone(obj/item/photo/clone)
+ clone = ..()
+ clone.photo_size = photo_size
+ return clone
/obj/item/photo/attack_self(mob/user)
user.examinate(src)
+/obj/item/photo/get_matter_amount_modifier()
+ return 0.2
+
/obj/item/photo/on_update_icon()
- overlays.Cut()
- var/scale = 8/(photo_size*32)
+ . = ..()
+
+ var/scale = 8/(photo_size * WORLD_ICON_SIZE)
var/image/small_img = image(img)
small_img.transform *= scale
- small_img.pixel_x = -32*(photo_size-1)/2 - 3
- small_img.pixel_y = -32*(photo_size-1)/2
- overlays |= small_img
-
+ small_img.pixel_x = -WORLD_ICON_SIZE * (photo_size-1)/2 - 3
+ small_img.pixel_y = -WORLD_ICON_SIZE * (photo_size-1)/2
+ add_overlay(small_img)
tiny = image(img)
- tiny.transform *= 0.5*scale
- tiny.underlays += image('icons/obj/bureaucracy.dmi',"photo")
- tiny.pixel_x = -32*(photo_size-1)/2 - 3
- tiny.pixel_y = -32*(photo_size-1)/2 + 3
+ tiny.transform *= 0.5 * scale
+ tiny.underlays += image(icon, "photo_underlay")
+ tiny.pixel_x = -WORLD_ICON_SIZE * (photo_size-1)/2 - 3
+ tiny.pixel_y = -WORLD_ICON_SIZE * (photo_size-1)/2 + 3
/obj/item/photo/attackby(obj/item/P, mob/user)
- if(istype(P, /obj/item/pen))
- var/txt = sanitize(input(user, "What would you like to write on the back?", "Photo Writing", null) as text, 128)
- if(loc == user && user.stat == 0)
- scribble = txt
- ..()
+ if(IS_PEN(P))
+ if(!CanPhysicallyInteractWith(user, src))
+ to_chat(user, SPAN_WARNING("You can't interact with this!"))
+ return
+ scribble = sanitize(input(user, "What would you like to write on the back? (Leave empty to erase)", "Photo Writing", scribble), MAX_DESC_LEN)
+ return TRUE
+ return ..()
/obj/item/photo/examine(mob/user, distance)
. = ..()
+ if(distance > 1)
+ to_chat(user, SPAN_NOTICE("It is too far away."))
+ return
if(!img)
return
- if(distance <= 1)
- show(user)
- to_chat(user, desc)
- else
- to_chat(user, "It is too far away.")
+ interact(user)
-/obj/item/photo/proc/show(mob/user)
+/obj/item/photo/interact(mob/user)
send_rsc(user, img, "tmp_photo_[id].png")
var/photo_html = {"
[name]
@@ -84,9 +136,28 @@ var/global/photo_count = 0
[scribble ? "
Written on the back:
[scribble]" : ""]