diff --git a/code/__DEFINES/configuration.dm b/code/__DEFINES/configuration.dm
index a4bf69b2ad..ac5a8ce735 100644
--- a/code/__DEFINES/configuration.dm
+++ b/code/__DEFINES/configuration.dm
@@ -1,7 +1,6 @@
//config files
#define CONFIG_GET(X) global.config.Get(/datum/config_entry/##X)
#define CONFIG_SET(X, Y) global.config.Set(/datum/config_entry/##X, ##Y)
-#define CONFIG_GET_ENTRY(X) global.config.GetEntryDatum(/datum/config_entry/##X)
#define CONFIG_MAPS_FILE "maps.txt"
diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm
index 1c6e96f9e4..ebcf61b000 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -14,13 +14,15 @@
var/list/modes // allowed modes
var/list/gamemode_cache
var/list/votable_modes // votable modes
- // var/list/ic_filter_regex
var/list/storyteller_cache
var/list/mode_names
var/list/mode_reports
var/list/mode_false_report_weight
var/motd
+ // var/policy
+
+ // var/static/regex/ic_filter_regex
/datum/controller/configuration/proc/admin_reload()
if(IsAdminAdvancedProcCall())
@@ -50,6 +52,11 @@
break
loadmaplist(CONFIG_MAPS_FILE)
LoadMOTD()
+ // LoadPolicy()
+ // LoadChatFilter()
+
+ if (Master)
+ Master.OnConfigLoad()
/datum/controller/configuration/proc/full_wipe()
if(IsAdminAdvancedProcCall())
@@ -105,9 +112,7 @@
var/list/lines = world.file2list("[directory]/[filename]")
var/list/_entries = entries
var/list/postload_required = list()
- var/linenumber = 0
for(var/L in lines)
- linenumber++
L = trim(L)
if(!L)
continue
@@ -135,7 +140,7 @@
if(entry == "$include")
if(!value)
- log_config("LINE [linenumber]: Invalid $include directive: [value]")
+ log_config("Warning: Invalid $include directive: [value]")
else
LoadEntries(value, stack)
++.
@@ -143,7 +148,7 @@
var/datum/config_entry/E = _entries[entry]
if(!E)
- log_config("LINE [linenumber]: Unknown setting: '[entry]'")
+ log_config("Unknown setting in configuration: '[entry]'")
continue
if(lockthis)
@@ -153,9 +158,9 @@
var/datum/config_entry/new_ver = entries_by_type[E.deprecated_by]
var/new_value = E.DeprecationUpdate(value)
var/good_update = istext(new_value)
- log_config("LINE [linenumber]: [entry] is deprecated and will be removed soon. Migrate to [new_ver.name]![good_update ? " Suggested new value is: [new_value]" : ""]")
+ log_config("Entry [entry] is deprecated and will be removed soon. Migrate to [new_ver.name]![good_update ? " Suggested new value is: [new_value]" : ""]")
if(!warned_deprecated_configs)
- addtimer(CALLBACK(GLOBAL_PROC, /proc/message_admins, "This server is using deprecated configuration settings. Please check the logs and update accordingly."), 0)
+ DelayedMessageAdmins("This server is using deprecated configuration settings. Please check the logs and update accordingly.")
warned_deprecated_configs = TRUE
if(good_update)
value = new_value
@@ -163,12 +168,12 @@
else
warning("[new_ver.type] is deprecated but gave no proper return for DeprecationUpdate()")
- var/validated = E.ValidateAndSet(value, TRUE)
+ var/validated = E.ValidateAndSet(value)
if(!validated)
- log_config("LINE [linenumber]: Failed to validate setting \"[value]\" for [entry]")
+ log_config("Failed to validate setting \"[value]\" for [entry]")
else
if(E.modified && !E.dupes_allowed)
- log_config("LINE [linenumber]: Duplicate setting for [entry] ([value], [E.resident_file]) detected! Using latest.")
+ log_config("Duplicate setting for [entry] ([value], [E.resident_file]) detected! Using latest.")
if(E.postload_required)
postload_required[E] = TRUE
@@ -196,13 +201,6 @@
stat("[name]:", statclick)
/datum/controller/configuration/proc/Get(entry_type)
- var/datum/config_entry/E = GetEntryDatum(entry_type)
- if((E.protection & CONFIG_ENTRY_HIDDEN) && IsAdminAdvancedProcCall() && GLOB.LastAdminCalledProc == "Get" && GLOB.LastAdminCalledTargetRef == "[REF(src)]")
- log_admin_private("Config access of [entry_type] attempted by [key_name(usr)]")
- return
- return E.config_entry_value
-
-/datum/controller/configuration/proc/GetEntryDatum(entry_type)
var/datum/config_entry/E = entry_type
var/entry_is_abstract = initial(E.abstract_type) == entry_type
if(entry_is_abstract)
@@ -210,7 +208,10 @@
E = entries_by_type[entry_type]
if(!E)
CRASH("Missing config entry for [entry_type]!")
- return E
+ if((E.protection & CONFIG_ENTRY_HIDDEN) && IsAdminAdvancedProcCall() && GLOB.LastAdminCalledProc == "Get" && GLOB.LastAdminCalledTargetRef == "[REF(src)]")
+ log_admin_private("Config access of [entry_type] attempted by [key_name(usr)]")
+ return
+ return E.config_entry_value
/datum/controller/configuration/proc/Set(entry_type, new_val)
var/datum/config_entry/E = entry_type
@@ -236,7 +237,6 @@
for(var/T in gamemode_cache)
// I wish I didn't have to instance the game modes in order to look up
// their information, but it is the only way (at least that I know of).
- // for future reference: just use initial() lol
var/datum/game_mode/M = new T()
if(M.config_tag)
@@ -258,7 +258,37 @@
var/tm_info = GLOB.revdata.GetTestMergeInfo()
if(motd || tm_info)
motd = motd ? "[motd]
[tm_info]" : tm_info
+/*
+Policy file should be a json file with a single object.
+Value is raw html.
+Possible keywords :
+Job titles / Assigned roles (ghost spawners for example) : Assistant , Captain , Ash Walker
+Mob types : /mob/living/simple_animal/hostile/carp
+Antagonist types : /datum/antagonist/highlander
+Species types : /datum/species/lizard
+special keywords defined in _DEFINES/admin.dm
+
+Example config:
+{
+ "Assistant" : "Don't kill everyone",
+ "/datum/antagonist/highlander" : "Kill everyone",
+ "Ash Walker" : "Kill all spacemans"
+}
+
+*/
+/*
+/datum/controller/configuration/proc/LoadPolicy()
+ policy = list()
+ var/rawpolicy = file2text("[directory]/policy.json")
+ if(rawpolicy)
+ var/parsed = safe_json_decode(rawpolicy)
+ if(!parsed)
+ log_config("JSON parsing failure for policy.json")
+ DelayedMessageAdmins("JSON parsing failure for policy.json")
+ else
+ policy = parsed
+*/
/datum/controller/configuration/proc/loadmaplist(filename)
log_config("Loading config file [filename]...")
filename = "[directory]/[filename]"
@@ -305,6 +335,8 @@
currentmap.voteweight = text2num(data)
if ("default","defaultmap")
defaultmap = currentmap
+ //if ("votable")
+ // currentmap.votable = TRUE
if ("endmap")
LAZYINITLIST(maplist)
maplist[currentmap.map_name] = currentmap
@@ -326,6 +358,7 @@
return new T
return new /datum/game_mode/extended()
+/// For dynamic.
/datum/controller/configuration/proc/pick_storyteller(storyteller_name)
for(var/T in storyteller_cache)
var/datum/dynamic_storyteller/S = T
@@ -334,6 +367,32 @@
return T
return /datum/dynamic_storyteller/classic
+/// Same with this
+/datum/controller/configuration/proc/get_runnable_storytellers()
+ var/list/datum/dynamic_storyteller/runnable_storytellers = new
+ var/list/probabilities = Get(/datum/config_entry/keyed_list/storyteller_weight)
+ var/list/repeated_mode_adjust = Get(/datum/config_entry/number_list/repeated_mode_adjust)
+ var/list/min_player_counts = Get(/datum/config_entry/keyed_list/storyteller_min_players)
+ for(var/T in storyteller_cache)
+ var/datum/dynamic_storyteller/S = T
+ var/config_tag = initial(S.config_tag)
+ var/probability = (config_tag in probabilities) ? probabilities[config_tag] : initial(S.weight)
+ var/min_players = (config_tag in min_player_counts) ? min_player_counts[config_tag] : initial(S.min_players)
+ if(probability <= 0)
+ continue
+ if(length(GLOB.player_list) < min_players)
+ continue
+ if(SSpersistence.saved_storytellers.len == repeated_mode_adjust.len)
+ var/name = initial(S.name)
+ var/recent_round = min(SSpersistence.saved_storytellers.Find(name),3)
+ var/adjustment = 0
+ while(recent_round)
+ adjustment += repeated_mode_adjust[recent_round]
+ recent_round = SSpersistence.saved_modes.Find(name,recent_round+1,0)
+ probability *= ((100-adjustment)/100)
+ runnable_storytellers[S] = probability
+ return runnable_storytellers
+
/datum/controller/configuration/proc/get_runnable_modes()
var/list/datum/game_mode/runnable_modes = new
var/list/probabilities = Get(/datum/config_entry/keyed_list/probability)
@@ -367,32 +426,6 @@
runnable_modes[M] = final_weight
return runnable_modes
-/datum/controller/configuration/proc/get_runnable_storytellers()
- var/list/datum/dynamic_storyteller/runnable_storytellers = new
- var/list/probabilities = Get(/datum/config_entry/keyed_list/storyteller_weight)
- var/list/repeated_mode_adjust = Get(/datum/config_entry/number_list/repeated_mode_adjust)
- var/list/min_player_counts = Get(/datum/config_entry/keyed_list/storyteller_min_players)
- for(var/T in storyteller_cache)
- var/datum/dynamic_storyteller/S = T
- var/config_tag = initial(S.config_tag)
- var/probability = (config_tag in probabilities) ? probabilities[config_tag] : initial(S.weight)
- var/min_players = (config_tag in min_player_counts) ? min_player_counts[config_tag] : initial(S.min_players)
- if(probability <= 0)
- continue
- if(length(GLOB.player_list) < min_players)
- continue
- if(SSpersistence.saved_storytellers.len == repeated_mode_adjust.len)
- var/name = initial(S.name)
- var/recent_round = min(SSpersistence.saved_storytellers.Find(name),3)
- var/adjustment = 0
- while(recent_round)
- adjustment += repeated_mode_adjust[recent_round]
- recent_round = SSpersistence.saved_modes.Find(name,recent_round+1,0)
- probability *= ((100-adjustment)/100)
- runnable_storytellers[S] = probability
- return runnable_storytellers
-
-
/datum/controller/configuration/proc/get_runnable_midround_modes(crew)
var/list/datum/game_mode/runnable_modes = new
var/list/probabilities = Get(/datum/config_entry/keyed_list/probability)
@@ -418,7 +451,6 @@
/*
/datum/controller/configuration/proc/LoadChatFilter()
var/list/in_character_filter = list()
-
if(!fexists("[directory]/in_character_filter.txt"))
return
log_config("Loading config file in_character_filter.txt...")
@@ -428,8 +460,8 @@
if(findtextEx(line,"#",1,2))
continue
in_character_filter += REGEX_QUOTE(line)
-
ic_filter_regex = in_character_filter.len ? regex("\\b([jointext(in_character_filter, "|")])\\b", "i") : null
-
- syncChatRegexes()
*/
+//Message admins when you can.
+/datum/controller/configuration/proc/DelayedMessageAdmins(text)
+ addtimer(CALLBACK(GLOBAL_PROC, /proc/message_admins, text), 0)
diff --git a/code/controllers/controller.dm b/code/controllers/controller.dm
index 06547d120d..c9d5f1e565 100644
--- a/code/controllers/controller.dm
+++ b/code/controllers/controller.dm
@@ -16,4 +16,4 @@
/datum/controller/proc/Recover()
-/datum/controller/proc/stat_entry()
\ No newline at end of file
+/datum/controller/proc/stat_entry()
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index cdbea1de85..8a381558a9 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -76,7 +76,11 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
// Highlander-style: there can only be one! Kill off the old and replace it with the new.
if(!random_seed)
- random_seed = (TEST_RUN_PARAMETER in world.params) ? 29051994 : rand(1, 1e9)
+ #ifdef UNIT_TESTS
+ random_seed = 29051994
+ #else
+ random_seed = rand(1, 1e9)
+ #endif
rand_seed(random_seed)
var/list/_subsystems = list()
@@ -184,9 +188,6 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if(delay)
sleep(delay)
- if(tgs_prime)
- world.TgsInitializationComplete()
-
if(init_sss)
init_subtypes(/datum/controller/subsystem, subsystems)
@@ -219,6 +220,9 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
world.fps = CONFIG_GET(number/fps)
var/initialized_tod = REALTIMEOFDAY
+ if(tgs_prime)
+ world.TgsInitializationComplete()
+
if(sleep_offline_after_initializations)
world.sleep_offline = TRUE
sleep(1)
@@ -643,3 +647,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
processing = CONFIG_GET(number/mc_tick_rate/base_mc_tick_rate)
else if (client_count > CONFIG_GET(number/mc_tick_rate/high_pop_mc_mode_amount))
processing = CONFIG_GET(number/mc_tick_rate/high_pop_mc_tick_rate)
+
+/datum/controller/master/proc/OnConfigLoad()
+ for (var/thing in subsystems)
+ var/datum/controller/subsystem/SS = thing
+ SS.OnConfigLoad()
diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm
index 4ee3a01956..38013fa7e2 100644
--- a/code/controllers/subsystem.dm
+++ b/code/controllers/subsystem.dm
@@ -1,39 +1,91 @@
+/**
+ * # Subsystem base class
+ *
+ * Defines a subsystem to be managed by the [Master Controller][/datum/controller/master]
+ *
+ * Simply define a child of this subsystem, using the [SUBSYSTEM_DEF] macro, and the MC will handle registration.
+ * Changing the name is required
+**/
/datum/controller/subsystem
// Metadata; you should define these.
- name = "fire coderbus" //name of the subsystem
- var/init_order = INIT_ORDER_DEFAULT //order of initialization. Higher numbers are initialized first, lower numbers later. Use defines in __DEFINES/subsystems.dm for easy understanding of order.
- var/wait = 20 //time to wait (in deciseconds) between each call to fire(). Must be a positive integer.
- var/priority = FIRE_PRIORITY_DEFAULT //When mutiple subsystems need to run in the same tick, higher priority subsystems will run first and be given a higher share of the tick before MC_TICK_CHECK triggers a sleep
- var/flags = 0 //see MC.dm in __DEFINES Most flags must be set on world start to take full effect. (You can also restart the mc to force them to process again)
+ /// Name of the subsystem - you must change this
+ name = "fire coderbus"
- var/initialized = FALSE //set to TRUE after it has been initialized, will obviously never be set if the subsystem doesn't initialize
+ /// Order of initialization. Higher numbers are initialized first, lower numbers later. Use or create defines such as [INIT_ORDER_DEFAULT] so we can see the order in one file.
+ var/init_order = INIT_ORDER_DEFAULT
- //set to 0 to prevent fire() calls, mostly for admin use or subsystems that may be resumed later
- // use the SS_NO_FIRE flag instead for systems that never fire to keep it from even being added to the list
+ /// Time to wait (in deciseconds) between each call to fire(). Must be a positive integer.
+ var/wait = 20
+
+ /// Priority Weight: When mutiple subsystems need to run in the same tick, higher priority subsystems will be given a higher share of the tick before MC_TICK_CHECK triggers a sleep, higher priority subsystems also run before lower priority subsystems
+ var/priority = FIRE_PRIORITY_DEFAULT
+
+ /// [Subsystem Flags][SS_NO_INIT] to control binary behavior. Flags must be set at compile time or before preinit finishes to take full effect. (You can also restart the mc to force them to process again)
+ var/flags = 0
+
+ /// This var is set to TRUE after the subsystem has been initialized.
+ var/initialized = FALSE
+
+ /// Set to 0 to prevent fire() calls, mostly for admin use or subsystems that may be resumed later
+ /// use the [SS_NO_FIRE] flag instead for systems that never fire to keep it from even being added to list that is checked every tick
var/can_fire = TRUE
- // Bookkeeping variables; probably shouldn't mess with these.
- var/last_fire = 0 //last world.time we called fire()
- var/next_fire = 0 //scheduled world.time for next fire()
- var/cost = 0 //average time to execute
- var/tick_usage = 0 //average tick usage
- var/tick_overrun = 0 //average tick overrun
- var/state = SS_IDLE //tracks the current state of the ss, running, paused, etc.
- var/paused_ticks = 0 //ticks this ss is taking to run right now.
- var/paused_tick_usage //total tick_usage of all of our runs while pausing this run
- var/ticks = 1 //how many ticks does this ss take to run on avg.
- var/times_fired = 0 //number of times we have called fire()
- var/queued_time = 0 //time we entered the queue, (for timing and priority reasons)
- var/queued_priority //we keep a running total to make the math easier, if priority changes mid-fire that would break our running total, so we store it here
- //linked list stuff for the queue
- var/datum/controller/subsystem/queue_next
- var/datum/controller/subsystem/queue_prev
-
+ ///Bitmap of what game states can this subsystem fire at. See [RUNLEVELS_DEFAULT] for more details.
var/runlevels = RUNLEVELS_DEFAULT //points of the game at which the SS can fire
- var/static/list/failure_strikes //How many times we suspect a subsystem type has crashed the MC, 3 strikes and you're out!
+ /*
+ * The following variables are managed by the MC and should not be modified directly.
+ */
+
+ /// Last world.time the subsystem completed a run (as in wasn't paused by [MC_TICK_CHECK])
+ var/last_fire = 0
+
+ /// Scheduled world.time for next fire()
+ var/next_fire = 0
+
+ /// Running average of the amount of milliseconds it takes the subsystem to complete a run (including all resumes but not the time spent paused)
+ var/cost = 0
+
+ /// Running average of the amount of tick usage in percents of a tick it takes the subsystem to complete a run
+ var/tick_usage = 0
+
+ /// Running average of the amount of tick usage (in percents of a game tick) the subsystem has spent past its allocated time without pausing
+ var/tick_overrun = 0
+
+ /// Tracks the current execution state of the subsystem. Used to handle subsystems that sleep in fire so the mc doesn't run them again while they are sleeping
+ var/state = SS_IDLE
+
+ /// Tracks how many fires the subsystem has consecutively paused on in the current run
+ var/paused_ticks = 0
+
+ /// Tracks how much of a tick the subsystem has consumed in the current run
+ var/paused_tick_usage
+
+ /// Tracks how many fires the subsystem takes to complete a run on average.
+ var/ticks = 1
+
+ /// Tracks the amount of completed runs for the subsystem
+ var/times_fired = 0
+
+ /// Time the subsystem entered the queue, (for timing and priority reasons)
+ var/queued_time = 0
+
+ /// Priority at the time the subsystem entered the queue. Needed to avoid changes in priority (by admins and the like) from breaking things.
+ var/queued_priority
+
+ /// How many times we suspect a subsystem type has crashed the MC, 3 strikes and you're out!
+ var/static/list/failure_strikes
+
+ /// Next subsystem in the queue of subsystems to run this tick
+ var/datum/controller/subsystem/queue_next
+ /// Previous subsystem in the queue of subsystems to run this tick
+ var/datum/controller/subsystem/queue_prev
+
+ //Do not blindly add vars here to the bottom, put it where it goes above
+ //If your var only has two values, put it in as a flag.
+
//Do not override
///datum/controller/subsystem/New()
@@ -46,6 +98,7 @@
//This is used so the mc knows when the subsystem sleeps. do not override.
/datum/controller/subsystem/proc/ignite(resumed = 0)
+ SHOULD_NOT_OVERRIDE(TRUE)
set waitfor = 0
. = SS_SLEEPING
fire(resumed)
@@ -87,7 +140,7 @@
queue_node_flags = queue_node.flags
if (queue_node_flags & SS_TICKER)
- if (!(SS_flags & SS_TICKER))
+ if ((SS_flags & (SS_TICKER|SS_BACKGROUND)) != SS_TICKER)
continue
if (queue_node_priority < SS_priority)
break
@@ -155,6 +208,9 @@
if(SS_SLEEPING)
state = SS_PAUSING
+/// Called after the config has been loaded or reloaded.
+/datum/controller/subsystem/proc/OnConfigLoad()
+
/datum/controller/subsystem/proc/subsystem_log(msg)
return log_subsystem(name, msg)
@@ -164,7 +220,7 @@
var/time = (REALTIMEOFDAY - start_timeofday) / 10
var/msg = "Initialized [name] subsystem within [time] second[time == 1 ? "" : "s"]!"
to_chat(world, "[msg]")
- log_subsystem("INIT", msg)
+ log_world(msg)
return time
//hook for printing stats to the "MC" statuspanel for admins to see performance and related stats etc.
diff --git a/code/game/machinery/computer/card.dm b/code/game/machinery/computer/card.dm
index 74cceacd12..46d419a8fe 100644
--- a/code/game/machinery/computer/card.dm
+++ b/code/game/machinery/computer/card.dm
@@ -353,7 +353,6 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0)
dat = list("", header.Join(), body, "
")
var/datum/browser/popup = new(user, "id_com", src.name, 900, 620)
popup.set_content(dat.Join())
- popup.set_title_image(user.browse_rsc_icon(src.icon, src.icon_state))
popup.open()
/obj/machinery/computer/card/Topic(href, href_list)
diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm
index 3fc69a62f2..3ef887b156 100755
--- a/code/game/machinery/computer/communications.dm
+++ b/code/game/machinery/computer/communications.dm
@@ -1,3 +1,15 @@
+#define STATE_DEFAULT 1
+#define STATE_CALLSHUTTLE 2
+#define STATE_CANCELSHUTTLE 3
+#define STATE_MESSAGELIST 4
+#define STATE_VIEWMESSAGE 5
+#define STATE_DELMESSAGE 6
+#define STATE_STATUSDISPLAY 7
+#define STATE_ALERT_LEVEL 8
+#define STATE_CONFIRM_LEVEL 9
+#define STATE_TOGGLE_EMERGENCY 10
+#define STATE_PURCHASE 11
+
// The communications computer
/obj/machinery/computer/communications
name = "communications console"
@@ -6,6 +18,7 @@
icon_keyboard = "tech_key"
req_access = list(ACCESS_HEADS)
circuit = /obj/item/circuitboard/computer/communications
+ light_color = LIGHT_COLOR_BLUE
var/auth_id = "Unknown" //Who is currently logged in?
var/list/datum/comm_message/messages = list()
var/datum/comm_message/currmsg
@@ -16,22 +29,10 @@
var/ai_message_cooldown = 0
var/tmp_alertlevel = 0
var/static/security_level_cd // used to stop mass spam.
- var/const/STATE_DEFAULT = 1
- var/const/STATE_CALLSHUTTLE = 2
- var/const/STATE_CANCELSHUTTLE = 3
- var/const/STATE_MESSAGELIST = 4
- var/const/STATE_VIEWMESSAGE = 5
- var/const/STATE_DELMESSAGE = 6
- var/const/STATE_STATUSDISPLAY = 7
- var/const/STATE_ALERT_LEVEL = 8
- var/const/STATE_CONFIRM_LEVEL = 9
- var/const/STATE_TOGGLE_EMERGENCY = 10
- var/const/STATE_PURCHASE = 11
var/stat_msg1
var/stat_msg2
- light_color = LIGHT_COLOR_BLUE
/obj/machinery/computer/communications/proc/checkCCcooldown()
var/obj/item/circuitboard/computer/communications/CM = circuit
@@ -46,7 +47,7 @@
/obj/machinery/computer/communications/Topic(href, href_list)
if(..())
return
- if(!usr.canUseTopic(src))
+ if(!usr.canUseTopic(src, !issilicon(usr)))
return
if(!is_station_level(z) && !is_reserved_level(z)) //Can only use in transit and on SS13
to_chat(usr, "Unable to establish a connection: \black You're too far away from the station!")
@@ -132,15 +133,20 @@
if("crossserver")
if(authenticated==2)
+ var/dest = href_list["cross_dest"]
if(!checkCCcooldown())
to_chat(usr, "Arrays recycling. Please stand by.")
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
return
- var/input = stripped_multiline_input(usr, "Please choose a message to transmit to allied stations. Please be aware that this process is very expensive, and abuse will lead to... termination.", "Send a message to an allied station.", "")
+ var/warning = dest == "all" ? "Please choose a message to transmit to allied stations." : "Please choose a message to transmit to [dest] sector station."
+ var/input = stripped_multiline_input(usr, "[warning] Please be aware that this process is very expensive, and abuse will lead to... termination.", "Send a message to an allied station.", "")
if(!input || !(usr in view(1,src)) || !checkCCcooldown())
return
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- send2otherserver("[station_name()]", input,"Comms_Console")
+ if(dest == "all")
+ send2otherserver("[station_name()]", input,"Comms_Console")
+ else
+ send2otherserver("[station_name()]", input,"Comms_Console", list(dest))
minor_announce(input, title = "Outgoing message to allied station")
usr.log_talk(input, LOG_SAY, tag="message to the other server")
message_admins("[ADMIN_LOOKUPFLW(usr)] has sent a message to the other server.")
@@ -156,12 +162,12 @@
var/datum/map_template/shuttle/S = locate(href_list["chosen_shuttle"]) in shuttles
if(S && istype(S))
if(SSshuttle.emergency.mode != SHUTTLE_RECALL && SSshuttle.emergency.mode != SHUTTLE_IDLE)
- to_chat(usr, "It's a bit late to buy a new shuttle, don't you think?")
+ to_chat(usr, "It's a bit late to buy a new shuttle, don't you think?")
return
if(SSshuttle.shuttle_purchased)
- to_chat(usr, "A replacement shuttle has already been purchased.")
+ to_chat(usr, "A replacement shuttle has already been purchased.")
else if(!S.prerequisites_met())
- to_chat(usr, "You have not met the requirements for purchasing this shuttle.")
+ to_chat(usr, "You have not met the requirements for purchasing this shuttle.")
else
var/points_to_check
var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_CAR)
@@ -183,7 +189,7 @@
if("callshuttle")
state = STATE_DEFAULT
- if(authenticated)
+ if(authenticated && SSshuttle.canEvac(usr))
state = STATE_CALLSHUTTLE
if("callshuttle2")
if(authenticated)
@@ -284,12 +290,12 @@
if("MessageCentCom")
if(authenticated)
if(!checkCCcooldown())
- to_chat(usr, "Arrays recycling. Please stand by.")
+ to_chat(usr, "Arrays recycling. Please stand by.")
return
var/input = stripped_input(usr, "Please choose a message to transmit to CentCom via quantum entanglement. Please be aware that this process is very expensive, and abuse will lead to... termination. Transmission does not guarantee a response.", "Send a message to CentCom.", "")
if(!input || !(usr in view(1,src)) || !checkCCcooldown())
return
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, 0)
+ playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
CentCom_announce(input, usr)
to_chat(usr, "Message transmitted to Central Command.")
for(var/client/X in GLOB.admins)
@@ -302,7 +308,7 @@
// OMG SYNDICATE ...LETTERHEAD
if("MessageSyndicate")
- if((authenticated==2) && (obj_flags & EMAGGED))
+ if((authenticated) && (obj_flags & EMAGGED))
if(!checkCCcooldown())
to_chat(usr, "Arrays recycling. Please stand by.")
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
@@ -332,7 +338,7 @@
if(!checkCCcooldown())
to_chat(usr, "Arrays recycling. Please stand by.")
return
- var/input = stripped_input(usr, "Please enter the reason for requesting the nuclear self-destruct codes. Misuse of the nuclear request system will not be tolerated under any circumstances. Transmission does not guarantee a response.", "Self Destruct Code Request.","")
+ var/input = stripped_input(usr, "Please enter the reason for requesting the nuclear self-destruct codes. Misuse of the nuclear request system will not be tolerated under any circumstances. Transmission does not guarantee a response.", "Self-Destruct Code Request.","")
if(!input || !(usr in view(1,src)) || !checkCCcooldown())
return
Nuke_request(input, usr)
@@ -347,7 +353,9 @@
aicurrmsg = null
aistate = STATE_DEFAULT
if("ai-callshuttle")
- aistate = STATE_CALLSHUTTLE
+ aistate = STATE_DEFAULT
+ if(SSshuttle.canEvac(usr))
+ aistate = STATE_CALLSHUTTLE
if("ai-callshuttle2")
SSshuttle.requestEvac(usr, href_list["call"])
aistate = STATE_DEFAULT
@@ -461,7 +469,7 @@
var/datum/browser/popup = new(user, "communications", "Communications Console", 400, 500)
- if(issilicon(user) || (hasSiliconAccessInArea(user) && !in_range(user,src)))
+ if(issilicon(user))
var/dat2 = interact_ai(user) // give the AI a different interact proc to limit its access
if(dat2)
dat += dat2
@@ -492,9 +500,15 @@
if (authenticated==2)
dat += "
Captain Functions"
dat += "
\[ Make a Captain's Announcement \]"
- var/cross_servers_count = length(CONFIG_GET(keyed_list/cross_server))
- if(cross_servers_count)
- dat += "
\[ Send a message to [cross_servers_count == 1 ? "an " : ""]allied station[cross_servers_count > 1 ? "s" : ""] \]"
+ var/list/cross_servers = CONFIG_GET(keyed_list/cross_server)
+ var/our_id = CONFIG_GET(string/cross_comms_name)
+ if(cross_servers.len)
+ for(var/server in cross_servers)
+ if(server == our_id)
+ continue
+ dat += "
\[ Send a message to station in [server] sector. \]"
+ if(cross_servers.len > 2)
+ dat += "
\[ Send a message to all allied stations \]"
if(SSmapping.config.allow_custom_shuttles)
dat += "
\[ Purchase Shuttle \]"
dat += "
\[ Change Alert Level \]"
@@ -720,8 +734,13 @@
to_chat(user, "Intercomms recharging. Please stand by.")
return
var/input = stripped_input(user, "Please choose a message to announce to the station crew.", "What?")
- if(!input || !user.canUseTopic(src))
+ if(!input || !user.canUseTopic(src, !issilicon(usr)))
return
+ if(!(user.can_speak())) //No more cheating, mime/random mute guy!
+ input = "..."
+ to_chat(user, "You find yourself unable to speak.")
+ else
+ input = user.treat_message(input) //Adds slurs and so on. Someone should make this use languages too.
SScommunications.make_announcement(user, is_silicon, input)
deadchat_broadcast("[user.real_name] made an priority announcement from [get_area_name(usr, TRUE)].", user)
@@ -770,3 +789,15 @@
content = new_content
if(new_possible_answers)
possible_answers = new_possible_answers
+
+#undef STATE_DEFAULT
+#undef STATE_CALLSHUTTLE
+#undef STATE_CANCELSHUTTLE
+#undef STATE_MESSAGELIST
+#undef STATE_VIEWMESSAGE
+#undef STATE_DELMESSAGE
+#undef STATE_STATUSDISPLAY
+#undef STATE_ALERT_LEVEL
+#undef STATE_CONFIRM_LEVEL
+#undef STATE_TOGGLE_EMERGENCY
+#undef STATE_PURCHASE
diff --git a/code/game/objects/items/RCD.dm b/code/game/objects/items/RCD.dm
index 2fbb45173b..b4eca10ed0 100644
--- a/code/game/objects/items/RCD.dm
+++ b/code/game/objects/items/RCD.dm
@@ -234,7 +234,6 @@ RLD
var/datum/browser/popup = new(user, "rcd_access", "Access Control", 900, 500, src)
popup.set_content(t1)
- popup.set_title_image(user.browse_rsc_icon(icon, icon_state))
popup.open()
/obj/item/construction/rcd/Topic(href, href_list)
diff --git a/code/modules/holiday/halloween/jacqueen.dm b/code/modules/holiday/halloween/jacqueen.dm
index 7c679027f7..573b04a296 100644
--- a/code/modules/holiday/halloween/jacqueen.dm
+++ b/code/modules/holiday/halloween/jacqueen.dm
@@ -334,8 +334,7 @@
to_chat(C, " You feel an overwhelming desire to [message]")
if(2)
visible_message("[src] waves their arms around, \"If only you had a better upbringing, your ears are now full of my singing!\"")
- var/client/C2 = C.client
- C.tgui_panel?.play_music("https://puu.sh/ExBbv.mp4")
+ C.client.tgui_panel?.play_music("https://puu.sh/ExBbv.mp4")
if(3)
visible_message("[src] waves their arms around, \"You're cute little bumpkin, On your head is a pumpkin!\"")
if(C.head)
diff --git a/code/modules/instruments/songs/editor.dm b/code/modules/instruments/songs/editor.dm
index 8c5171667a..e385eed142 100644
--- a/code/modules/instruments/songs/editor.dm
+++ b/code/modules/instruments/songs/editor.dm
@@ -82,7 +82,6 @@
var/datum/browser/popup = new(user, "instrument", parent?.name || "instrument", 700, 500)
popup.set_content(dat.Join(""))
- popup.set_title_image(user.browse_rsc_icon(parent.icon, parent.icon_state))
popup.open()
/datum/song/proc/ParseSong(text)
diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm
index bcb658eab8..df38b1a908 100644
--- a/code/modules/mob/living/carbon/human/human_movement.dm
+++ b/code/modules/mob/living/carbon/human/human_movement.dm
@@ -12,10 +12,10 @@
/mob/living/carbon/human/movement_delay()
. = ..()
if(CHECK_MOBILITY(src, MOBILITY_STAND) && m_intent == MOVE_INTENT_RUN && (combat_flags & COMBAT_FLAG_SPRINT_ACTIVE))
- var/static/datum/config_entry/number/movedelay/sprint_speed_increase/SSI
+ var/static/SSI
if(!SSI)
- SSI = CONFIG_GET_ENTRY(number/movedelay/sprint_speed_increase)
- . -= SSI.config_entry_value
+ SSI = CONFIG_GET(number/movedelay/sprint_speed_increase)
+ . -= SSI //but WHY
if (m_intent == MOVE_INTENT_WALK && HAS_TRAIT(src, TRAIT_SPEEDY_STEP))
. -= 1.5
diff --git a/code/modules/paperwork/photocopier.dm b/code/modules/paperwork/photocopier.dm
index 0b1f3bb01d..115a5b944d 100644
--- a/code/modules/paperwork/photocopier.dm
+++ b/code/modules/paperwork/photocopier.dm
@@ -1,12 +1,20 @@
-/* Photocopiers!
- * Contains:
- * Photocopier
- * Toner Cartridge
- */
-/*
- * Photocopier
- */
+/// For use with the `color_mode` var. Photos will be printed in greyscale while the var has this value.
+#define PHOTO_GREYSCALE "Greyscale"
+/// For use with the `color_mode` var. Photos will be printed in full color while the var has this value.
+#define PHOTO_COLOR "Color"
+
+/// How much toner is used for making a copy of a paper.
+#define PAPER_TONER_USE 0.125
+/// How much toner is used for making a copy of a photo.
+#define PHOTO_TONER_USE 0.625
+/// How much toner is used for making a copy of a document.
+#define DOCUMENT_TONER_USE 0.75
+/// How much toner is used for making a copy of an ass.
+#define ASS_TONER_USE 0.625
+/// The maximum amount of copies you can make with one press of the copy button.
+#define MAX_COPIES_AT_ONCE 10
+
/obj/machinery/photocopier
name = "photocopier"
desc = "Used to copy important documents and anatomy studies."
@@ -19,183 +27,314 @@
power_channel = EQUIP
max_integrity = 300
integrity_failure = 0.33
- var/obj/item/paper/copy = null //what's in the copier!
- var/obj/item/photo/photocopy = null
- var/obj/item/documents/doccopy = null
- var/copies = 1 //how many copies to print!
- var/toner = 40 //how much toner is left! woooooo~
- var/maxcopies = 10 //how many copies can be copied at once- idea shamelessly stolen from bs12's copier!
- var/greytoggle = "Greyscale"
- var/mob/living/ass //i can't believe i didn't write a stupid-ass comment about this var when i first coded asscopy.
+ /// A reference to an `/obj/item/paper` inside the copier, if one is inserted. Otherwise null.
+ var/obj/item/paper/paper_copy
+ /// A reference to an `/obj/item/photo` inside the copier, if one is inserted. Otherwise null.
+ var/obj/item/photo/photo_copy
+ /// A reference to an `/obj/item/documents` inside the copier, if one is inserted. Otherwise null.
+ var/obj/item/documents/document_copy
+ /// A reference to a mob on top of the photocopier trying to copy their ass. Null if there is no mob.
+ var/mob/living/ass
+ /// A reference to the toner cartridge that's inserted into the copier. Null if there is no cartridge.
+ var/obj/item/toner/toner_cartridge
+ /// How many copies will be printed with one click of the "copy" button.
+ var/num_copies = 1
+ /// Used with photos. Determines if the copied photo will be in greyscale or color.
+ var/color_mode = PHOTO_COLOR
+ /// Indicates whether the printer is currently busy copying or not.
var/busy = FALSE
-/obj/machinery/photocopier/ui_interact(mob/user)
+/obj/machinery/photocopier/Initialize()
. = ..()
- var/list/dat = list("Photocopier
")
- if(copy || photocopy || doccopy || (ass && (ass.loc == src.loc)))
- dat += "Remove Paper
"
- if(toner)
- dat += "Copy
"
- dat += "Printing: [copies] copies."
- dat += "- "
- dat += "+
"
- if(photocopy)
- dat += "Printing in [greytoggle]
"
- else if(toner)
- dat += "Please insert paper to copy.
"
- if(isAI(user))
- dat += "Print photo from database
"
- dat += "Current toner level: [toner]"
- if(!toner)
- dat +="
Please insert a new toner cartridge!"
- user << browse(dat.Join(""), "window=copier")
- onclose(user, "copier")
+ //AddComponent(/datum/component/payment, 5, SSeconomy.get_dep_account(ACCOUNT_CIV), PAYMENT_CLINICAL)
+ toner_cartridge = new(src)
-/obj/machinery/photocopier/Topic(href, href_list)
+/obj/machinery/photocopier/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Photocopier")
+ ui.open()
+
+/obj/machinery/photocopier/ui_data(mob/user)
+ var/list/data = list()
+ data["has_item"] = !copier_empty()
+ data["num_copies"] = num_copies
+
+ if(photo_copy)
+ data["is_photo"] = TRUE
+ data["color_mode"] = color_mode
+
+ if(isAI(user))
+ data["isAI"] = TRUE
+ data["can_AI_print"] = toner_cartridge ? toner_cartridge.charges >= PHOTO_TONER_USE : FALSE
+ else
+ data["isAI"] = FALSE
+
+ if(toner_cartridge)
+ data["has_toner"] = TRUE
+ data["current_toner"] = toner_cartridge.charges
+ data["max_toner"] = toner_cartridge.max_charges
+ data["has_enough_toner"] = has_enough_toner()
+ else
+ data["has_toner"] = FALSE
+ data["has_enough_toner"] = FALSE
+
+ return data
+
+/obj/machinery/photocopier/ui_act(action, list/params)
if(..())
return
- if(href_list["copy"])
- if(copy)
- for(var/i = 0, i < copies, i++)
- if(toner > 0 && !busy && copy)
- var/copy_as_paper = 1
- if(istype(copy, /obj/item/paper/contract/employment))
- var/obj/item/paper/contract/employment/E = copy
- var/obj/item/paper/contract/employment/C = new /obj/item/paper/contract/employment (loc, E.target.current)
- if(C)
- copy_as_paper = 0
- if(copy_as_paper)
- var/obj/item/paper/c = new /obj/item/paper (loc)
- if(length(copy.info) > 0) //Only print and add content if the copied doc has words on it
- 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.name = copy.name
- c.update_icon()
- c.stamps = copy.stamps
- if(copy.stamped)
- c.stamped = copy.stamped.Copy()
- c.copy_overlays(copy, TRUE)
- toner--
- busy = TRUE
- addtimer(CALLBACK(src, .proc/reset_busy), 1.5 SECONDS)
- else
- break
- updateUsrDialog()
- else if(photocopy)
- for(var/i = 0, i < copies, i++)
- if(toner >= 5 && !busy && photocopy) //Was set to = 0, but if there was say 3 toner left and this ran, you would get -2 which would be weird for ink
- new /obj/item/photo (loc, photocopy.picture.Copy(greytoggle == "Greyscale"? TRUE : FALSE))
- busy = TRUE
- addtimer(CALLBACK(src, .proc/reset_busy), 1.5 SECONDS)
- else
- break
- else if(doccopy)
- for(var/i = 0, i < copies, i++)
- if(toner > 5 && !busy && doccopy)
- new /obj/item/documents/photocopy(loc, doccopy)
- toner-= 6 // the sprite shows 6 papers, yes I checked
- busy = TRUE
- addtimer(CALLBACK(src, .proc/reset_busy), 1.5 SECONDS)
- else
- break
- updateUsrDialog()
- else if(ass) //ASS COPY. By Miauw
- for(var/i = 0, i < copies, i++)
- var/icon/temp_img
- if(ishuman(ass) && (ass.get_item_by_slot(ITEM_SLOT_ICLOTHING) || ass.get_item_by_slot(ITEM_SLOT_OCLOTHING)))
- to_chat(usr, "You feel kind of silly, copying [ass == usr ? "your" : ass][ass == usr ? "" : "\'s"] ass with [ass == usr ? "your" : "[ass.p_their()]"] clothes on." )
- break
- else if(toner >= 5 && !busy && check_ass()) //You have to be sitting on the copier and either be a xeno or a human without clothes on.
- if(isalienadult(ass) || istype(ass, /mob/living/simple_animal/hostile/alien)) //Xenos have their own asses, thanks to Pybro.
- temp_img = icon('icons/ass/assalien.png')
- else if(ishuman(ass)) //Suit checks are in check_ass
- temp_img = icon(ass.gender == FEMALE ? 'icons/ass/assfemale.png' : 'icons/ass/assmale.png')
- else if(isdrone(ass)) //Drones are hot
- temp_img = icon('icons/ass/assdrone.png')
- else
- break
- busy = TRUE
- sleep(15)
- var/obj/item/photo/p = new /obj/item/photo (loc)
- var/datum/picture/toEmbed = new(name = "[ass]'s Ass", desc = "You see [ass]'s ass on the photo.", image = temp_img)
- p.pixel_x = rand(-10, 10)
- p.pixel_y = rand(-10, 10)
- toEmbed.psize_x = 128
- toEmbed.psize_y = 128
- p.set_picture(toEmbed, TRUE, TRUE)
- toner -= 5
- busy = FALSE
- else
- break
- updateUsrDialog()
- else if(href_list["remove"])
- if(copy)
- remove_photocopy(copy, usr)
- copy = null
- else if(photocopy)
- remove_photocopy(photocopy, usr)
- photocopy = null
- else if(doccopy)
- remove_photocopy(doccopy, usr)
- doccopy = null
- else if(check_ass())
- to_chat(ass, "You feel a slight pressure on your ass.")
- updateUsrDialog()
- else if(href_list["min"])
- if(copies > 1)
- copies--
- updateUsrDialog()
- else if(href_list["add"])
- if(copies < maxcopies)
- copies++
- updateUsrDialog()
- else if(href_list["aipic"])
- if(!isAI(usr))
- return
- if(toner >= 5 && !busy)
+
+ switch(action)
+ // Copying paper, photos, documents and asses.
+ if("make_copy")
+ if(busy)
+ to_chat(usr, "[src] is currently busy copying something. Please wait until it is finished.")
+ return FALSE
+ if(paper_copy)
+ if(!length(paper_copy.info))
+ to_chat(usr, "An error message flashes across [src]'s screen: \"The supplied paper is blank. Aborting.\"")
+ return FALSE
+ // Basic paper
+ if(istype(paper_copy, /obj/item/paper))
+ do_copy_loop(CALLBACK(src, .proc/make_paper_copy), usr)
+ return TRUE
+ // Devil contract paper.
+ if(istype(paper_copy, /obj/item/paper/contract/employment))
+ do_copy_loop(CALLBACK(src, .proc/make_devil_paper_copy), usr)
+ return TRUE
+ // Copying photo.
+ if(photo_copy)
+ do_copy_loop(CALLBACK(src, .proc/make_photo_copy), usr)
+ return TRUE
+ // Copying Documents.
+ if(document_copy)
+ do_copy_loop(CALLBACK(src, .proc/make_document_copy), usr)
+ return TRUE
+ // ASS COPY. By Miauw
+ if(ass)
+ do_copy_loop(CALLBACK(src, .proc/make_ass_copy), usr)
+ return TRUE
+
+ // Remove the paper/photo/document from the photocopier.
+ if("remove")
+ if(paper_copy)
+ remove_photocopy(paper_copy, usr)
+ paper_copy = null
+ else if(photo_copy)
+ remove_photocopy(photo_copy, usr)
+ photo_copy = null
+ else if(document_copy)
+ remove_photocopy(document_copy, usr)
+ document_copy = null
+ else if(check_ass())
+ to_chat(ass, "You feel a slight pressure on your ass.")
+ return TRUE
+
+ // AI printing photos from their saved images.
+ if("ai_photo")
+ if(busy)
+ to_chat(usr, "[src] is currently busy copying something. Please wait until it is finished.")
+ return FALSE
var/mob/living/silicon/ai/tempAI = usr
- if(tempAI.aicamera.stored.len == 0)
- to_chat(usr, "No images saved")
+ if(!length(tempAI.aicamera.stored))
+ to_chat(usr, "No images saved.")
return
var/datum/picture/selection = tempAI.aicamera.selectpicture(usr)
- var/obj/item/photo/photo = new(loc, selection)
- photo.pixel_x = rand(-10, 10)
- photo.pixel_y = rand(-10, 10)
- toner -= 5 //AI prints color pictures only, thus they can do it more efficiently
- busy = TRUE
- addtimer(CALLBACK(src, .proc/reset_busy), 1.5 SECONDS)
- updateUsrDialog()
- else if(href_list["colortoggle"])
- if(greytoggle == "Greyscale")
- greytoggle = "Color"
- else
- greytoggle = "Greyscale"
- updateUsrDialog()
+ var/obj/item/photo/photo = new(loc, selection) // AI prints color photos only.
+ give_pixel_offset(photo)
+ toner_cartridge.charges -= PHOTO_TONER_USE
+ return TRUE
+ // Switch between greyscale and color photos
+ if("color_mode")
+ if(params["mode"] in list(PHOTO_GREYSCALE, PHOTO_COLOR))
+ color_mode = params["mode"]
+ return TRUE
+
+ // Remove the toner cartridge from the copier.
+ if("remove_toner")
+ if(issilicon(usr) || (ishuman(usr) && !usr.put_in_hands(toner_cartridge)))
+ toner_cartridge.forceMove(drop_location())
+ toner_cartridge = null
+ return TRUE
+
+ // Set the number of copies to be printed with 1 click of the "copy" button.
+ if("set_copies")
+ num_copies = clamp(text2num(params["num_copies"]), 1, MAX_COPIES_AT_ONCE)
+ return TRUE
+
+/**
+ * Determines if the photocopier has enough toner to create `num_copies` amount of copies of the currently inserted item.
+ */
+/obj/machinery/photocopier/proc/has_enough_toner()
+ if(paper_copy)
+ return toner_cartridge.charges >= (PAPER_TONER_USE * num_copies)
+ else if(document_copy)
+ return toner_cartridge.charges >= (DOCUMENT_TONER_USE * num_copies)
+ else if(photo_copy)
+ return toner_cartridge.charges >= (PHOTO_TONER_USE * num_copies)
+ else if(ass)
+ return toner_cartridge.charges >= (ASS_TONER_USE * num_copies)
+ return FALSE
+
+/**
+ * Will invoke the passed in `copy_cb` callback in 1 second intervals, and charge the user 5 credits for each copy made.
+ *
+ * Arguments:
+ * * copy_cb - a callback for which proc to call. Should only be one of the `make_x_copy()` procs, such as `make_paper_copy()`.
+ * * user - the mob who clicked copy.
+ */
+/obj/machinery/photocopier/proc/do_copy_loop(datum/callback/copy_cb, mob/user)
+ busy = TRUE
+ var/i
+ for(i in 1 to num_copies)
+ //if(attempt_charge(src, user) & COMPONENT_OBJ_CANCEL_CHARGE)
+ // break
+ addtimer(copy_cb, i SECONDS)
+ addtimer(CALLBACK(src, .proc/reset_busy), i SECONDS)
+
+/**
+ * Sets busy to `FALSE`. Created as a proc so it can be used in callbacks.
+ */
/obj/machinery/photocopier/proc/reset_busy()
busy = FALSE
- updateUsrDialog()
-/obj/machinery/photocopier/proc/do_insertion(obj/item/O, mob/user)
- O.forceMove(src)
- to_chat(user, "You insert [O] into [src].")
+/**
+ * Gives items a random x and y pixel offset, between -10 and 10 for each.
+ *
+ * This is done that when someone prints multiple papers, we dont have them all appear to be stacked in the same exact location.
+ *
+ * Arguments:
+ * * copied_item - The paper, document, or photo that was just spawned on top of the printer.
+ */
+/obj/machinery/photocopier/proc/give_pixel_offset(obj/item/copied_item)
+ copied_item.pixel_x = rand(-10, 10)
+ copied_item.pixel_y = rand(-10, 10)
+
+/**
+ * Handles the copying of devil contract paper. Transfers all the text, stamps and so on from the old paper, to the copy.
+ *
+ * Checks first if `paper_copy` exists. Since this proc is called from a timer, it's possible that it was removed.
+ * Does not check if it has enough toner because devil contracts cost no toner to print.
+ */
+/obj/machinery/photocopier/proc/make_devil_paper_copy()
+ if(!paper_copy)
+ return
+ var/obj/item/paper/contract/employment/E = paper_copy
+ var/obj/item/paper/contract/employment/C = new(loc, E.target.current)
+ give_pixel_offset(C)
+
+/**
+ * Handles the copying of paper. Transfers all the text, stamps and so on from the old paper, to the copy.
+ *
+ * Checks first if `paper_copy` exists. Since this proc is called from a timer, it's possible that it was removed.
+ */
+/obj/machinery/photocopier/proc/make_paper_copy()
+ if(!paper_copy)
+ return
+ var/obj/item/paper/copied_paper = new(loc)
+ give_pixel_offset(copied_paper)
+ if(toner_cartridge.charges > 10) // Lots of toner, make it dark.
+ copied_paper.info = ""
+ else // No toner? shitty copies for you!
+ copied_paper.info = ""
+
+ var/copied_info = paper_copy.info
+ copied_info = replacetext(copied_info, ""
+ copied_paper.name = paper_copy.name
+ copied_paper.update_icon()
+ copied_paper.stamps = paper_copy.stamps
+ if(paper_copy.stamped)
+ copied_paper.stamped = paper_copy.stamped.Copy()
+ copied_paper.copy_overlays(paper_copy, TRUE)
+ toner_cartridge.charges -= PAPER_TONER_USE
+
+/**
+ * Handles the copying of photos, which can be printed in either color or greyscale.
+ *
+ * Checks first if `photo_copy` exists. Since this proc is called from a timer, it's possible that it was removed.
+ */
+/obj/machinery/photocopier/proc/make_photo_copy()
+ if(!photo_copy)
+ return
+ var/obj/item/photo/copied_pic = new(loc, photo_copy.picture.Copy(color_mode == PHOTO_GREYSCALE ? TRUE : FALSE))
+ give_pixel_offset(copied_pic)
+ toner_cartridge.charges -= PHOTO_TONER_USE
+
+/**
+ * Handles the copying of documents.
+ *
+ * Checks first if `document_copy` exists. Since this proc is called from a timer, it's possible that it was removed.
+ */
+/obj/machinery/photocopier/proc/make_document_copy()
+ if(!document_copy)
+ return
+ var/obj/item/documents/photocopy/copied_doc = new(loc, document_copy)
+ give_pixel_offset(copied_doc)
+ toner_cartridge.charges -= DOCUMENT_TONER_USE
+
+/**
+ * Handles the copying of an ass photo.
+ *
+ * Calls `check_ass()` first to make sure that `ass` exists, among other conditions. Since this proc is called from a timer, it's possible that it was removed.
+ * Additionally checks that the mob has their clothes off.
+ */
+/obj/machinery/photocopier/proc/make_ass_copy()
+ if(!check_ass())
+ return
+ if(ishuman(ass) && (ass.get_item_by_slot(ITEM_SLOT_ICLOTHING) || ass.get_item_by_slot(ITEM_SLOT_OCLOTHING)))
+ to_chat(usr, "You feel kind of silly, copying [ass == usr ? "your" : ass][ass == usr ? "" : "\'s"] ass with [ass == usr ? "your" : "[ass.p_their()]"] clothes on." )
+ return
+
+ var/icon/temp_img
+ if(isalienadult(ass) || istype(ass, /mob/living/simple_animal/hostile/alien)) //Xenos have their own asses, thanks to Pybro.
+ temp_img = icon('icons/ass/assalien.png')
+ else if(ishuman(ass)) //Suit checks are in check_ass
+ temp_img = icon(ass.gender == FEMALE ? 'icons/ass/assfemale.png' : 'icons/ass/assmale.png')
+ else if(isdrone(ass)) //Drones are hot
+ temp_img = icon('icons/ass/assdrone.png')
+
+ var/obj/item/photo/copied_ass = new /obj/item/photo(loc)
+ var/datum/picture/toEmbed = new(name = "[ass]'s Ass", desc = "You see [ass]'s ass on the photo.", image = temp_img)
+ give_pixel_offset(copied_ass)
+ toEmbed.psize_x = 128
+ toEmbed.psize_y = 128
+ copied_ass.set_picture(toEmbed, TRUE, TRUE)
+ toner_cartridge.charges -= ASS_TONER_USE
+
+/**
+ * Inserts the item into the copier. Called in `attackby()` after a human mob clicked on the copier with a paper, photo, or document.
+ *
+ * Arugments:
+ * * object - the object that got inserted.
+ * * user - the mob that inserted the object.
+ */
+/obj/machinery/photocopier/proc/do_insertion(obj/item/object, mob/user)
+ object.forceMove(src)
+ to_chat(user, "You insert [object] into [src].")
flick("photocopier1", src)
- updateUsrDialog()
-/obj/machinery/photocopier/proc/remove_photocopy(obj/item/O, mob/user)
+/**
+ * Called when someone hits the "remove item" button on the copier UI.
+ *
+ * If the user is a silicon, it drops the object at the location of the copier. If the user is not a silicon, it tries to put the object in their hands first.
+ * Sets `busy` to `FALSE` because if the inserted item is removed, the copier should halt copying.
+ *
+ * Arguments:
+ * * object - the item we're trying to remove.
+ * * user - the user removing the item.
+ */
+/obj/machinery/photocopier/proc/remove_photocopy(obj/item/object, mob/user)
if(!issilicon(user)) //surprised this check didn't exist before, putting stuff in AI's hand is bad
- O.forceMove(user.loc)
- user.put_in_hands(O)
+ object.forceMove(user.loc)
+ user.put_in_hands(object)
else
- O.forceMove(drop_location())
- to_chat(user, "You take [O] out of [src].")
+ object.forceMove(drop_location())
+ to_chat(user, "You take [object] out of [src]. [busy ? "The [src] comes to a halt." : ""]")
/obj/machinery/photocopier/attackby(obj/item/O, mob/user, params)
if(default_unfasten_wrench(user, O))
@@ -210,7 +349,7 @@
else
if(!user.temporarilyRemoveItemFromInventory(O))
return
- copy = O
+ paper_copy = O
do_insertion(O, user)
else
to_chat(user, "There is already something in [src]!")
@@ -219,7 +358,7 @@
if(copier_empty())
if(!user.temporarilyRemoveItemFromInventory(O))
return
- photocopy = O
+ photo_copy = O
do_insertion(O, user)
else
to_chat(user, "There is already something in [src]!")
@@ -228,38 +367,35 @@
if(copier_empty())
if(!user.temporarilyRemoveItemFromInventory(O))
return
- doccopy = O
+ document_copy = O
do_insertion(O, user)
else
to_chat(user, "There is already something in [src]!")
else if(istype(O, /obj/item/toner))
- if(toner <= 0)
- if(!user.temporarilyRemoveItemFromInventory(O))
- return
- qdel(O)
- toner = 40
- to_chat(user, "You insert [O] into [src].")
- updateUsrDialog()
- else
- to_chat(user, "This cartridge is not yet ready for replacement! Use up the rest of the toner.")
+ if(toner_cartridge)
+ to_chat(user, "[src] already has a toner cartridge inserted. Remove that one first.")
+ return
+ O.forceMove(src)
+ toner_cartridge = O
+ to_chat(user, "You insert [O] into [src].")
else if(istype(O, /obj/item/areaeditor/blueprints))
- to_chat(user, "The Blueprint is too large to put into the copier. You need to find something else to record the document")
+ to_chat(user, "The Blueprint is too large to put into the copier. You need to find something else to record the document.")
else
return ..()
/obj/machinery/photocopier/obj_break(damage_flag)
. = ..()
- if(. && toner > 0)
+ if(. && toner_cartridge.charges)
new /obj/effect/decal/cleanable/oil(get_turf(src))
- toner = 0
+ toner_cartridge.charges = 0
/obj/machinery/photocopier/MouseDrop_T(mob/target, mob/user)
check_ass() //Just to make sure that you can re-drag somebody onto it after they moved off.
- if (!istype(target) || target.anchored || target.buckled || !Adjacent(target) || !user.canUseTopic(src, BE_CLOSE) || target == ass || copier_blocked())
+ if(!istype(target) || target.anchored || target.buckled || !Adjacent(target) || !user.canUseTopic(src, BE_CLOSE) || target == ass || copier_blocked())
return
- src.add_fingerprint(user)
+ add_fingerprint(user)
if(target == user)
user.visible_message("[user] starts climbing onto the photocopier!", "You start climbing onto the photocopier...")
else
@@ -277,49 +413,63 @@
target.forceMove(drop_location())
ass = target
- if(photocopy)
- photocopy.forceMove(drop_location())
- visible_message("[photocopy] is shoved out of the way by [ass]!")
- photocopy = null
+ if(photo_copy)
+ photo_copy.forceMove(drop_location())
+ visible_message("[photo_copy] is shoved out of the way by [ass]!")
+ photo_copy = null
- else if(copy)
- copy.forceMove(drop_location())
- visible_message("[copy] is shoved out of the way by [ass]!")
- copy = null
- updateUsrDialog()
+ else if(paper_copy)
+ paper_copy.forceMove(drop_location())
+ visible_message("[paper_copy] is shoved out of the way by [ass]!")
+ paper_copy = null
+ else if(document_copy)
+ document_copy.forceMove(drop_location())
+ visible_message("[document_copy] is shoved out of the way by [ass]!")
+ document_copy = null
+
+/obj/machinery/photocopier/Exited(atom/movable/AM, atom/newloc)
+ check_ass() // There was potentially a person sitting on the copier, check if they're still there.
+ return ..()
+
+/**
+ * Checks the living mob `ass` exists and its location is the same as the photocopier.
+ *
+ * Returns FALSE if `ass` doesn't exist or is not at the copier's location. Returns TRUE otherwise.
+ */
/obj/machinery/photocopier/proc/check_ass() //I'm not sure wether I made this proc because it's good form or because of the name.
if(!ass)
- return 0
- if(ass.loc != src.loc)
+ return FALSE
+ if(ass.loc != loc)
ass = null
- updateUsrDialog()
- return 0
- else if(ishuman(ass))
- if(!ass.get_item_by_slot(ITEM_SLOT_ICLOTHING) && !ass.get_item_by_slot(ITEM_SLOT_OCLOTHING))
- return 1
- else
- return 0
- else
- return 1
+ return FALSE
+ return TRUE
+/**
+ * Checks if the copier is deleted, or has something dense at its location. Called in `MouseDrop_T()`
+ */
/obj/machinery/photocopier/proc/copier_blocked()
if(QDELETED(src))
return
if(loc.density)
- return 1
+ return TRUE
for(var/atom/movable/AM in loc)
if(AM == src)
continue
if(AM.density)
- return 1
- return 0
+ return TRUE
+ return FALSE
+/**
+ * Checks if there is an item inserted into the copier or a mob sitting on top of it.
+ *
+ * Return `FALSE` is the copier has something inside of it. Returns `TRUE` if it doesn't.
+ */
/obj/machinery/photocopier/proc/copier_empty()
- if(copy || photocopy || check_ass())
- return 0
+ if(paper_copy || photo_copy || document_copy || check_ass())
+ return FALSE
else
- return 1
+ return TRUE
/*
* Toner cartridge
@@ -343,3 +493,11 @@
desc = "Why would ANYONE need THIS MUCH TONER?"
charges = 200
max_charges = 200
+
+#undef PHOTO_GREYSCALE
+#undef PHOTO_COLOR
+#undef PAPER_TONER_USE
+#undef PHOTO_TONER_USE
+#undef DOCUMENT_TONER_USE
+#undef ASS_TONER_USE
+#undef MAX_COPIES_AT_ONCE
diff --git a/code/modules/shuttle/computer.dm b/code/modules/shuttle/computer.dm
index 682a7fa14b..cee83c5b23 100644
--- a/code/modules/shuttle/computer.dm
+++ b/code/modules/shuttle/computer.dm
@@ -33,7 +33,6 @@
var/datum/browser/popup = new(user, "computer", M ? M.name : "shuttle", 300, 200)
popup.set_content("[dat]")
- popup.set_title_image(usr.browse_rsc_icon(src.icon, src.icon_state))
popup.open()
/obj/machinery/computer/shuttle/Topic(href, href_list)
diff --git a/config/policy.json b/config/policy.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/config/policy.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/tgstation.dme b/tgstation.dme
index ff4ea35efe..e5a59d5dde 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3399,6 +3399,7 @@
#include "code\modules\tgui\states\always.dm"
#include "code\modules\tgui\states\conscious.dm"
#include "code\modules\tgui\states\contained.dm"
+#include "code\modules\tgui\states\debug.dm"
#include "code\modules\tgui\states\deep_inventory.dm"
#include "code\modules\tgui\states\default.dm"
#include "code\modules\tgui\states\hands.dm"