Files
Aurora.3/code/modules/paperwork/paper.dm
T
FabianK3andGitHub 36d267d83e Persistent data framework (#21055)
# Summary

This PR has the goal of adding a base framework for tracking, saving and
otherwise managing persistent data between rounds, written from scratch.

Persistent data can be anything - Most expectedly noticeboards, dirt,
papers, etc.

_Technical details about the PR below are subject to change._

## Estimated scope

The first iteration of the persistence framework should include the
functioning subsystem and the first implementation of a persistent type
using said subsystem.

- [x] Prepare database for persistent data
- [x] Add new features to `/obj/`
- [x] Logic to get and load persistent data into objects for the
subsystem
  - [x] Internal logic for tracking and other
- [x] Implement subsystem
  - [x] Basics, files, integration of subsystem
  - [x] Database queries for different operations
- [x] Methods for registering and de-registering object tracking for
`/obj`
  - [x] Round start logic
  - [x] Round end logic
- [x] Implement first persistent type: Papers on notice boards
- [ ] Code review
  - [x] Scope validation
  - [x] Testing
  - [x] Full changelog
- Merge
- [ ] Setup logging config for new subsystem
- [x] Documentation for developers ([How to add new persistent
types](https://github.com/Aurorastation/Aurora.3/wiki/Persistence))

## Inner workings

The new persistence subsystem has the following concept:
At round start the subsystem reads existing data from the database and
creates objects for them.
During the round objects can be added to the tracking using a register
function (or removed by a de-register function).
At round end the subsystem goes over all it's actively tracked objects:
Create new objects (that don't have a persistent ID from the database),
update objects that changed during the round and expire objects that are
no longer tracked.

The objects in questions can register themselves, de-register themselves
and decide themselves what content should be stored and handle how to
set themselves up during the subsystems init phase.

## Information on the new table

`id` - database generated ID for tracking persistent objects over
multiple rounds, objects without an ID are considered new and created
during the round.
`author_ckey` - With the first implementation unused, but expected to be
required in the future to allow any staff/moderation on persistent data.
Nullable as persistent data also can be things like decals without an
actual owner.
`type` - Type of persistent data used for creating objects at round
start.
`created_at` - Statistical values.
`expire_at` - Used for cleaning the database and setting limits for
persistent data types. It's optional and allows permanent persistent
data (command notice boards?).
`content` - JSON formatted data containing the actual properties of an
individual tracked object (e.g. what's written on a paper, what's its
title).
`x`, `y`, `z` - Coordinates taken and given from/to the object during
save/load. Those can be null for future purposes.

## The goal on how to add new persistent data types

`/obj` received two new methods which need to be overriden in the to-be
added new persistent type:
`persistence_get_content()` and `persistence_apply_content(content, x,
y, z)`.

The first method has the responsibility to provide all custom content of
a new type to the subsystem in the form of an associated list (`["title"
= "Hello, World!", "value" = 1337].

What does that mean?  
In the example of a piece of paper, the method needs to put the papers
title and text content into a an associated list, that list will be
saved by the persistence subsystem.

The second method has the responsibility to apply the previously saved
content when persistent data is initialized at the start of the round:
The associated list needs to be read back and applied to the paper,
additionally coordinates are provided for the object that have been
taken during saving. Note that the coordinates might be null and could
be ignored during setup.

During the round, when a new object is created, like a paper, you need
to call the subsystem and register a track:
`SSpersistence.register_track(your_object_to_track, ckey (optional)`.
The subsystem will be using the methods above when the round ends and
saves the object - and loads it at the start of the round using the
second method. The ckey needs to be given for all persistent that
contains user generated content, e.g. papers.

In case a persistent object gets removed, you need to call
`SSpersistence.deregister_track(your_tracked_object)`. At the end of the
round the object will be removed from the storage.

`/obj` contains the variable (along some other technical vars)
`persistance_initial_expiration_time_days`, which has a default value of
30 days, but can be safely overriden on a per-type basis. This value (in
days) will be used for the persistent data entry expiration date when a
new object of said type is stored in the database.

## PR description changelog

- Added *The goal on how to add new persistent data types*.
- Updates *Estimated scope* format.
- Updates *Inner workings*.
- Final iteration.

*(Grammer and formatting not tracked manually)*
2025-08-16 22:00:12 +00:00

878 lines
31 KiB
Plaintext

/*
* Paper
* also scraps of paper
*/
/obj/item/paper
name = "paper"
desc = "A piece of paper."
gender = NEUTER
icon = 'icons/obj/bureaucracy.dmi'
icon_state = "paper"
var/base_state = "paper"
item_state = "paper"
contained_sprite = 1
throwforce = 0
w_class = WEIGHT_CLASS_TINY
throw_range = 1
throw_speed = 1
layer = ABOVE_OBJ_LAYER
slot_flags = SLOT_HEAD
body_parts_covered = HEAD
attack_verb = list("bapped")
///What's actually written on the paper.
var/info
///A different version of the paper which includes html links at fields and EOF
var/info_links
///The (text for the) stamps on the paper.
var/stamps
///Amount of user created fields
var/fields
var/free_space = MAX_PAPER_MESSAGE_LEN
var/list/stamped
///Icons and
var/list/ico[0]
///offsets stored for later
var/list/offset_x[0]
///usage by the photocopier
var/list/offset_y[0]
var/rigged = 0
var/last_honk = 0
/// The name of the paper before it was folded into a plane.
var/old_name
/// If it can be folded into a plane or swan
var/can_fold = TRUE
/// Is it made of paper and/or burnable material?
var/paper_like = TRUE
var/const/deffont = "Verdana"
var/const/signfont = "Times New Roman"
var/const/crayonfont = "Comic Sans MS"
var/const/fountainfont = "Segoe Script"
var/const/typewriterfont = "Typewriter"
drop_sound = 'sound/items/drop/paper.ogg'
pickup_sound = 'sound/items/pickup/paper.ogg'
var/can_change_icon_state = TRUE
var/set_unsafe_on_init = FALSE
/obj/item/paper/feedback_hints(mob/user, distance, is_adjacent)
. += ..()
if (old_name && (icon_state == "paper_plane" || icon_state == "paper_swan"))
. += SPAN_NOTICE("You're going to have to unfold it before you can read it.")
return
if(name != initial(name))
. += "It's titled '[name]'."
if(distance <= 1)
show_content(user)
else
. += SPAN_NOTICE("You have to go closer if you want to read it.")
/obj/item/paper/Initialize(mapload, text, title)
. = ..()
base_state = initial(icon_state)
if (text || title)
if(set_unsafe_on_init)
set_content_unsafe(title, text ? text : info)
else
set_content(title, text ? text : info)
else
updateinfolinks()
if (mapload)
update_icon()
else
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_icon)), 1)
/obj/item/paper/proc/set_content(title, text)
if(title)
name = title
if (text && length(text))
info = parsepencode(text)
else
info = ""
update_icon()
update_space(info)
updateinfolinks()
// DO NOT USE THIS FOR UNTRUSTED PLAYER INPUT. IT DOES NOT SANITIZE.
/obj/item/paper/proc/set_content_unsafe(title, text)
if (title)
name = title
if (text && length(text))
info = text
else
info = ""
update_icon()
update_space(info)
updateinfolinks()
/obj/item/paper/update_icon()
if(!can_change_icon_state)
return
else if (info && length(trim(info)))
icon_state = "[base_state]_words"
else
icon_state = "[base_state]"
/**
* Updates the amount of free space in the paper
*
* * new_text - The new text the paper contains (supposedly), text
*/
/obj/item/paper/proc/update_space(new_text)
if(new_text)
free_space -= length(strip_html_properly(new_text))
/obj/item/paper/get_examine_text(mob/user, distance, is_adjacent, infix, suffix)
. = ..()
if (old_name && (icon_state == "paper_plane" || icon_state == "paper_swan"))
. += SPAN_NOTICE("You're going to have to unfold it before you can read it.")
return
if(name != initial(name))
. += "It's titled '[name]'."
if(distance <= 1)
show_content(user)
else
. += SPAN_NOTICE("You have to go closer if you want to read it.")
/obj/item/paper/proc/show_content(mob/user, forceshow)
simple_asset_ensure_is_sent(user, /datum/asset/simple/paper)
var/datum/browser/paper_win = new(user, name, null, 450, 500, null, TRUE)
paper_win.set_content(get_content(user, can_read(user, forceshow)))
paper_win.add_stylesheet("paper_languages", 'html/browser/paper_languages.css')
paper_win.open()
/obj/item/paper/proc/can_read(var/mob/user, var/forceshow = FALSE)
var/can_read = (istype(user, /mob/living/carbon/human) || isghost(user) || istype(user, /mob/living/silicon)) || forceshow
if(!forceshow && istype(user,/mob/living/silicon/ai))
var/mob/living/silicon/ai/AI = user
can_read = get_dist(src, AI.camera) < 2
return can_read
/obj/item/paper/proc/get_content(var/mob/user, var/can_read = TRUE)
return "<head><title>[capitalize_first_letters(name)]</title><style>body {background-color: [color];}</style></head><body>[can_read ? parse_languages(user, info) : stars(info)][stamps]</body>"
/obj/item/paper/verb/rename()
set name = "Rename paper"
set category = "Object"
set src in usr
if(use_check_and_message(usr, USE_ALLOW_NON_ADJACENT))
return
if((usr.is_clumsy()) && prob(50))
to_chat(usr, SPAN_WARNING("You cut yourself on the paper."))
return
var/n_name = sanitizeSafe(input(usr, "What would you like to label the paper?", "Paper Labelling", null) as text, MAX_NAME_LEN)
if(use_check_and_message(usr, USE_ALLOW_NON_ADJACENT))
return
var/atom/surface_atom = recursive_loc_turf_check(src, 3, usr)
if(surface_atom == usr || surface_atom.Adjacent(usr))
if(n_name)
name = "[initial(name)] ([n_name])"
else
name = initial(name)
add_fingerprint(usr)
/obj/item/paper/attack_self(mob/living/user as mob)
if(user.a_intent == I_HURT && paper_like)
if(icon_state == "scrap")
user.show_message(SPAN_WARNING("\The [src] is already crumpled."))
return
//crumple dat paper
info = stars(info,85)
user.visible_message("\The [user] crumples \the [src] into a ball!", "You crumple \the [src] into a ball.")
playsound(src, 'sound/bureaucracy/papercrumple.ogg', 50, 1)
icon_state = "scrap"
throw_range = 4 //you can now make epic paper ball hoops into the disposals (kinda dumb that you could only throw crumpled paper 1 tile) -wezzy
return
if (user.a_intent == I_GRAB && icon_state != "scrap" && can_fold)
if (icon_state == "paper_plane")
user.show_message(SPAN_ALERT("The paper is already folded into a plane."))
return
user.visible_message(SPAN_NOTICE("\The [user] carefully folds \the [src] into a plane."),
SPAN_NOTICE("You carefully fold \the [src] into a plane."), "\The [user] folds \the [src] into a plane.")
playsound(src, 'sound/bureaucracy/paperfold.ogg', 50, 1)
icon_state = "paper_plane"
throw_range = 8
old_name = name
name = "paper plane"
ClearOverlays() //Removes stamp icons
return
if (user.a_intent == I_DISARM && icon_state != "scrap" && can_fold)
if (icon_state == "paper_swan")
user.show_message(SPAN_ALERT("The paper is already folded into a swan."))
return
user.visible_message(SPAN_NOTICE("\The [user] carefully folds \the [src] into an origami swan."),
SPAN_NOTICE("You carefully fold \the [src] into a swan."), "\The [user] folds \the [src] into a swan.")
playsound(src, 'sound/bureaucracy/paperfold.ogg', 50, 1)
icon_state = "paper_swan"
old_name = name
name = "origami swan"
ClearOverlays() //Removes stamp icons
return
if (user.a_intent == I_HELP && old_name && (icon_state == "paper_plane" || icon_state == "paper_swan"))
user.visible_message(SPAN_NOTICE("\The [user] unfolds \the [src]."), SPAN_NOTICE("You unfold \the [src]."), "You hear paper rustling.")
playsound(src, 'sound/bureaucracy/paperfold.ogg', 50, 1)
icon_state = base_state
throw_range = initial(throw_range)
name = old_name
old_name = null
restore_stamps()
update_icon()
return
examinate(user, src)
if(rigged && (Holiday == "April Fool's Day"))
if(last_honk <= world.time - 20) //Spam limiter.
last_honk = world.time
playsound(src.loc, 'sound/items/bikehorn.ogg', 50, 1)
src.add_fingerprint(user)
/obj/item/paper/attack_ai(var/mob/living/silicon/ai/user)
show_content(user)
/obj/item/paper/attack(mob/living/target_mob, mob/living/user, target_zone)
var/mob/living/carbon/M = target_mob
if(!istype(M))
return ..()
if(target_zone == BP_EYES)
user.visible_message(SPAN_NOTICE("You show \the [src] to [M]."), \
SPAN_NOTICE("[user] holds up \the [src] and shows it to [M]."))
examinate(M, src)
else if(target_zone == BP_MOUTH && paper_like) // lipstick wiping
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H == user)
to_chat(user, SPAN_NOTICE("You wipe off the lipstick with [src]."))
H.lipstick_color = null
H.update_body()
else
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, 1 SECOND, H, do_flags = (DO_DEFAULT | DO_USER_UNIQUE_ACT) & ~DO_BOTH_CAN_TURN))
user.visible_message(SPAN_NOTICE("[user] wipes [H]'s lipstick off with \the [src]."), \
SPAN_NOTICE("You wipe off [H]'s lipstick."))
H.lipstick_color = null
H.update_body()
/obj/item/paper/proc/addtofield(var/id, var/text, var/links = 0)
var/locid = 0
var/laststart = 1
var/textindex = 1
while(1) // I know this can cause infinite loops and fuck up the whole server, but the if(istart==0) should be safe as fuck
var/istart = 0
if(links)
istart = findtext(info_links, "<span class=\"paper_field\">", laststart)
else
istart = findtext(info, "<span class=\"paper_field\">", laststart)
if(istart==0)
return // No field found with matching id
laststart = istart+1
locid++
if(locid == id)
var/iend = 1
if(links)
iend = findtext(info_links, "</span>", istart)
else
iend = findtext(info, "</span>", istart)
textindex = iend
break
if(links)
var/before = copytext(info_links, 1, textindex)
var/after = copytext(info_links, textindex)
info_links = before + text + after
else
var/before = copytext(info, 1, textindex)
var/after = copytext(info, textindex)
info = before + text + after
updateinfolinks()
/obj/item/paper/proc/updateinfolinks()
info_links = info
for (var/i = 1, i <= min(fields, 35), i++)
addtofield(i, "<font face=\"[deffont]\"><A href='byond://?src=[REF(src)];write=[i]'>write</A></font>", 1)
info_links = info_links + "<font face=\"[deffont]\"><A href='byond://?src=[REF(src)];write=end'>write</A></font>"
/obj/item/paper/proc/clearpaper()
info = null
stamps = null
free_space = MAX_PAPER_MESSAGE_LEN
stamped = list()
ClearOverlays()
updateinfolinks()
update_icon()
/obj/item/paper/proc/get_signature(var/obj/item/pen/P, mob/user as mob)
if(P && P.ispen())
return P.get_signature(user)
if (user)
if (user.mind && user.mind.signature)
return user.mind.signature
else if (user.real_name)
return "<i>[user.real_name]</i>"
return "<i>Anonymous</i>"
/obj/item/paper/proc/get_signfont(var/obj/item/pen/P, var/mob/user)
if (!istype(P, /obj/item/pen/chameleon))
if (user && user.mind && user.mind.signfont)
return user.mind.signfont
return signfont
/obj/item/paper/proc/parsepencode(t, obj/item/pen/P, mob/user, iscrayon, isfountain, istypewriter)
if(user)
t = parse_languages(user, t, TRUE)
if(P)
t = replacetext(t, "\[sign\]", "<font face=\"[get_signfont(P, user)]\">[get_signature(P, user)]</font>")
if(iscrayon) // If it is a crayon, and he still tries to use these, make them empty!
t = replacetext(t, "\[*\]", "")
t = replacetext(t, "\[hr\]", "")
t = replacetext(t, "\[small\]", "")
t = replacetext(t, "\[/small\]", "")
t = replacetext(t, "\[list\]", "")
t = replacetext(t, "\[/list\]", "")
t = replacetext(t, "\[table\]", "")
t = replacetext(t, "\[/table\]", "")
t = replacetext(t, "\[row\]", "")
t = replacetext(t, "\[cell\]", "")
t = replacetext(t, "\[logo_scc\]", "")
t = replacetext(t, "\[logo_scc_small\]", "")
t = replacetext(t, "\[logo_nt\]", "")
t = replacetext(t, "\[logo_nt_small\]", "")
t = replacetext(t, "\[logo_zh\]", "")
t = replacetext(t, "\[logo_zh_small\]", "")
t = replacetext(t, "\[logo_idris\]", "")
t = replacetext(t, "\[logo_idris_small\]", "")
t = replacetext(t, "\[logo_eridani\]", "")
t = replacetext(t, "\[logo_eridani_small\]", "")
t = replacetext(t, "\[logo_zavod\]", "")
t = replacetext(t, "\[logo_zavod_small\]", "")
t = replacetext(t, "\[logo_hp\]", "")
t = replacetext(t, "\[logo_hp_small\]", "")
t = replacetext(t, "\[logo_be\]", "")
t = replacetext(t, "\[logo_golden\]", "")
t = replacetext(t, "\[logo_pvpolice\]", "")
t = replacetext(t, "\[logo_pvpolice_small\]", "")
t = replacetext(t, "\[barcode\]", "")
if(istypewriter)
t = replacetext(t, "\[*\]", "")
t = replacetext(t, "\[hr\]", "")
t = replacetext(t, "\[small\]", "")
t = replacetext(t, "\[/small\]", "")
t = replacetext(t, "\[list\]", "")
t = replacetext(t, "\[/list\]", "")
t = replacetext(t, "\[table\]", "")
t = replacetext(t, "\[/table\]", "")
t = replacetext(t, "\[row\]", "")
t = replacetext(t, "\[cell\]", "")
t = replacetext(t, "\[barcode\]", "")
if(iscrayon)
t = "<font face=\"[crayonfont]\" color=[P ? P.colour : "black"]><b>[t]</b></font>"
else if(isfountain)
t = "<font face=\"[fountainfont]\" color=[P ? P.colour : "black"]><i>[t]</i></font>"
else if(istypewriter)
t = "<font face=\"[typewriterfont]\" color=[P ? P.colour : "black"]><i>[t]</i></font>"
else
t = "<font face=\"[deffont]\" color=[P ? P.colour : "black"]>[t]</font>"
t = pencode2html(t)
//Count the fields
var/laststart = 1
while(1)
var/i = findtext(t, "<span class=\"paper_field\">", laststart) //</span>
if(i==0)
break
laststart = i+1
fields++
return t
/obj/item/paper/proc/burnpaper(obj/item/P, mob/user)
var/class = "warning"
if(!use_check_and_message(user) && paper_like)
if(istype(P, /obj/item/flame/lighter/zippo))
class = "rose"
user.visible_message("<span class='[class]'>[user] holds \the [P] up to \the [src], it looks like [user.get_pronoun("he")]'s trying to burn it!</span>", \
"<span class='[class]'>You hold \the [P] up to \the [src], burning it slowly.</span>")
playsound(src.loc, 'sound/bureaucracy/paperburn.ogg', 50, 1)
if(icon_state == "scrap")
flick("scrap_onfire", src)
else
flick("paper_onfire", src)
addtimer(CALLBACK(src, PROC_REF(burnpaper_callback), P, user, class), 20, TIMER_UNIQUE)
/obj/item/paper/proc/burnpaper_callback(obj/item/P, mob/user, class = "warning")
if (QDELETED(user) || QDELETED(src))
return
if(get_dist(src, user) < 2 && user.get_active_hand() == P)
user.visible_message("<span class='[class]'>[user] burns right through \the [src], turning it to ash. It flutters through the air before settling on the floor in a heap.</span>", \
"<span class='[class]'>You burn right through \the [src], turning it to ash. It flutters through the air before settling on the floor in a heap.</span>")
if(user.get_inactive_hand() == src)
user.drop_from_inventory(src)
new /obj/effect/decal/cleanable/ash(src.loc)
qdel(src)
else
to_chat(user, SPAN_WARNING("You must hold \the [P] steady to burn \the [src]."))
/obj/item/paper/proc/restore_stamps() //Used when unfolding paper from cranes or planes, restores the stamp overlays.
for(var/i = 1, i <= ico.len, i++)
var/image/S = image('icons/obj/bureaucracy.dmi', ico[i])
S.pixel_x = offset_x[i]
S.pixel_y = offset_y[i]
AddOverlays(S)
/**
* Takes the paper's info variable, a user, and parses language markers that exist
* in it. It returns an HTML string which represents the languages properly.
*
* @param user The mob we're parsing the text for.
*
* @return An HTML string where all of the [lang][/lang] marker contents are replaced
* with scrambled and properly fonted content depending on what languages the user knows.
*/
/obj/item/paper/proc/parse_languages(mob/user, input, language_check = FALSE)
// Just a safety fallback.
if (!user)
return input
var/static/regex/written_lang_regex
if (!written_lang_regex)
written_lang_regex = new(@"(\[lang=([#_a-zA-Z0-9\^]{1})])(.*?)(\[\/lang])", "g")
. = input
while (written_lang_regex.Find(.))
var/datum/language/L = GLOB.language_keys[written_lang_regex.group[2]]
// Unknown language.
if (!L || !L.written_style)
continue
var/content = written_lang_regex.group[3]
var/reader_understands = user.say_understands(null, L)
// Replace the content with <p>content here</p>
if(!reader_understands)
if(language_check)
. = replacetext(., written_lang_regex.match, "")
else
content = L.scramble(content)
if(!language_check)
// Refer to paper/proc/show_content to edit the spans here.
. = replacetext(., written_lang_regex.match, "<span class='[L.written_style] [reader_understands ? "understood" : "scramble"]'>[L.short && reader_understands ? "([L.short]) [content]" : content]</span>")
/obj/item/paper/Topic(href, href_list)
..()
if(!usr || (usr.stat || usr.restrained()))
return
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, SPAN_INFO("There isn't enough space left on \the [src] to write anything."))
return
var/t = sanitize(input("Enter what you want to write:", "Write", null, null) as message, free_space, extra = 0)
if(!t)
return
var/obj/item/i = usr.get_active_hand() // Check to see if he still got that darn pen, also check if he's using a crayon or pen.
if(!i && istype(loc, /obj/item/portable_typewriter))
var/obj/item/portable_typewriter/T = loc
if(T.pen)
i = T.pen
if(i && istype(i, /obj/item/portable_typewriter) || !i && istype(loc, /obj/item/portable_typewriter))
var/obj/item/portable_typewriter/T = i
if(T.pen)
i = T.pen
if(!i || !i.ispen())
i = usr.get_inactive_hand()
var/obj/item/clipboard/c
var/iscrayon = FALSE
var/isfountain = FALSE
var/istypewriter = FALSE
if(!i.ispen())
if(usr.back && istype(usr.back,/obj/item/rig))
var/obj/item/rig/r = usr.back
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
if(istype(src.loc, /obj/item/clipboard))
c = src.loc
if(c.haspen)
i = c.haspen
else
return
if(istype(i, /obj/item/pen/crayon))
iscrayon = TRUE
if(istype(i, /obj/item/pen/fountain) || istype(i, /obj/item/pen/augment))
var/obj/item/pen/f = i
if(f.cursive)
isfountain = TRUE
else
isfountain = FALSE
if(istype(i, /obj/item/pen/typewriter))
istypewriter = TRUE
if(!write_check(usr))
return
var/last_fields_value = fields
t = parsepencode(t, i, usr, iscrayon, isfountain, istypewriter) // Encode everything from pencode to html
if(fields > 50)//large amount of fields creates a heavy load on the server, see updateinfolinks() and addtofield()
to_chat(usr, SPAN_WARNING("Too many fields. Sorry, you can't do this."))
fields = last_fields_value
return
if(id!="end")
addtofield(text2num(id), t) // He wants to edit a field, let him.
else
info += t // Oh, he wants to edit to the end of the file, let him.
updateinfolinks()
update_space(t)
var/datum/browser/paper_win = new(usr, name, null, 450, 500, null, TRUE)
paper_win.set_content("<head><title>[capitalize_first_letters(name)]</title><style>body {background-color: [color];}</style></head><body>[parse_languages(usr, info_links)][stamps]</body>")
paper_win.add_stylesheet("paper_languages", 'html/browser/paper_languages.css')
paper_win.open()
if(istype(i, /obj/item/pen/typewriter))
playsound(src, ('sound/machines/typewriter.ogg'), 40)
else
playsound(src, pick('sound/bureaucracy/pen1.ogg','sound/bureaucracy/pen2.ogg'), 20)
update_icon()
if(c)
c.update_icon()
/obj/item/paper/proc/write_check(var/mob/user)
. = TRUE
// 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(loc != user && !Adjacent(user))
. = FALSE
if(!. && istype(loc, /obj/item/clipboard))
var/obj/item/clipboard/C = loc
if(C.loc == user || C.Adjacent(user))
. = TRUE
if(!. && istype(loc, /obj/item/folder))
var/obj/item/folder/F = loc
if(F.loc_check(user) || F.Adjacent(user))
. = TRUE
if(!. && istype(loc, /obj/item/portable_typewriter))
var/obj/item/portable_typewriter/T = loc
if(T.loc == user || T.Adjacent(user))
. = TRUE
/obj/item/paper/attackby(obj/item/attacking_item, mob/user)
..()
if(istype(attacking_item, /obj/item/tape_roll) && !istype(src, /obj/item/paper/business_card))
var/obj/item/tape_roll/tape = attacking_item
tape.stick(src, user)
return
if(istype(attacking_item, /obj/item/paper) || istype(attacking_item, /obj/item/photo))
if (istype(attacking_item, /obj/item/paper/carbon))
var/obj/item/paper/carbon/C = attacking_item
if (!C.iscopy && !C.copied)
to_chat(user, SPAN_NOTICE("Take off the carbon copy first."))
add_fingerprint(user)
return
var/obj/item/paper_bundle/B = new(src.loc)
if (name != initial(name))
B.name = name
else if (attacking_item.name != initial(attacking_item.name))
B.name = attacking_item.name
user.drop_from_inventory(attacking_item,B)
//TODO: Look into this stuff
if (istype(user, /mob/living/carbon/human))
var/mob/living/carbon/human/h_user = user
if (h_user.r_hand == src)
h_user.drop_from_inventory(src)
h_user.equip_to_slot_if_possible(B, slot_r_hand)
else if (h_user.l_hand == src)
h_user.drop_from_inventory(src)
h_user.equip_to_slot_if_possible(B, slot_l_hand)
else if (h_user.l_store == src)
h_user.drop_from_inventory(src)
B.forceMove(h_user)
B.hud_layerise()
h_user.l_store = B
h_user.update_inv_pockets()
else if (h_user.r_store == src)
h_user.drop_from_inventory(src)
B.forceMove(h_user)
B.hud_layerise()
h_user.r_store = B
h_user.update_inv_pockets()
else if (h_user.head == src)
h_user.u_equip(src)
h_user.put_in_hands(B)
else if (!istype(src.loc, /turf))
src.forceMove(get_turf(h_user))
if(h_user.client) h_user.client.screen -= src
h_user.put_in_hands(B)
to_chat(user, SPAN_NOTICE("You clip the [attacking_item.name] to [(src.name == "paper") ? "the paper" : src.name]."))
src.forceMove(B)
B.pages.Add(src)
B.pages.Add(attacking_item)
B.amount = 2
B.update_icon()
else if(attacking_item.ispen())
if(icon_state == "scrap")
to_chat(user, SPAN_WARNING("The [src] is too crumpled to write on."))
return
var/obj/item/pen/robopen/RP = attacking_item
if(istype(RP) && RP.mode == RENAME_PAPER)
RP.RenamePaper(user,src)
else
var/datum/browser/paper_win = new(user, name, null, 450, 500, null, TRUE)
paper_win.set_content("<head><title>[capitalize_first_letters(name)]</title><style>body {background-color: [color];}</style></head><body>[info_links][stamps]</body>")
paper_win.add_stylesheet("paper_languages", 'html/browser/paper_languages.css')
paper_win.open()
return
else if(istype(attacking_item, /obj/item/stamp) || istype(attacking_item, /obj/item/clothing/ring/seal))
if((!in_range(src, usr) && loc != user && !( istype(loc, /obj/item/clipboard) ) && loc.loc != user && user.get_active_hand() != attacking_item))
return
stamps += (stamps=="" ? "<HR>" : "<BR>") + "<i>This paper has been stamped with the [attacking_item.name].</i>"
var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
var/x
var/y
if(istype(attacking_item, /obj/item/stamp/captain) || istype(attacking_item, /obj/item/stamp/centcomm))
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(!ico)
ico = new
ico += "paper_[attacking_item.icon_state]"
stampoverlay.icon_state = "paper_[attacking_item.icon_state]"
if(!stamped)
stamped = new
stamped += attacking_item.type
AddOverlays(stampoverlay)
playsound(src, 'sound/bureaucracy/stamp.ogg', 50, 1)
to_chat(user, SPAN_NOTICE("You stamp the paper with \the [attacking_item]."))
else if(attacking_item.isFlameSource())
burnpaper(attacking_item, user)
update_icon()
add_fingerprint(user)
return
/*
* Premade paper
*/
/obj/item/paper/Court
name = "Judgement"
info = "For crimes against the station, the offender is sentenced to:<BR>\n<BR>\n"
/obj/item/paper/crumpled
name = "paper scrap"
icon_state = "scrap"
/obj/item/paper/crumpled/update_icon()
return
/obj/item/paper/crumpled/bloody
icon_state = "scrap_bloodied"
/obj/item/paper/incident
name = "incident form receipt"
var/datum/crime_incident/incident
var/sentence = 1 // Is this form contain a sentence of guilty?
/obj/item/paper/incident/New()
var/T = parsepencode({"\[center\]\[logo_scc\]\[/center\]
\[center\]\[b\]\[i\]Encoded SCC Security Incident Report\[/b\]\[/i\]\[hr\]
\[small\]FOR USE BY <b>SCCV HORIZON</b> SECURITY ONLY\[/small\]\[br\]
\[barcode\]\[/center\]"})
info = T
..()
/obj/item/paper/sentencing
name = "Sentencing and You"
icon_state = "pamphlet"
/obj/item/paper/sentencing/New()
var/T = parsepencode(
{"\[center\]\[logo_scc_small\]\[/center\]
\[center\]\[b\]\[i\]Operation of Sentencing Consoles\[/b\]\[/i\]\[hr\]
\[small\]In compliance with SCC criminal regulations, the \[b\][station_name()]\[/b\] has been equipped with state of the art sentencing consoles. The operation of these consoles, or computers, is quite simple:
While preparing a convicted individual, remove their ID and have the console scan it.\[br\]
Next, select all applicable charges from the menu available. The console will calculate the sentence based on the minimum recommended sentence - any variables such as repeat offense will need to be manually accounted for.\[br\]
After all the charges have been applied, the processing officer is invited to add a short description of the incident, any related evidence, and any witness testimonies.\[br\]
Simply press the option "Render Guilty", and the sentence is complete! The convict's records will be automatically updated to reflect their crimes. You should now insert the printed receipt into the cell timer, and begin processing.\[br\]
\[hr\]Please note: Cell timers will \[b\]NOT\[/b\] function without a valid incident form report inserted into them."}
)
info = T
..()
/obj/item/paper/sentencing/update_icon()
return
/obj/item/paper/nka_pledge
name = "imperial volunteer Alam'ardii corps pledge"
info = "<center><b><u>Imperial Volunteer Alam'ardii Corps Pledge</u></b></center> <hr> <center><i><u>May the Gods bless his Kingdom and Dynasty</u></i></center> <hr> I, <field>, hereby declare, under a vow of loyalty and compromise, that I shall serve as a volunteer in the Imperial Volunteer Alam'ardii Corps, for the mininum duration of three years or until discharge. I accept the duty of aiding the New Kingdom of Adhomai and His Majesty, King Vahzirthaamro Azunja, in this struggle and I shall not relinquish this pledge. <hr> Volunteer Signature: <field> <hr> Recruiting Officer Stamp:"
/obj/item/paper/medscan
icon_state = "medscan"
color = "#eeffe8"
set_unsafe_on_init = TRUE
var/datum/weakref/scan_target
/obj/item/paper/medscan/Initialize(mapload, text, title, var/atom/set_scan_target)
. = ..()
scan_target = WEAKREF(set_scan_target)
/*#############################################
PERSISTENT
#############################################*/
/obj/item/paper/persistence_get_content()
var/list/content = list()
content["title"] = name
content["text"] = info
return content
/obj/item/paper/persistence_apply_content(content, x, y, z)
set_content(content["title"], content["text"])
src.x = x
src.y = y
src.z = z
for(var/obj/object in loc) // Pin to noticeboard
if(istype(object, /obj/structure/noticeboard))
var/obj/structure/noticeboard/notice_board = object
notice_board.add_papers_from_turf()
/*#############################################
FLUFF PAPERS SUBTYPE
#############################################*/
/**
* # Fluff papers
*
* Fluff papers that you can map in, used in mapping
*
* You **have** to create a subtype for the map you're using it in, and have the info/name variables set in code, **not in map** ie:
*
* ```
* /obj/item/paper/fluff/<mapname>/(<paper_name>(/)?)+
* ```
*
* This subtype will take care of updating the free space on the paper on initialization, and can be written in different languages
*/
ABSTRACT_TYPE(/obj/item/paper/fluff)
/// The language to translate the paper into, one of the `LANGUAGE_*` in `code\__DEFINES\species_languages.dm`
var/language
/obj/item/paper/fluff/Initialize(mapload, text, title)
. = ..()
if(src.language)
var/datum/language/L = GLOB.all_languages[src.language]
if(istype(L) && L.written_style) //Don't want to try and write in Hivenet or something
var/key = L.key
var/languagetext = "\[lang=[key]]"
languagetext += "[info]\[/lang]"
src.info = parsepencode(languagetext)
update_icon()
update_space(src.info)
/// Used in the bunker on the SCCV Horizon.
/obj/item/paper/fluff/bunker
name = "bunker evacuation route instructions"
desc = "A paper. It has evacuation route instructions printed on it."
info = "<font face=\"Verdana\"><center>SCCV Horizon Command Bunker<br>Evacuation Route Instructions</center><font size=\"2\"><ol><li>Put on the emergency \
welding goggles.</li><li>Grasp the emergency welding tool firmly in your hands, turn it on, and start cutting a hole in the floor.</li><li>Wait for \
the newly created hole to cool.<li>Use the emergency crowbar to pry away the metal.</li><li>Deploy the emergency ladder.</li><li>Dispose of the used \
equipment, if necessary.</li></ol></font></font>"
/// Used in the bridge on the SCCV Horizon
/obj/item/paper/fluff/bridge
name = "bridge evacuation route instructions"
desc = "A paper. It has evacuation route instructions printed on it."
info = "<font face=\"Verdana\"><center>SCCV Horizon Command <br>Evacuation Route Instructions</center><font size=\"2\"><ol><li>Put on the emergency \
welding goggles.</li><li>Grasp the emergency welding tool firmly in your hands, turn it on, and start cutting a hole in the floor.</li><li>Wait for \
the newly created hole to cool.<li>Use the emergency crowbar to pry away the metal.</li><li>Deploy the emergency ladder.</li><li>Dispose of the used \
equipment, if necessary.</li></ol></font></font>"
/// Used on the IAC ship, meant for distribution.
/obj/item/paper/fluff/iac
name = "interstellar aid corps info pamphlet"
desc = "A paper. It has an IAC logo stamped right on front of it."
info = "<font face=\"Verdana\"><center><b>The Interstellar Aid Corps Needs YOUR Help!</b></center><br>With the crisis in the Wildlands still affecting billions \
of people, now more than ever the IAC needs your help to provide relief to those worst affected by Phoron shortages, Warlord attacks, and even a lack of \
basic necessities, such as food and water.<br>The IAC and its personnel have been able to provide relief to hundreds of colonies, but this can only happen \
with your support. As such, local IAC vessels and stations are accepting donations of non-perishable foods and water, as well as medical supplies of any type. \
Additionally, at the behest of the IAC coordinator in your area of space, you can join for a blood drive or apply to volunteer in the IAC. <br> \
<center><i>Remember - it's up to all of us to look after our galaxy!</i></center></font>"