Files
Bubberstation/code/game/objects/items/devices/taperecorder.dm
Timberpoes d06f6c9a55 Fix tape recorder printing out transcripts phat enough to crash your game. (#69121)
About The Pull Request

Tape recorders can hold 10 minutes of say logs and then transcribe them.

People say a lot of things in 10 minutes.

These transcriptions can be quite large.

These transcriptions have a lot of spans, which get sanitised away to nothingness JS-side. This causes a lot of client-side JS processing on paper for transcripts that go super saiyan beyond the character limit.

This can hang peoples' games up when they're running SS13 on their Samsung SmartFridge.

This PR paginates printed transcripts.
2022-08-12 20:45:49 -07:00

468 lines
14 KiB
Plaintext

/obj/item/taperecorder
name = "universal recorder"
desc = "A device that can record to cassette tapes, and play them. It automatically translates the content in playback."
icon = 'icons/obj/device.dmi'
icon_state = "taperecorder_empty"
inhand_icon_state = "analyzer"
worn_icon_state = "analyzer"
lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
w_class = WEIGHT_CLASS_SMALL
slot_flags = ITEM_SLOT_BELT
custom_materials = list(/datum/material/iron=60, /datum/material/glass=30)
force = 2
throwforce = 2
speech_span = SPAN_TAPE_RECORDER
drop_sound = 'sound/items/handling/taperecorder_drop.ogg'
pickup_sound = 'sound/items/handling/taperecorder_pickup.ogg'
var/recording = FALSE
var/playing = FALSE
var/playsleepseconds = 0
var/obj/item/tape/mytape
var/starting_tape_type = /obj/item/tape/random
var/open_panel = FALSE
var/canprint = TRUE
var/list/icons_available = list()
var/radial_icon_file = 'icons/hud/radial_taperecorder.dmi'
///Whether we've warned during this recording session that the tape is almost up.
var/time_warned = FALSE
///Seconds under which to warn that the tape is almost up.
var/time_left_warning = 60 SECONDS
///Sound loop that plays when recording or playing back.
var/datum/looping_sound/tape_recorder_hiss/soundloop
/obj/item/taperecorder/Initialize(mapload)
. = ..()
if(starting_tape_type)
mytape = new starting_tape_type(src)
soundloop = new(src)
update_appearance()
become_hearing_sensitive()
/obj/item/taperecorder/Destroy()
QDEL_NULL(soundloop)
QDEL_NULL(mytape)
return ..()
/obj/item/taperecorder/proc/readout()
if(mytape)
if(playing)
return span_notice("<b>PLAYING</b>")
else
var/time = mytape.used_capacity / 10 //deciseconds / 10 = seconds
var/mins = round(time / 60)
var/secs = time - mins * 60
return span_notice("<b>[mins]</b>m <b>[secs]</b>s")
return span_notice("<b>NO TAPE INSERTED</b>")
/obj/item/taperecorder/examine(mob/user)
. = ..()
if(in_range(src, user) || isobserver(user))
. += span_notice("The wire panel is [open_panel ? "opened" : "closed"]. The display reads:")
. += "[readout()]"
/obj/item/taperecorder/AltClick(mob/user)
. = ..()
play()
/obj/item/taperecorder/proc/update_available_icons()
icons_available = list()
if(!playing && !recording)
icons_available += list("Record" = image(radial_icon_file,"record"))
icons_available += list("Play" = image(radial_icon_file,"play"))
if(canprint && mytape?.storedinfo.len)
icons_available += list("Print Transcript" = image(radial_icon_file,"print"))
if(playing || recording)
icons_available += list("Stop" = image(radial_icon_file,"stop"))
if(mytape)
icons_available += list("Eject" = image(radial_icon_file,"eject"))
/obj/item/taperecorder/proc/update_sound()
if(!playing && !recording)
soundloop.stop()
else
soundloop.start()
/obj/item/taperecorder/attackby(obj/item/I, mob/user, params)
if(!mytape && istype(I, /obj/item/tape))
if(!user.transferItemToLoc(I,src))
return
mytape = I
to_chat(user, span_notice("You insert [I] into [src]."))
playsound(src, 'sound/items/taperecorder/taperecorder_close.ogg', 50, FALSE)
update_appearance()
/obj/item/taperecorder/proc/eject(mob/user)
if(mytape)
playsound(src, 'sound/items/taperecorder/taperecorder_open.ogg', 50, FALSE)
to_chat(user, span_notice("You remove [mytape] from [src]."))
stop()
user.put_in_hands(mytape)
mytape = null
update_appearance()
/obj/item/taperecorder/fire_act(exposed_temperature, exposed_volume)
mytape.unspool() //Fires unspool the tape, which makes sense if you don't think about it
..()
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/item/taperecorder/attack_hand(mob/user, list/modifiers)
if(loc != user || !mytape || !user.is_holding(src))
return ..()
eject(user)
/obj/item/taperecorder/proc/can_use(mob/user)
if(user && ismob(user))
if(!user.incapacitated())
return TRUE
return FALSE
/obj/item/taperecorder/verb/ejectverb()
set name = "Eject Tape"
set category = "Object"
if(!can_use(usr))
return
if(!mytape)
return
eject(usr)
/obj/item/taperecorder/update_icon_state()
if(!mytape)
icon_state = "taperecorder_empty"
return ..()
if(recording)
icon_state = "taperecorder_recording"
return ..()
if(playing)
icon_state = "taperecorder_playing"
return ..()
icon_state = "taperecorder_idle"
return ..()
/obj/item/taperecorder/Hear(message, atom/movable/speaker, message_langs, raw_message, radio_freq, spans, list/message_mods = list())
. = ..()
if(mytape && recording)
mytape.timestamp += mytape.used_capacity
mytape.storedinfo += "\[[time2text(mytape.used_capacity,"mm:ss")]\] [message]"
/obj/item/taperecorder/verb/record()
set name = "Start Recording"
set category = "Object"
if(!can_use(usr))
return
if(!mytape || mytape.unspooled)
return
if(recording)
return
if(playing)
return
playsound(src, 'sound/items/taperecorder/taperecorder_play.ogg', 50, FALSE)
if(mytape.used_capacity < mytape.max_capacity)
recording = TRUE
say("Recording started.")
update_sound()
update_appearance()
var/used = mytape.used_capacity //to stop runtimes when you eject the tape
var/max = mytape.max_capacity
while(recording && used < max)
mytape.used_capacity += 1 SECONDS
used += 1 SECONDS
if(max - used < time_left_warning && !time_warned)
time_warned = TRUE
say("[(max - used) / 10] seconds left!") //deciseconds / 10 = seconds
sleep(1 SECONDS)
if(used >= max)
say("Tape full.")
stop()
else
say("The tape is full!")
playsound(src, 'sound/items/taperecorder/taperecorder_stop.ogg', 50, FALSE)
/obj/item/taperecorder/verb/stop()
set name = "Stop"
set category = "Object"
if(!can_use(usr))
return
if(recording)
playsound(src, 'sound/items/taperecorder/taperecorder_stop.ogg', 50, FALSE)
say("Recording stopped.")
recording = FALSE
else if(playing)
playsound(src, 'sound/items/taperecorder/taperecorder_stop.ogg', 50, FALSE)
say("Playback stopped.")
playing = FALSE
time_warned = FALSE
update_appearance()
update_sound()
/obj/item/taperecorder/verb/play()
set name = "Play Tape"
set category = "Object"
if(!can_use(usr))
return
if(!mytape || mytape.unspooled)
return
if(recording)
return
if(playing)
return
playing = TRUE
update_appearance()
update_sound()
say("Playback started.")
playsound(src, 'sound/items/taperecorder/taperecorder_play.ogg', 50, FALSE)
var/used = mytape.used_capacity //to stop runtimes when you eject the tape
var/max = mytape.max_capacity
for(var/i = 1, used <= max, sleep(playsleepseconds))
if(!mytape)
break
if(playing == FALSE)
break
if(mytape.storedinfo.len < i)
say("End of recording.")
break
say("[mytape.storedinfo[i]]", sanitize=FALSE)//We want to display this properly, don't double encode
if(mytape.storedinfo.len < i + 1)
playsleepseconds = 1
sleep(1 SECONDS)
else
playsleepseconds = mytape.timestamp[i + 1] - mytape.timestamp[i]
if(playsleepseconds > 14 SECONDS)
sleep(1 SECONDS)
say("Skipping [playsleepseconds/10] seconds of silence.")
playsleepseconds = 1 SECONDS
i++
stop()
/obj/item/taperecorder/attack_self(mob/user)
if(!mytape)
to_chat(user, span_notice("\The [src] is empty."))
return
if(mytape.unspooled)
to_chat(user, span_warning("\The tape inside \the [src] is broken!"))
return
update_available_icons()
if(icons_available)
var/selection = show_radial_menu(user, src, icons_available, radius = 38, require_near = TRUE, tooltips = TRUE)
if(!selection)
return
switch(selection)
if("Stop")
stop()
if("Record")
record()
if("Play")
play()
if("Print Transcript")
print_transcript()
if("Eject")
eject(user)
/obj/item/taperecorder/verb/print_transcript()
set name = "Print Transcript"
set category = "Object"
var/list/transcribed_info = mytape.storedinfo
if(!length(transcribed_info))
return
if(!mytape)
return
if(!canprint)
to_chat(usr, span_warning("The recorder can't print that fast!"))
return
if(recording || playing)
return
if(!can_use(usr))
return
var/transcribed_text = "<b>Transcript:</b><br><br>"
var/page_count = 1
var/tape_name = mytape.name
var/initial_tape_name = initial(mytape.name)
var/paper_name = "paper- '[tape_name == initial_tape_name ? "Tape" : "[tape_name]"] Transcript'"
for(var/transcript_excerpt in transcribed_info)
var/excerpt_length = length(transcript_excerpt)
// Very unexpected. Better abort non-gracefully.
if(excerpt_length > MAX_PAPER_LENGTH)
say("Error: Data corruption detected. Cannot print.")
CRASH("Transcript entry has more than [MAX_PAPER_LENGTH] chars: [excerpt_length] chars")
// If we're going to overflow the paper's length, print the current transcribed text out first and reset to prevent us
// going over the paper char count.
if((length(transcribed_text) + excerpt_length) > MAX_PAPER_LENGTH)
var/obj/item/paper/transcript_paper = new /obj/item/paper(get_turf(src))
transcript_paper.add_raw_text(transcribed_text)
transcript_paper.name = "[paper_name] page [page_count]"
transcript_paper.update_appearance()
transcribed_text = ""
page_count++
transcribed_text += "[transcript_excerpt]<br>"
var/obj/item/paper/transcript_paper = new /obj/item/paper(get_turf(src))
transcript_paper.add_raw_text(transcribed_text)
transcript_paper.name = "[paper_name] page [page_count]"
transcript_paper.update_appearance()
say("Transcript printed, [page_count] pages.")
playsound(src, 'sound/items/taperecorder/taperecorder_print.ogg', 50, FALSE)
// Can't put the entire stack into their hands if there's multple pages, but hey we can at least put one page in.
usr.put_in_hands(transcript_paper)
canprint = FALSE
addtimer(VARSET_CALLBACK(src, canprint, TRUE), 30 SECONDS)
//empty tape recorders
/obj/item/taperecorder/empty
starting_tape_type = null
/obj/item/tape
name = "tape"
desc = "A magnetic tape that can hold up to ten minutes of content on either side."
icon_state = "tape_white"
icon = 'icons/obj/device.dmi'
inhand_icon_state = "analyzer"
lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
w_class = WEIGHT_CLASS_TINY
custom_materials = list(/datum/material/iron=20, /datum/material/glass=5)
force = 1
throwforce = 0
obj_flags = UNIQUE_RENAME //my mixtape
drop_sound = 'sound/items/handling/tape_drop.ogg'
pickup_sound = 'sound/items/handling/tape_pickup.ogg'
///Because we can't expect God to do all the work.
var/initial_icon_state
var/max_capacity = 10 MINUTES
var/used_capacity = 0 SECONDS
///Numbered list of chat messages the recorder has heard with spans and prepended timestamps. Used for playback and transcription.
var/list/storedinfo = list()
///Numbered list of seconds the messages in the previous list appear at on the tape. Used by playback to get the timing right.
var/list/timestamp = list()
var/used_capacity_otherside = 0 SECONDS //Separate my side
var/list/storedinfo_otherside = list()
var/list/timestamp_otherside = list()
var/unspooled = FALSE
var/list/icons_available = list()
var/radial_icon_file = 'icons/hud/radial_tape.dmi'
/obj/item/tape/fire_act(exposed_temperature, exposed_volume)
unspool()
..()
/obj/item/tape/Initialize(mapload)
. = ..()
initial_icon_state = icon_state //random tapes will set this after choosing their icon
var/mycolor = random_short_color()
name += " ([mycolor])" //multiple tapes can get confusing fast
if(icon_state == "tape_greyscale")
add_atom_colour("#[mycolor]", FIXED_COLOUR_PRIORITY)
if(prob(50))
tapeflip()
/obj/item/tape/proc/update_available_icons()
icons_available = list()
if(!unspooled)
icons_available += list("Unwind tape" = image(radial_icon_file,"tape_unwind"))
icons_available += list("Flip tape" = image(radial_icon_file,"tape_flip"))
/obj/item/tape/attack_self(mob/user)
update_available_icons()
if(icons_available)
var/selection = show_radial_menu(user, src, icons_available, radius = 38, require_near = TRUE, tooltips = TRUE)
if(!selection)
return
switch(selection)
if("Flip tape")
if(loc != user)
return
tapeflip()
to_chat(user, span_notice("You turn \the [src] over."))
playsound(src, 'sound/items/taperecorder/tape_flip.ogg', 70, FALSE)
if("Unwind tape")
if(loc != user)
return
unspool()
to_chat(user, span_warning("You pull out all the tape!"))
/obj/item/tape/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
if(prob(50))
tapeflip()
. = ..()
/obj/item/tape/proc/unspool()
//Let's not add infinite amounts of overlays when our fire_act is called repeatedly
if(!unspooled)
add_overlay("ribbonoverlay")
unspooled = TRUE
/obj/item/tape/proc/respool()
cut_overlay("ribbonoverlay")
unspooled = FALSE
/obj/item/tape/proc/tapeflip()
//first we save a copy of our current side
var/list/storedinfo_currentside = storedinfo.Copy()
var/list/timestamp_currentside = timestamp.Copy()
var/used_capacity_currentside = used_capacity
//then we overwite our current side with our other side
storedinfo = storedinfo_otherside.Copy()
timestamp = timestamp_otherside.Copy()
used_capacity = used_capacity_otherside
//then we overwrite our other side with the saved side
storedinfo_otherside = storedinfo_currentside.Copy()
timestamp_otherside = timestamp_currentside.Copy()
used_capacity_otherside = used_capacity_currentside
if(icon_state == initial_icon_state)
icon_state = "[initial_icon_state]_reverse"
else if(icon_state == "[initial_icon_state]_reverse") //so flipping doesn't overwrite an unexpected icon_state (e.g. an admin's)
icon_state = initial_icon_state
/obj/item/tape/screwdriver_act(mob/living/user, obj/item/tool)
if(!unspooled)
return FALSE
to_chat(user, span_notice("You start winding the tape back in..."))
if(tool.use_tool(src, user, 120))
to_chat(user, span_notice("You wind the tape back in."))
respool()
//Random colour tapes
/obj/item/tape/random
icon_state = "random_tape"
/obj/item/tape/random/Initialize(mapload)
icon_state = "tape_[pick("white", "blue", "red", "yellow", "purple", "greyscale")]"
. = ..()
/obj/item/tape/dyed
icon_state = "greyscale"