mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-07-14 08:34:16 +01:00
Complete Library Overhaul Part 1 (#17832)
* first edits * more commits yay * more changes * Update paradise.dme * changes * more edits * more edits * changes * more changes * changes * more stuff stuff Update tgui.bundle.js * FUCK * Many UI changes * Update Lib Computer UI and Correct Map Issues * squashed commits :) Guts Old Book/Manual Code and Reworks it final (almost) touches * this should pass tests now * fixes random ruin having a removed object * adds review suggestions Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com> * squashed commits :) update schema adds python requested changes implements proper python & sql scripts applies some requested changes + minor fixes bump SQL version to 40 Co-Authored-By: AffectedArc07 <25063394+AffectedArc07@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: AffectedArc07 <25063394+AffectedArc07@users.noreply.github.com> * applies requested changes from code review * lets pass some tests today more fixes * Apply suggestions from code review Co-authored-by: Farie82 <farie82@users.noreply.github.com> * more requested changes fulfilled * should fix map issues + a couple other things * fixes & sanitization stuff * Apply suggestions from code review Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com> * updates UI and implements more requested changes * mother of bug fixes & UI cleanup * removes unused code * Apply suggestions from code review Co-authored-by: Farie82 <farie82@users.noreply.github.com> * fixes * adds GC checks * Apply suggestions from code review Co-authored-by: Farie82 <farie82@users.noreply.github.com> * requested changes + TGUI Cleanup and Styling Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com> Co-authored-by: AffectedArc07 <25063394+AffectedArc07@users.noreply.github.com> Co-authored-by: Farie82 <farie82@users.noreply.github.com> Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com>
This commit is contained in:
@@ -56,8 +56,7 @@ GLOBAL_LIST_INIT(admin_verbs_admin, list(
|
||||
/client/proc/update_mob_sprite,
|
||||
/client/proc/man_up,
|
||||
/client/proc/global_man_up,
|
||||
/client/proc/delbook,
|
||||
/client/proc/view_flagged_books,
|
||||
/client/proc/library_manager,
|
||||
/client/proc/view_asays,
|
||||
/client/proc/empty_ai_core_toggle_latejoin,
|
||||
/client/proc/aooc,
|
||||
@@ -131,8 +130,7 @@ GLOBAL_LIST_INIT(admin_verbs_server, list(
|
||||
/datum/admins/proc/toggleAI,
|
||||
/client/proc/cmd_admin_delete, /*delete an instance/object/mob/etc*/
|
||||
/client/proc/cmd_debug_del_sing,
|
||||
/client/proc/delbook,
|
||||
/client/proc/view_flagged_books,
|
||||
/client/proc/library_manager,
|
||||
/client/proc/view_asays,
|
||||
/client/proc/toggle_antagHUD_use,
|
||||
/client/proc/toggle_antagHUD_restrictions,
|
||||
|
||||
@@ -3273,64 +3273,6 @@
|
||||
usr << browse("<HTML><HEAD><TITLE>Details</TITLE></HEAD><BODY><TT>[replacetext(text, "\n", "<BR>")]</TT></BODY></HTML>",
|
||||
"window=show_details;size=500x200")
|
||||
|
||||
// Library stuff
|
||||
else if(href_list["library_book_id"])
|
||||
var/isbn = text2num(href_list["library_book_id"])
|
||||
|
||||
if(href_list["view_library_book"])
|
||||
var/datum/db_query/query_view_book = SSdbcore.NewQuery("SELECT content, title FROM library WHERE id=:isbn", list(
|
||||
"isbn" = isbn
|
||||
))
|
||||
if(!query_view_book.warn_execute())
|
||||
qdel(query_view_book)
|
||||
return
|
||||
|
||||
var/content = ""
|
||||
var/title = ""
|
||||
while(query_view_book.NextRow())
|
||||
content = query_view_book.item[1]
|
||||
title = html_encode(query_view_book.item[2])
|
||||
|
||||
var/dat = "<pre><code>"
|
||||
dat += "[html_encode(html_to_pencode(content))]"
|
||||
dat += "</code></pre>"
|
||||
|
||||
var/datum/browser/popup = new(usr, "admin_view_book", "[title]", 700, 400)
|
||||
popup.set_content(dat)
|
||||
popup.open(0)
|
||||
|
||||
qdel(query_view_book)
|
||||
log_admin("[key_name(usr)] has viewed the book [isbn].")
|
||||
message_admins("[key_name_admin(usr)] has viewed the book [isbn].")
|
||||
return
|
||||
|
||||
else if(href_list["unflag_library_book"])
|
||||
var/datum/db_query/query_unflag_book = SSdbcore.NewQuery("UPDATE library SET flagged = 0 WHERE id=:isbn", list(
|
||||
"isbn" = isbn
|
||||
))
|
||||
if(!query_unflag_book.warn_execute())
|
||||
qdel(query_unflag_book)
|
||||
return
|
||||
|
||||
qdel(query_unflag_book)
|
||||
log_admin("[key_name(usr)] has unflagged the book [isbn].")
|
||||
message_admins("[key_name_admin(usr)] has unflagged the book [isbn].")
|
||||
|
||||
else if(href_list["delete_library_book"])
|
||||
var/datum/db_query/query_delbook = SSdbcore.NewQuery("DELETE FROM library WHERE id=:isbn", list(
|
||||
"isbn" = isbn
|
||||
))
|
||||
if(!query_delbook.warn_execute())
|
||||
qdel(query_delbook)
|
||||
return
|
||||
|
||||
qdel(query_delbook)
|
||||
log_admin("[key_name(usr)] has deleted the book [isbn].")
|
||||
message_admins("[key_name_admin(usr)] has deleted the book [isbn].")
|
||||
|
||||
// Refresh the page
|
||||
src.view_flagged_books()
|
||||
|
||||
else if(href_list["create_outfit_finalize"])
|
||||
if(!check_rights(R_EVENT))
|
||||
return
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/client/proc/delbook()
|
||||
set name = "Delete Book"
|
||||
set desc = "Permamently deletes a book from the database."
|
||||
set category = "Admin"
|
||||
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
|
||||
var/isbn = input("ISBN number?", "Delete Book") as num | null
|
||||
if(!isbn)
|
||||
return
|
||||
|
||||
var/datum/db_query/query_delbook = SSdbcore.NewQuery("DELETE FROM library WHERE id=:isbn", list(
|
||||
"isbn" = text2num(isbn) // just to be sure
|
||||
))
|
||||
if(!query_delbook.warn_execute())
|
||||
qdel(query_delbook)
|
||||
return
|
||||
|
||||
qdel(query_delbook)
|
||||
log_admin("[key_name(usr)] has deleted the book [isbn].")
|
||||
message_admins("[key_name_admin(usr)] has deleted the book [isbn].")
|
||||
|
||||
/client/proc/view_flagged_books()
|
||||
set name = "View Flagged Books"
|
||||
set desc = "View books flagged for content."
|
||||
set category = "Admin"
|
||||
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
|
||||
holder.view_flagged_books()
|
||||
|
||||
/datum/admins/proc/view_flagged_books()
|
||||
if(!usr.client.holder)
|
||||
return
|
||||
|
||||
var/dat = "<table><tr><th>ISBN</th><th>Title</th><th>Total Flags</th><th>Options</th></tr>"
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("SELECT id, title, flagged FROM library WHERE flagged > 0 ORDER BY flagged DESC")
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
var/books = 0
|
||||
while(query.NextRow())
|
||||
books++
|
||||
var/isbn = query.item[1]
|
||||
dat += "<tr><td>[add_zero(isbn, 4)]</td><td>[query.item[2]]</td><td>[query.item[3]]</td><td>"
|
||||
dat += "<a href='?_src_=holder;library_book_id=[isbn];view_library_book=1;'>View Content</a>"
|
||||
dat += "<a href='?_src_=holder;library_book_id=[isbn];unflag_library_book=1;'>Unflag</a>"
|
||||
dat += "<a href='?_src_=holder;library_book_id=[isbn];delete_library_book=1;'>Delete</a>"
|
||||
dat += "</td>"
|
||||
|
||||
dat += "</table>"
|
||||
qdel(query)
|
||||
|
||||
if(!books)
|
||||
dat = "<h1>No flagged books! :)</h1>"
|
||||
|
||||
var/datum/browser/popup = new(usr, "admin_view_flagged_books", "Flagged Books", 700, 400)
|
||||
popup.set_content(dat)
|
||||
popup.open(0)
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
///Max Writeable Content Pages per book, players really don't need more than this
|
||||
#define MAX_PAGES 5
|
||||
|
||||
/**
|
||||
* # Standard Book
|
||||
*
|
||||
* Game Object which stores pages of text usually written by players, has other editable information such as the book's
|
||||
* title, author, summary, and categories. Has other values that are generated when books are acquired through the library
|
||||
* computer.
|
||||
*
|
||||
* Like other User Interfaces that heavily rely on player input, using newer tools such as TGUI presents sanitization issues
|
||||
* so books will remain using BrowserUI until further notice.
|
||||
*/
|
||||
/obj/item/book
|
||||
name = "book"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "book"
|
||||
throw_speed = 1
|
||||
throw_range = 5
|
||||
force = 2
|
||||
w_class = WEIGHT_CLASS_NORMAL
|
||||
attack_verb = list("bashed", "whacked")
|
||||
resistance_flags = FLAMMABLE
|
||||
drop_sound = 'sound/items/handling/book_drop.ogg'
|
||||
pickup_sound = 'sound/items/handling/book_pickup.ogg'
|
||||
|
||||
///Title & Real name of the book
|
||||
var/title
|
||||
///Who wrote the book, can be changed by pen or PC
|
||||
var/author
|
||||
///Short summary of the contents of the book, can be changed by pen or PC
|
||||
var/summary
|
||||
///Book Rating - Assigned by library computer based on how many/how players have rated this book
|
||||
var/rating
|
||||
///Book Categories - used for differentiating types of books, set by players upon upload, viewable upon examining book
|
||||
var/categories = list()
|
||||
///The background color of the book, useful for themed programmatic books, must be in #FFFFFF hex color format
|
||||
var/book_bgcolor = "#FFF2E5"
|
||||
///Content Pages of the books, this variable is a list of strings containting the HTML + Text of each page
|
||||
var/pages = list()
|
||||
///What page is the book currently opened to? Page 0 - Intro Page | Page 1-5 - Content Pages
|
||||
var/current_page = 0
|
||||
|
||||
///Book UI Popup Height
|
||||
var/book_height = 400
|
||||
///Book UI Popup Width
|
||||
var/book_width = 400
|
||||
///Prevents book from being uploaded - For all printed books
|
||||
var/copyright = FALSE
|
||||
///Prevents book contents from being edited
|
||||
var/protected = FALSE
|
||||
///Book's id within the library system, unique to each book object, should not be declared manually
|
||||
var/libraryid
|
||||
///Indicates whether or not a books pages have been carved out
|
||||
var/carved = FALSE
|
||||
///Item that is stored inside the book
|
||||
var/obj/item/store
|
||||
|
||||
/obj/item/book/Initialize(mapload, datum/cachedbook/CB, _copyright = FALSE, _protected = FALSE)
|
||||
. = ..()
|
||||
if(!CB)
|
||||
return
|
||||
author = CB.author
|
||||
title = CB.title
|
||||
pages = CB.content
|
||||
summary = CB.summary
|
||||
categories = CB.categories
|
||||
copyright = _copyright
|
||||
protected = _protected
|
||||
rating = CB.rating
|
||||
name = "Book: [CB.title]"
|
||||
icon_state = "book[rand(1,8)]"
|
||||
|
||||
|
||||
/obj/item/book/attack(mob/M, mob/living/user)
|
||||
if(user.a_intent == INTENT_HELP)
|
||||
force = 0
|
||||
attack_verb = list("educated")
|
||||
else
|
||||
force = initial(force)
|
||||
attack_verb = list("bashed", "whacked")
|
||||
..()
|
||||
|
||||
/obj/item/book/attack_self(mob/user)
|
||||
if(carved)
|
||||
//Attempt to remove inserted object, if none found, remind user that someone vandalized their book (Bastards)!
|
||||
if(!remove_stored_item(user, TRUE))
|
||||
to_chat(user, "<span class='notice'>The pages of [title] have been cut out!</span>")
|
||||
return
|
||||
user.visible_message("<span class='notice'>[user] opens a book titled \"[title]\" and begins reading intently.</span>")
|
||||
read_book(user)
|
||||
|
||||
/obj/item/book/attackby(obj/item/I, mob/user, params)
|
||||
if(istype(I, /obj/item/pen))
|
||||
edit_book(user)
|
||||
else if(istype(I, /obj/item/barcodescanner))
|
||||
var/obj/item/barcodescanner/scanner = I
|
||||
scanner.scanBook(src, user) //abstraction and proper scoping ftw | did you know barcode scanner code used to be here?
|
||||
return
|
||||
else if(I.sharp && !carved) //don't use sharp objects on your books if you don't want to carve out all of its pages kids!
|
||||
carve_book(user, I)
|
||||
else if(store_item(I, user))
|
||||
return
|
||||
else
|
||||
..()
|
||||
|
||||
/obj/item/book/examine(mob/user)
|
||||
. = ..()
|
||||
if(isobserver(user))
|
||||
read_book(user)
|
||||
|
||||
/**
|
||||
* Internal Checker Proc
|
||||
*
|
||||
* Gives free pass to observers to read, ensures that all other mobs attempting to read book are A) literated and
|
||||
* B) are within range to actually read the book.
|
||||
*/
|
||||
/obj/item/book/proc/can_read(mob/user)
|
||||
if(isobserver(user)) //We check this first because ghosts should be able to read any book they can see no matter what
|
||||
return TRUE
|
||||
if(!in_range(src, user))
|
||||
return FALSE
|
||||
if(!user.is_literate()) //this person was 2 cool 4 school and cannot READ
|
||||
to_chat(user, "<span class='warning'>You attempt to the read the book but remember that you don't actually know how to read.</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Read Book Proc
|
||||
*
|
||||
* Checks if players is able to read book and that book is readable before calling the neccesary procs to open up UI
|
||||
*/
|
||||
/obj/item/book/proc/read_book(mob/user)
|
||||
if(!length(pages)) //You can't read a book with no pages in it
|
||||
to_chat(user, "<span class='notice'>This book is completely blank!</span>")
|
||||
return
|
||||
if(!can_read(user))
|
||||
return
|
||||
|
||||
show_content(user) //where all the magic happens
|
||||
onclose(user, "book")
|
||||
|
||||
/**
|
||||
* Show Content Proc
|
||||
*
|
||||
* Builds the browserUI html to show to the player then open up the User interface. It first build the header navigation
|
||||
* buttons and then builds the rest of the UI based on what page the player is turned to.
|
||||
*/
|
||||
/obj/item/book/proc/show_content(mob/user)
|
||||
var/dat = ""
|
||||
//First, we're going to choose/generate our header buttons for switching pages and store it in var/dat
|
||||
var/header_left = "<div style='float:left; text-align:left; width:49.9%'></div>"
|
||||
var/header_right = "<div style ='float;left; text-align:right; width:49.9%'></div>"
|
||||
if(length(pages)) //No need to have page switching buttons if there's no pages
|
||||
if(current_page < length(pages))
|
||||
header_right = "<div style='float:left; text-align:right; width:49.9%'><a href='?src=[UID()];next_page=1'>Next Page</a></div><br><hr>"
|
||||
if(current_page)
|
||||
header_left = "<div style='float:left; text-align:left; width:49.9%'><a href='?src=[UID()];prev_page=1'>Previous Page</a></div>"
|
||||
|
||||
dat += header_left + header_right
|
||||
//Now we're going to display the header buttons + the current page selected, if it's page 0, we display the cover_page instead
|
||||
if(!current_page)
|
||||
var/cover_page = {"<center><h1>[title]</h1><br></h2>Written by: [author]</h2></center><br><hr><b>Summary:</b> [summary]"}
|
||||
user << browse("<body bgcolor='[book_bgcolor]'>[dat]<br>" + "[cover_page]", "window=book[UID()];size=400x400")
|
||||
return
|
||||
else
|
||||
user << browse("<body bgcolor='[book_bgcolor]'>[dat]<br>" + "[pages[current_page]]", "window=book[UID()]")
|
||||
|
||||
/obj/item/book/Topic(href, href_list)
|
||||
if(..())
|
||||
return
|
||||
if(href_list["next_page"])
|
||||
if(current_page > length(pages)) //should never be false, but just in-case
|
||||
current_page = length(pages)
|
||||
return
|
||||
current_page++
|
||||
playsound(loc, "pageturn", 50, 1)
|
||||
read_book(usr) //scuffed but this is how you update the UI
|
||||
updateUsrDialog()
|
||||
if(href_list["prev_page"])
|
||||
if(current_page < 0) //should never be false, but just in-case
|
||||
current_page = 0
|
||||
return
|
||||
current_page--
|
||||
playsound(loc, "pageturn", 50, 1)
|
||||
read_book(usr) //scuffed but this is how you update the UI
|
||||
updateUsrDialog()
|
||||
|
||||
/**
|
||||
* Edit Book Proc
|
||||
*
|
||||
* This is where a lot of the magic happens, upon interacting with the book with a pen, this proc will open up options
|
||||
* for the player to edit the book. Most importantly, this is where we account for player stupidity and maliciousness
|
||||
* any input must strip/reject bad text and HTML from user input, additionally we account for players trying to screw
|
||||
* with the Database. This will also limit the max characters players can upload at one time to prevent spamming.
|
||||
*/
|
||||
/obj/item/book/proc/edit_book(mob/user)
|
||||
if(protected) //we don't want people touching "special" books, especially ones that use iframes
|
||||
to_chat(user, "<span class='notice'>These pages don't seem to take the ink well. Looks like you can't modify it.</span>")
|
||||
return
|
||||
var/choice = input(user, "What would you like to edit?") as null|anything in list("Title", "Edit Current Page", "Author", "Summary", "Add Page", "Remove Page")
|
||||
switch(choice)
|
||||
if("Title")
|
||||
var/newtitle = reject_bad_text(stripped_input(user, "Write a new title:"))
|
||||
if(!newtitle)
|
||||
to_chat(user, "<span class='notice'>You change your mind.</span>")
|
||||
return
|
||||
//Like with paper, the name (not title) of the book should indicate that THIS IS A BOOK when actions are performed with it
|
||||
//this is to prevent players from naming it "Nuclear Authentification Disk" or "Energy Sword" to fuck with security
|
||||
name = "Book: " + newtitle
|
||||
title = newtitle
|
||||
if("Author")
|
||||
var/newauthor = stripped_input(user, "Write the author's name:")
|
||||
if(!newauthor)
|
||||
to_chat(user, "<span class='notice'>You change your mind.</span>")
|
||||
return
|
||||
author = newauthor
|
||||
if("Summary")
|
||||
var/newsummary = strip_html(input(user, "Write the new summary:") as message|null, MAX_SUMMARY_LEN)
|
||||
if(!newsummary)
|
||||
to_chat(user, "<span class='notice'>You change your mind.</span>")
|
||||
return
|
||||
summary = newsummary
|
||||
if("Edit Current Page")
|
||||
if(carved)
|
||||
to_chat(user, "<span class='notice'>The pages of [title] have been cut out!</span>")
|
||||
return
|
||||
if(!current_page)
|
||||
to_chat(user, "<span class='notice'>You need to turn to a page before writing in the book.</span>")
|
||||
return
|
||||
var/character_space_remaining = MAX_CHARACTERS_PER_BOOKPAGE - length(pages[current_page])
|
||||
if(character_space_remaining <= 0)
|
||||
to_chat(user, "<span class='notice'>There's not enough space left on this page to write anything!</span>")
|
||||
return
|
||||
var/content = strip_html(input(user, "Add Text to this page, you have [character_space_remaining] characters of space left:") as message|null, MAX_CHARACTERS_PER_BOOKPAGE)
|
||||
if(!content)
|
||||
to_chat(user, "<span class='notice'>You change your mind.</span>")
|
||||
return
|
||||
//check if length of current text content + what player is adding is larger than our character limit
|
||||
else if((length(content) + length(pages[current_page])) > MAX_CHARACTERS_PER_BOOKPAGE)
|
||||
//if true, let's cut down the text to fit perfectly into our character limit, player is only half-pissed!
|
||||
pages[current_page] += dd_limittext(content, (MAX_CHARACTERS_PER_BOOKPAGE - length(pages[current_page])))
|
||||
else
|
||||
pages[current_page] += content
|
||||
if("Add Page")
|
||||
if(carved)
|
||||
to_chat(user, "<span class='notice'>You can't add anymore pages, the pages of [title] have been cut out and the book is ruined!</span>")
|
||||
return
|
||||
if(length(pages) >= MAX_PAGES)
|
||||
to_chat(user, "<span class='notice'>You can't fit anymore pages in this book!</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You add another page to the book!</span>")
|
||||
pages += " "
|
||||
if("Remove Page")
|
||||
if(!length(pages))
|
||||
to_chat(user, "<span class='notice'>There aren't any pages in this book!</span>")
|
||||
return
|
||||
var/page_choice = stripped_input(user, "There are [length(pages)] pages, which page number would you like to remove?")
|
||||
if(!page_choice)
|
||||
to_chat(user, "<span class='notice'>You change your mind.</span>")
|
||||
return
|
||||
if(!isnum(page_choice) || page_choice <= 0 || page_choice > length(pages))
|
||||
to_chat(user, "<span class='notice'>That is not an acceptable value.</span")
|
||||
return
|
||||
pages -= pages[page_choice]
|
||||
|
||||
/obj/item/book/proc/edit_page(content, page_number)
|
||||
if(!content)
|
||||
return
|
||||
if(length(pages) < page_number) //can't edit the page if it doesn't exist
|
||||
return
|
||||
pages += content
|
||||
|
||||
/obj/item/book/proc/add_page(content)
|
||||
var/text = content
|
||||
if(!text)
|
||||
text = " "
|
||||
if(length(pages) >= MAX_PAGES)
|
||||
return FALSE
|
||||
pages += text
|
||||
current_page = length(pages) //open to newely added page so player can edit it
|
||||
return TRUE
|
||||
|
||||
/obj/item/book/proc/remove_page(page_number)
|
||||
if(length(pages) < page_number) //can't remove the page if it doesn't exist
|
||||
return FALSE
|
||||
pages -= pages[page_number]
|
||||
page_number = length(pages) //if page_number is somehow at a value it shouldn't be we fix it here aswell
|
||||
return TRUE //we want to make sure whatever is calling this proc knows the operation was succesful
|
||||
|
||||
/obj/item/book/proc/carve_book(mob/user, obj/item/I)
|
||||
if(carved)
|
||||
to_chat(user, "<span class='warning'>[title] has already been carved out!</span>")
|
||||
return
|
||||
if(!I.sharp)
|
||||
to_chat(user, "<span class='warning'>You can't carve [title] using that!</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You begin to carve out [title].</span>")
|
||||
if(I.use_tool(src, user, 30, volume = I.tool_volume))
|
||||
user.visible_message("<span class='warning'>[user] appears to carve out the pages inside of [title]!</span>",\
|
||||
"<span class='danger'>You carve out [title]!</span>")
|
||||
carved = TRUE
|
||||
return TRUE
|
||||
|
||||
/obj/item/book/proc/store_item(obj/item/I, mob/user)
|
||||
if(!carved)
|
||||
return
|
||||
if(store)
|
||||
to_chat(user, "<span class='notice'>There is already something in [src]!</span>")
|
||||
return
|
||||
|
||||
//does it exist, if so is it an abstract item?
|
||||
if(!istype(I) || (I.flags & ABSTRACT))
|
||||
return
|
||||
if(I.flags & NODROP)
|
||||
to_chat(user, "<span class='notice'>[I] stays stuck to your hand when you try and hide it in the book!.</span>")
|
||||
return
|
||||
//Checking to make sure the item we're storing isn't larger than/equal to size of the book, prevents recursive storing aswell
|
||||
if(I.w_class >= w_class)
|
||||
to_chat(user, "<span class='notice'>[I] is to large to fit in [src].</span>")
|
||||
return
|
||||
|
||||
user.drop_item()
|
||||
I.forceMove(src)
|
||||
RegisterSignal(I, COMSIG_PARENT_QDELETING, .proc/clear_stored_item) //ensure proper GC'ing
|
||||
store = I
|
||||
to_chat(user, "<span class='notice'>You hide [I] in [name].</span>")
|
||||
return TRUE
|
||||
|
||||
///needed for proper GC'ing
|
||||
/obj/item/book/proc/clear_stored_item()
|
||||
store = null
|
||||
|
||||
/obj/item/book/proc/remove_stored_item(mob/user, display_message = TRUE)
|
||||
if(!store)
|
||||
if(display_message) //we don't wanna display this message in certain cases if there's not a user removing it
|
||||
to_chat(user, "<span class='notice'>You search [name] but there is nothing in it!</span>")
|
||||
return FALSE
|
||||
if(display_message)
|
||||
to_chat(user, "<span class='notice'>You carefully remove [store] from [name]!</span>")
|
||||
store.forceMove(get_turf(store.loc))
|
||||
clear_stored_item()
|
||||
UnregisterSignal(store, COMSIG_PARENT_QDELETING)
|
||||
|
||||
return TRUE
|
||||
|
||||
//* Book Spawners n'stuff *//
|
||||
/obj/item/book/random
|
||||
icon_state = "random_book"
|
||||
var/amount = 1
|
||||
|
||||
/obj/item/book/random/Initialize()
|
||||
..()
|
||||
var/list/books = GLOB.library_catalog.get_random_book(amount)
|
||||
for(var/datum/cachedbook/book as anything in books)
|
||||
new /obj/item/book(loc, book, TRUE, FALSE)
|
||||
return INITIALIZE_HINT_QDEL
|
||||
|
||||
/obj/item/book/random/triple
|
||||
amount = 3
|
||||
|
||||
//* Codex Gigas *//
|
||||
//This book used to have its own dm file, due to devil code removal, it is now only a cosmetic item for the time being
|
||||
//this will be its resting place until it is used for something else eventually.
|
||||
/obj/item/book/codex_gigas
|
||||
name = "\improper Codex Gigas"
|
||||
desc = "A book documenting the nature of devils, it seems whatever magic that once possessed this codex is long gone."
|
||||
icon_state = "demonomicon"
|
||||
throw_speed = 1
|
||||
throw_range = 10
|
||||
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
||||
author = "Forces beyond your comprehension"
|
||||
protected = TRUE
|
||||
title = "The codex gigas"
|
||||
copyright = TRUE
|
||||
|
||||
#undef MAX_PAGES
|
||||
@@ -1,11 +0,0 @@
|
||||
/obj/item/book/codex_gigas
|
||||
name = "\improper Codex Gigas"
|
||||
desc = "A book documenting the nature of devils."
|
||||
icon_state ="demonomicon"
|
||||
throw_speed = 1
|
||||
throw_range = 10
|
||||
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
|
||||
author = "Forces beyond your comprehension"
|
||||
unique = TRUE
|
||||
title = "The codex gigas"
|
||||
has_drm = TRUE
|
||||
@@ -1,110 +0,0 @@
|
||||
#define MAX_BOOK_FLAGS 3 // maximum number of times a book can be flagged before being removed from results
|
||||
|
||||
/obj/machinery/computer/library
|
||||
name = "visitor computer"
|
||||
anchored = 1
|
||||
density = 1
|
||||
icon_keyboard = ""
|
||||
icon_screen = "computer_on"
|
||||
var/screenstate = 0
|
||||
var/page_num = 1
|
||||
var/num_pages = 0
|
||||
var/num_results = 0
|
||||
var/datum/library_query/query = new()
|
||||
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "computer"
|
||||
|
||||
/obj/machinery/computer/library/proc/interact_check(mob/user)
|
||||
if(stat & (BROKEN | NOPOWER))
|
||||
return 1
|
||||
|
||||
if(!Adjacent(user))
|
||||
if(!issilicon(user) && !isobserver(user))
|
||||
user.unset_machine()
|
||||
user << browse(null, "window=library")
|
||||
return 1
|
||||
|
||||
user.set_machine(src)
|
||||
return 0
|
||||
|
||||
/obj/machinery/computer/library/proc/get_page(page_num)
|
||||
var/searchquery = ""
|
||||
var/where = 0
|
||||
var/list/sql_params = list()
|
||||
if(query)
|
||||
if(query.title && query.title != "")
|
||||
searchquery += " WHERE title LIKE :title"
|
||||
sql_params["title"] = "%[query.title]%"
|
||||
where = 1
|
||||
if(query.author && query.author != "")
|
||||
searchquery += " [!where ? "WHERE" : "AND"] author LIKE :author"
|
||||
sql_params["author"] = "%[query.author]%"
|
||||
where = 1
|
||||
if(query.category && query.category != "")
|
||||
searchquery += " [!where ? "WHERE" : "AND"] category LIKE :cat"
|
||||
sql_params["cat"] = "%[query.category]%"
|
||||
if(query.category == "Fiction")
|
||||
searchquery += " AND category NOT LIKE '%Non-Fiction%'"
|
||||
where = 1
|
||||
|
||||
// This one doesnt take player input directly, so it doesnt require params
|
||||
searchquery += " [!where ? "WHERE" : "AND"] flagged < [MAX_BOOK_FLAGS]"
|
||||
// This does though
|
||||
var/sql = "SELECT id, author, title, category, ckey, flagged FROM library [searchquery] LIMIT :lowerlimit, :upperlimit"
|
||||
sql_params["lowerlimit"] = text2num((page_num - 1) * LIBRARY_BOOKS_PER_PAGE)
|
||||
sql_params["upperlimit"] = LIBRARY_BOOKS_PER_PAGE
|
||||
|
||||
// Pagination
|
||||
var/datum/db_query/select_query = SSdbcore.NewQuery(sql, sql_params)
|
||||
|
||||
if(!select_query.warn_execute())
|
||||
qdel(select_query)
|
||||
return
|
||||
|
||||
var/list/results = list()
|
||||
while(select_query.NextRow())
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.LoadFromRow(list(
|
||||
"id" =select_query.item[1],
|
||||
"author" =select_query.item[2],
|
||||
"title" =select_query.item[3],
|
||||
"category"=select_query.item[4],
|
||||
"ckey" =select_query.item[5],
|
||||
"flagged" =text2num(select_query.item[6])
|
||||
))
|
||||
results += CB
|
||||
qdel(select_query)
|
||||
return results
|
||||
|
||||
/obj/machinery/computer/library/proc/get_num_results()
|
||||
var/sql = "SELECT COUNT(id) FROM library"
|
||||
|
||||
var/datum/db_query/count_query = SSdbcore.NewQuery(sql)
|
||||
if(!count_query.warn_execute())
|
||||
qdel(count_query)
|
||||
return
|
||||
|
||||
while(count_query.NextRow())
|
||||
var/value = text2num(count_query.item[1])
|
||||
qdel(count_query)
|
||||
return value
|
||||
qdel(count_query)
|
||||
return 0
|
||||
|
||||
/obj/machinery/computer/library/proc/get_pagelist()
|
||||
var/pagelist = "<div class='pages'>"
|
||||
var/start = max(1, page_num - 3)
|
||||
var/end = min(num_pages, page_num + 3)
|
||||
for(var/i = start to end)
|
||||
var/dat = "<a href='?src=[UID()];page=[i]'>[i]</a>"
|
||||
if(i == page_num)
|
||||
dat = "<font size=3><b>[dat]</b></font>"
|
||||
if(i != end)
|
||||
dat += " "
|
||||
pagelist += dat
|
||||
pagelist += "</div>"
|
||||
return pagelist
|
||||
|
||||
/obj/machinery/computer/library/proc/getBookByID(id as text)
|
||||
return GLOB.library_catalog.getBookByID(id)
|
||||
@@ -1,477 +0,0 @@
|
||||
/*
|
||||
* Library Computer
|
||||
*/
|
||||
/obj/machinery/computer/library/checkout
|
||||
name = "Check-In/Out Computer"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "computer"
|
||||
anchored = 1
|
||||
density = 1
|
||||
var/arcanecheckout = 0
|
||||
//var/screenstate = 0 // 0 - Main Menu, 1 - Inventory, 2 - Checked Out, 3 - Check Out a Book
|
||||
var/buffer_book
|
||||
var/buffer_mob
|
||||
var/upload_category = "Fiction"
|
||||
var/list/checkouts = list()
|
||||
var/list/inventory = list()
|
||||
var/checkoutperiod = 5 // In minutes
|
||||
var/obj/machinery/libraryscanner/scanner // Book scanner that will be used when uploading books to the Archive
|
||||
|
||||
var/bibledelay = 0 // LOL NO SPAM (1 minute delay) -- Doohl
|
||||
var/booklist
|
||||
|
||||
/obj/machinery/computer/library/checkout/attack_hand(mob/user as mob)
|
||||
if(..())
|
||||
return
|
||||
interact(user)
|
||||
|
||||
/obj/machinery/computer/library/checkout/interact(mob/user)
|
||||
if(interact_check(user))
|
||||
return
|
||||
|
||||
var/dat = ""
|
||||
switch(screenstate)
|
||||
if(0)
|
||||
// Main Menu
|
||||
|
||||
dat += {"<ol>
|
||||
<li><A href='?src=[UID()];switchscreen=1'>View General Inventory</A></li>
|
||||
<li><A href='?src=[UID()];switchscreen=2'>View Checked Out Inventory</A></li>
|
||||
<li><A href='?src=[UID()];switchscreen=3'>Check out a Book</A></li>
|
||||
<li><A href='?src=[UID()];switchscreen=4'>Connect to External Archive</A></li>
|
||||
<li><A href='?src=[UID()];switchscreen=5'>Upload New Title to Archive</A></li>
|
||||
<li><A href='?src=[UID()];switchscreen=6'>Print a Bible</A></li>
|
||||
<li><A href='?src=[UID()];switchscreen=7'>Print a Manual</A></li>"}
|
||||
if(src.emagged)
|
||||
dat += "<li><A href='?src=[UID()];switchscreen=8'>Access the Forbidden Lore Vault</A></li>"
|
||||
dat += "</ol>"
|
||||
|
||||
if(src.arcanecheckout)
|
||||
new /obj/item/melee/cultblade/dagger(src.loc)
|
||||
to_chat(user, "<span class='warning'>Your sanity barely endures the seconds spent in the vault's browsing window. The only thing to remind you of this when you stop browsing is a strange looking dagger sitting on the desk. You don't really remember where it came from.</span>")
|
||||
user.visible_message("[user] stares at the blank screen for a few moments, [user.p_their()] expression frozen in fear. When [user.p_they()] finally awaken[user.p_s()] from it, [user.p_they()] look[user.p_s()] a lot older.", 2)
|
||||
src.arcanecheckout = 0
|
||||
if(1)
|
||||
// Inventory
|
||||
dat += "<h3>Inventory</h3>"
|
||||
for(var/obj/item/book/b in inventory)
|
||||
dat += "[b.name] <A href='?src=[UID()];delbook=\ref[b]'>(Delete)</A><BR>"
|
||||
dat += "<A href='?src=[UID()];switchscreen=0'>(Return to main menu)</A><BR>"
|
||||
if(2)
|
||||
// Checked Out
|
||||
dat += "<h3>Checked Out Books</h3><BR>"
|
||||
for(var/datum/borrowbook/b in checkouts)
|
||||
var/timetaken = world.time - b.getdate
|
||||
//timetaken *= 10
|
||||
timetaken /= 600
|
||||
timetaken = round(timetaken)
|
||||
var/timedue = b.duedate - world.time
|
||||
//timedue *= 10
|
||||
timedue /= 600
|
||||
if(timedue <= 0)
|
||||
timedue = "<font color=red><b>(OVERDUE)</b> [timedue]</font>"
|
||||
else
|
||||
timedue = round(timedue)
|
||||
|
||||
dat += {"\"[b.bookname]\", Checked out to: [b.mobname]<BR>--- Taken: [timetaken] minutes ago, Due: in [timedue] minutes<BR>
|
||||
<A href='?src=[UID()];checkin=\ref[b]'>(Check In)</A><BR><BR>"}
|
||||
dat += "<A href='?src=[UID()];switchscreen=0'>(Return to main menu)</A><BR>"
|
||||
if(3)
|
||||
// Check Out a Book
|
||||
|
||||
dat += {"<h3>Check Out a Book</h3><BR>
|
||||
Book: [src.buffer_book]
|
||||
<A href='?src=[UID()];editbook=1'>\[Edit\]</A><BR>
|
||||
Recipient: [src.buffer_mob]
|
||||
<A href='?src=[UID()];editmob=1'>\[Edit\]</A><BR>
|
||||
Checkout Date : [world.time/600]<BR>
|
||||
Due Date: [(world.time + checkoutperiod)/600]<BR>
|
||||
(Checkout Period: [checkoutperiod] minutes) (<A href='?src=[UID()];increasetime=1'>+</A>/<A href='?src=[UID()];decreasetime=1'>-</A>)
|
||||
<A href='?src=[UID()];checkout=1'>(Commit Entry)</A><BR>
|
||||
<A href='?src=[UID()];switchscreen=0'>(Return to main menu)</A><BR>"}
|
||||
if(4)
|
||||
dat += "<h3>External Archive</h3>"
|
||||
if(!SSdbcore.IsConnected())
|
||||
dat += "<font color=red><b>ERROR</b>: Unable to contact External Archive. Please contact your system administrator for assistance.</font>"
|
||||
else
|
||||
num_results = src.get_num_results()
|
||||
num_pages = CEILING(num_results/LIBRARY_BOOKS_PER_PAGE, 1)
|
||||
dat += {"<ul>
|
||||
<li><A href='?src=[UID()];id=-1'>(Order book by SS<sup>13</sup>BN)</A></li>
|
||||
</ul>"}
|
||||
var/pagelist = get_pagelist()
|
||||
|
||||
dat += {"<h2>Search Settings</h2><br />
|
||||
<A href='?src=[UID()];settitle=1'>Filter by Title: [query.title]</A><br />
|
||||
<A href='?src=[UID()];setcategory=1'>Filter by Category: [query.category]</A><br />
|
||||
<A href='?src=[UID()];setauthor=1'>Filter by Author: [query.author]</A><br />
|
||||
<A href='?src=[UID()];search=1'>\[Start Search\]</A><br />"}
|
||||
dat += pagelist
|
||||
|
||||
dat += {"<form name='pagenum' action='?src=[UID()]' method='get'>
|
||||
<input type='hidden' name='src' value='[UID()]'>
|
||||
<input type='text' name='pagenum' value='[page_num]' maxlength="5" size="5">
|
||||
<input type='submit' value='Jump To Page'>
|
||||
</form>"}
|
||||
|
||||
dat += {"<table border=\"0\">
|
||||
<tr>
|
||||
<td>Author</td>
|
||||
<td>Title</td>
|
||||
<td>Category</td>
|
||||
<td>Controls</td>
|
||||
</tr>"}
|
||||
|
||||
for(var/datum/cachedbook/CB in get_page(page_num))
|
||||
var/author = CB.author
|
||||
var/controls = "<A href='?src=[UID()];id=[CB.id]'>\[Order\]</A>"
|
||||
controls += {" <A href="?src=[UID()];flag=[CB.id]">\[Flag[CB.flagged ? "ged" : ""]\]</A>"}
|
||||
if(check_rights(R_ADMIN, 0, user = user))
|
||||
controls += " <A style='color:red' href='?src=[UID()];del=[CB.id]'>\[Delete\]</A>"
|
||||
author += " (<A style='color:red' href='?src=[UID()];delbyckey=[ckey(CB.ckey)]'>[ckey(CB.ckey)])</A>)"
|
||||
dat += {"<tr>
|
||||
<td>[author]</td>
|
||||
<td>[CB.title]</td>
|
||||
<td>[CB.category]</td>
|
||||
<td>[controls]</td>
|
||||
</tr>"}
|
||||
|
||||
dat += "</table><br />[pagelist]"
|
||||
|
||||
dat += "<br /><A href='?src=[UID()];switchscreen=0'>(Return to main menu)</A><BR>"
|
||||
if(5)
|
||||
dat += "<h3>Upload a New Title</h3>"
|
||||
if(!scanner)
|
||||
for(var/obj/machinery/libraryscanner/S in range(9))
|
||||
scanner = S
|
||||
break
|
||||
if(!scanner)
|
||||
dat += "<FONT color=red>No scanner found within wireless network range.</FONT><BR>"
|
||||
else if(!scanner.cache)
|
||||
dat += "<FONT color=red>No data found in scanner memory.</FONT><BR>"
|
||||
else
|
||||
|
||||
dat += {"<TT>Data marked for upload...</TT><BR>
|
||||
<TT>Title: </TT>[scanner.cache.name]<BR>"}
|
||||
if(!scanner.cache.author)
|
||||
scanner.cache.author = "Anonymous"
|
||||
|
||||
dat += {"<TT>Author: </TT><A href='?src=[UID()];uploadauthor=1'>[scanner.cache.author]</A><BR>
|
||||
<TT>Category: </TT><A href='?src=[UID()];uploadcategory=1'>[upload_category]</A><BR>
|
||||
<A href='?src=[UID()];upload=1'>\[Upload\]</A><BR>"}
|
||||
dat += "<A href='?src=[UID()];switchscreen=0'>(Return to main menu)</A><BR>"
|
||||
if(7)
|
||||
dat += "<H3>Print a Manual</H3>"
|
||||
dat += "<table>"
|
||||
|
||||
var/list/forbidden = list(
|
||||
/obj/item/book/manual/random
|
||||
)
|
||||
|
||||
if(!emagged)
|
||||
forbidden |= /obj/item/book/manual/nuclear
|
||||
|
||||
var/manualcount = 1
|
||||
var/obj/item/book/manual/M = null
|
||||
|
||||
for(var/manual_type in subtypesof(/obj/item/book/manual))
|
||||
if(!(manual_type in forbidden))
|
||||
M = new manual_type()
|
||||
dat += "<tr><td><A href='?src=[UID()];manual=[manualcount]'>[M.title]</A></td></tr>"
|
||||
QDEL_NULL(M)
|
||||
manualcount++
|
||||
dat += "</table>"
|
||||
dat += "<BR><A href='?src=[UID()];switchscreen=0'>(Return to main menu)</A><BR>"
|
||||
|
||||
if(8)
|
||||
|
||||
dat += {"<h3>Accessing Forbidden Lore Vault v 1.3</h3>
|
||||
Are you absolutely sure you want to proceed? EldritchArtifacts Inc. takes no responsibilities for loss of sanity resulting from this action.<p>
|
||||
<A href='?src=[UID()];arccheckout=1'>Yes.</A><BR>
|
||||
<A href='?src=[UID()];switchscreen=0'>No.</A><BR>"}
|
||||
|
||||
var/datum/browser/B = new /datum/browser(user, "library", "Book Inventory Management")
|
||||
B.set_content(dat)
|
||||
B.open()
|
||||
|
||||
/obj/machinery/computer/library/checkout/emag_act(mob/user)
|
||||
if(density && !emagged)
|
||||
emagged = 1
|
||||
to_chat(user, "<span class='notice'>You override the library computer's printing restrictions.</span>")
|
||||
|
||||
/obj/machinery/computer/library/checkout/attackby(obj/item/W as obj, mob/user as mob)
|
||||
if(default_unfasten_wrench(user, W))
|
||||
power_change()
|
||||
return
|
||||
if(istype(W, /obj/item/barcodescanner))
|
||||
var/obj/item/barcodescanner/scanner = W
|
||||
scanner.computer = src
|
||||
to_chat(user, "[scanner]'s associated machine has been set to [src].")
|
||||
audible_message("[src] lets out a low, short blip.", hearing_distance = 2)
|
||||
return 1
|
||||
else
|
||||
return ..()
|
||||
|
||||
/obj/machinery/computer/library/checkout/Topic(href, href_list)
|
||||
if(..())
|
||||
usr << browse(null, "window=library")
|
||||
onclose(usr, "library")
|
||||
return 1
|
||||
|
||||
if(href_list["pagenum"])
|
||||
if(!num_pages)
|
||||
page_num = 1
|
||||
else
|
||||
var/pn = text2num(href_list["pagenum"])
|
||||
if(!isnull(pn))
|
||||
page_num = clamp(pn, 1, num_pages)
|
||||
|
||||
if(href_list["page"])
|
||||
if(num_pages == 0)
|
||||
page_num = 1
|
||||
else
|
||||
page_num = clamp(text2num(href_list["page"]), 1, num_pages)
|
||||
if(href_list["settitle"])
|
||||
var/newtitle = input("Enter a title to search for:") as text|null
|
||||
if(newtitle)
|
||||
query.title = sanitize(newtitle)
|
||||
else
|
||||
query.title = null
|
||||
if(href_list["setcategory"])
|
||||
var/newcategory = input("Choose a category to search for:") in (list("Any") + GLOB.library_section_names)
|
||||
if(newcategory == "Any")
|
||||
query.category = null
|
||||
else if(newcategory)
|
||||
query.category = sanitize(newcategory)
|
||||
if(href_list["setauthor"])
|
||||
var/newauthor = input("Enter an author to search for:") as text|null
|
||||
if(newauthor)
|
||||
query.author = sanitize(newauthor)
|
||||
else
|
||||
query.author = null
|
||||
|
||||
if(href_list["search"])
|
||||
num_results = src.get_num_results()
|
||||
num_pages = CEILING(num_results/LIBRARY_BOOKS_PER_PAGE, 1)
|
||||
page_num = 1
|
||||
|
||||
screenstate = 4
|
||||
if(href_list["del"])
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
var/datum/cachedbook/target = getBookByID(href_list["del"]) // Sanitized in getBookByID
|
||||
var/ans = alert(usr, "Are you sure you wish to delete \"[target.title]\", by [target.author]? This cannot be undone.", "Library System", "Yes", "No")
|
||||
if(ans=="Yes")
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("DELETE FROM library WHERE id=:id", list(
|
||||
"id" = text2num(target.id)
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
qdel(query)
|
||||
log_admin("LIBRARY: [key_name(usr)] has deleted \"[target.title]\", by [target.author] ([target.ckey])!")
|
||||
message_admins("[key_name_admin(usr)] has deleted \"[target.title]\", by [target.author] ([target.ckey])!")
|
||||
src.updateUsrDialog()
|
||||
return
|
||||
|
||||
if(href_list["delbyckey"])
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
var/tckey = ckey(href_list["delbyckey"])
|
||||
var/ans = alert(usr,"Are you sure you wish to delete all books by [tckey]? This cannot be undone.", "Library System", "Yes", "No")
|
||||
if(ans=="Yes")
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("DELETE FROM library WHERE ckey=:ckey", list(
|
||||
"ckey" = tckey
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
if(query.affected == 0)
|
||||
to_chat(usr, "<span class='danger'>Unable to find any matching rows.</span>")
|
||||
qdel(query)
|
||||
return
|
||||
qdel(query)
|
||||
log_admin("LIBRARY: [key_name(usr)] has deleted [query.affected] books written by [tckey]!")
|
||||
message_admins("[key_name_admin(usr)] has deleted [query.affected] books written by [tckey]!")
|
||||
src.updateUsrDialog()
|
||||
return
|
||||
|
||||
if(href_list["flag"])
|
||||
if(!SSdbcore.IsConnected())
|
||||
alert("Connection to Archive has been severed. Aborting.")
|
||||
return
|
||||
var/id = href_list["flag"]
|
||||
if(id)
|
||||
var/datum/cachedbook/B = getBookByID(id)
|
||||
if(B)
|
||||
if((input(usr, "Are you sure you want to flag [B.title] as having inappropriate content?", "Flag Book #[B.id]") in list("Yes", "No")) == "Yes")
|
||||
GLOB.library_catalog.flag_book_by_id(usr, id)
|
||||
|
||||
if(href_list["switchscreen"])
|
||||
switch(href_list["switchscreen"])
|
||||
if("0")
|
||||
screenstate = 0
|
||||
if("1")
|
||||
screenstate = 1
|
||||
if("2")
|
||||
screenstate = 2
|
||||
if("3")
|
||||
screenstate = 3
|
||||
if("4")
|
||||
screenstate = 4
|
||||
if("5")
|
||||
screenstate = 5
|
||||
if("6")
|
||||
if(!bibledelay)
|
||||
|
||||
var/obj/item/storage/bible/B = new /obj/item/storage/bible(src.loc)
|
||||
if(SSticker && ( SSticker.Bible_icon_state && SSticker.Bible_item_state) )
|
||||
B.icon_state = SSticker.Bible_icon_state
|
||||
B.item_state = SSticker.Bible_item_state
|
||||
B.name = SSticker.Bible_name
|
||||
B.deity_name = SSticker.Bible_deity_name
|
||||
|
||||
bibledelay = 1
|
||||
spawn(60)
|
||||
bibledelay = 0
|
||||
|
||||
else
|
||||
visible_message("<b>[src]</b>'s monitor flashes, \"Bible printer currently unavailable, please wait a moment.\"")
|
||||
|
||||
if("7")
|
||||
screenstate = 7
|
||||
if("8")
|
||||
screenstate = 8
|
||||
if(href_list["arccheckout"])
|
||||
if(src.emagged)
|
||||
src.arcanecheckout = 1
|
||||
src.screenstate = 0
|
||||
if(href_list["increasetime"])
|
||||
checkoutperiod += 1
|
||||
if(href_list["decreasetime"])
|
||||
checkoutperiod -= 1
|
||||
if(checkoutperiod < 1)
|
||||
checkoutperiod = 1
|
||||
if(href_list["editbook"])
|
||||
buffer_book = copytext(sanitize(input("Enter the book's title:") as text|null),1,MAX_MESSAGE_LEN)
|
||||
if(href_list["editmob"])
|
||||
buffer_mob = copytext(sanitize(input("Enter the recipient's name:") as text|null),1,MAX_NAME_LEN)
|
||||
if(href_list["checkout"])
|
||||
var/datum/borrowbook/b = new /datum/borrowbook
|
||||
b.bookname = sanitize(buffer_book)
|
||||
b.mobname = sanitize(buffer_mob)
|
||||
b.getdate = world.time
|
||||
b.duedate = world.time + (checkoutperiod * 600)
|
||||
checkouts.Add(b)
|
||||
if(href_list["checkin"])
|
||||
var/datum/borrowbook/b = locate(href_list["checkin"])
|
||||
checkouts.Remove(b)
|
||||
if(href_list["delbook"])
|
||||
var/obj/item/book/b = locate(href_list["delbook"])
|
||||
inventory.Remove(b)
|
||||
if(href_list["uploadauthor"])
|
||||
var/newauthor = copytext(sanitize(input("Enter the author's name: ") as text|null),1,MAX_MESSAGE_LEN)
|
||||
if(newauthor && scanner)
|
||||
scanner.cache.author = newauthor
|
||||
if(href_list["uploadcategory"])
|
||||
var/newcategory = input("Choose a category: ") in list("Fiction", "Non-Fiction", "Adult", "Reference", "Religion")
|
||||
if(newcategory)
|
||||
upload_category = newcategory
|
||||
if(href_list["upload"])
|
||||
if(scanner)
|
||||
if(scanner.cache)
|
||||
var/choice = input("Are you certain you wish to upload this title to the Archive?") in list("Confirm", "Abort")
|
||||
if(choice == "Confirm")
|
||||
if(!SSdbcore.IsConnected())
|
||||
alert("Connection to Archive has been severed. Aborting.")
|
||||
else
|
||||
var/datum/db_query/query = SSdbcore.NewQuery({"
|
||||
INSERT INTO library (author, title, content, category, ckey, flagged)
|
||||
VALUES (:author, :title, :content, :category, :ckey, 0)"}, list(
|
||||
"author" = scanner.cache.author,
|
||||
"title" = scanner.cache.name,
|
||||
"content" = scanner.cache.dat,
|
||||
"category" = upload_category,
|
||||
"ckey" = usr.ckey
|
||||
))
|
||||
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
qdel(query)
|
||||
log_admin("[usr.name]/[usr.key] has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] characters in length")
|
||||
message_admins("[key_name_admin(usr)] has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] characters in length")
|
||||
|
||||
if(href_list["id"])
|
||||
if(href_list["id"]=="-1")
|
||||
href_list["id"] = input("Enter your order:") as null|num
|
||||
if(!href_list["id"])
|
||||
return
|
||||
|
||||
if(!SSdbcore.IsConnected())
|
||||
alert("Connection to Archive has been severed. Aborting.")
|
||||
return
|
||||
|
||||
var/datum/cachedbook/newbook = getBookByID(href_list["id"]) // Sanitized in getBookByID
|
||||
if(!newbook)
|
||||
alert("No book found")
|
||||
return
|
||||
if((newbook.forbidden == 2 && !emagged) || newbook.forbidden == 1)
|
||||
alert("This book is forbidden and cannot be printed.")
|
||||
return
|
||||
|
||||
if(bibledelay)
|
||||
visible_message("<b>[src]</b>'s monitor flashes, \"Printer unavailable. Please allow a short time before attempting to print.\"")
|
||||
else
|
||||
bibledelay = 1
|
||||
spawn(60)
|
||||
bibledelay = 0
|
||||
make_external_book(newbook)
|
||||
if(href_list["manual"])
|
||||
if(!href_list["manual"]) return
|
||||
var/bookid = href_list["manual"]
|
||||
|
||||
if(!SSdbcore.IsConnected())
|
||||
alert("Connection to Archive has been severed. Aborting.")
|
||||
return
|
||||
|
||||
var/datum/cachedbook/newbook = getBookByID("M[bookid]")
|
||||
if(!newbook)
|
||||
alert("No book found")
|
||||
return
|
||||
if((newbook.forbidden == 2 && !emagged) || newbook.forbidden == 1)
|
||||
alert("This book is forbidden and cannot be printed.")
|
||||
return
|
||||
|
||||
if(bibledelay)
|
||||
for(var/mob/V in hearers(src))
|
||||
V.show_message("<b>[src]</b>'s monitor flashes, \"Printer unavailable. Please allow a short time before attempting to print.\"")
|
||||
else
|
||||
bibledelay = 1
|
||||
spawn(60)
|
||||
bibledelay = 0
|
||||
make_external_book(newbook)
|
||||
|
||||
src.add_fingerprint(usr)
|
||||
src.updateUsrDialog()
|
||||
return
|
||||
|
||||
/*
|
||||
* Library Scanner
|
||||
*/
|
||||
|
||||
/obj/machinery/computer/library/checkout/proc/make_external_book(datum/cachedbook/newbook)
|
||||
if(!newbook || !newbook.id)
|
||||
return
|
||||
var/obj/item/book/B = new newbook.path(loc)
|
||||
|
||||
if(!newbook.programmatic)
|
||||
B.name = "Book: [newbook.title]"
|
||||
B.title = newbook.title
|
||||
B.author = newbook.author
|
||||
B.dat = newbook.content
|
||||
B.icon_state = "book[rand(1,16)]"
|
||||
B.has_drm = TRUE
|
||||
visible_message("[src]'s printer hums as it produces a completely bound book. How did it do that?")
|
||||
@@ -1,127 +0,0 @@
|
||||
/obj/machinery/computer/library/public
|
||||
name = "visitor computer"
|
||||
|
||||
/obj/machinery/computer/library/public/attack_hand(mob/user as mob)
|
||||
if(..())
|
||||
return
|
||||
interact(user)
|
||||
|
||||
/obj/machinery/computer/library/public/attackby(obj/item/W as obj, mob/user as mob)
|
||||
if(default_unfasten_wrench(user, W))
|
||||
power_change()
|
||||
return
|
||||
return ..()
|
||||
|
||||
/obj/machinery/computer/library/public/interact(mob/user)
|
||||
if(interact_check(user))
|
||||
return
|
||||
|
||||
var/dat = ""
|
||||
switch(screenstate)
|
||||
if(0)
|
||||
|
||||
dat += {"<h2>Search Settings</h2><br />
|
||||
<A href='?src=[UID()];settitle=1'>Filter by Title: [query.title]</A><br />
|
||||
<A href='?src=[UID()];setcategory=1'>Filter by Category: [query.category]</A><br />
|
||||
<A href='?src=[UID()];setauthor=1'>Filter by Author: [query.author]</A><br />
|
||||
<A href='?src=[UID()];search=1'>\[Start Search\]</A><br />"}
|
||||
if(1)
|
||||
if(!SSdbcore.IsConnected())
|
||||
dat += "<font color=red><b>ERROR</b>: Unable to contact External Archive. Please contact your system administrator for assistance.</font><br />"
|
||||
else if(num_results == 0)
|
||||
dat += "<em>No results found.</em>"
|
||||
else
|
||||
var/pagelist = get_pagelist()
|
||||
|
||||
dat += pagelist
|
||||
dat += {"<form name='pagenum' action='?src=[UID()]' method='get'>
|
||||
<input type='hidden' name='src' value='[UID()]'>
|
||||
<input type='text' name='pagenum' value='[page_num]' maxlength="5" size="5">
|
||||
<input type='submit' value='Jump To Page'>
|
||||
</form>"}
|
||||
dat += {"<table border=\"0\">
|
||||
<tr>
|
||||
<td>Author</td>
|
||||
<td>Title</td>
|
||||
<td>Category</td>
|
||||
<td>SS<sup>13</sup>BN</td>
|
||||
<td>Controls</td>
|
||||
</tr>"}
|
||||
for(var/datum/cachedbook/CB in get_page(page_num))
|
||||
dat += {"<tr>
|
||||
<td>[CB.author]</td>
|
||||
<td>[CB.title]</td>
|
||||
<td>[CB.category]</td>
|
||||
<td>[CB.id]</td>
|
||||
<td><A href="?src=[UID()];flag=[CB.id]">\[Flag[CB.flagged ? "ged" : ""]\]</A></td>
|
||||
</tr>"}
|
||||
|
||||
dat += "</table><br />[pagelist]"
|
||||
dat += "<A href='?src=[UID()];back=1'>\[Go Back\]</A><br />"
|
||||
var/datum/browser/B = new /datum/browser(user, "library", "Library Visitor")
|
||||
B.set_content(dat)
|
||||
B.open()
|
||||
|
||||
/obj/machinery/computer/library/public/Topic(href, href_list)
|
||||
if(..())
|
||||
usr << browse(null, "window=publiclibrary")
|
||||
onclose(usr, "publiclibrary")
|
||||
return
|
||||
|
||||
if(href_list["pagenum"])
|
||||
if(!num_pages)
|
||||
page_num = 1
|
||||
else
|
||||
var/pn = text2num(href_list["pagenum"])
|
||||
if(!isnull(pn))
|
||||
page_num = clamp(pn, 1, num_pages)
|
||||
|
||||
if(href_list["settitle"])
|
||||
var/newtitle = input("Enter a title to search for:") as text|null
|
||||
if(newtitle)
|
||||
query.title = sanitize(newtitle)
|
||||
else
|
||||
query.title = null
|
||||
if(href_list["setcategory"])
|
||||
var/newcategory = input("Choose a category to search for:") in (list("Any") + GLOB.library_section_names)
|
||||
if(newcategory == "Any")
|
||||
query.category = null
|
||||
else if(newcategory)
|
||||
query.category = sanitize(newcategory)
|
||||
if(href_list["setauthor"])
|
||||
var/newauthor = input("Enter an author to search for:") as text|null
|
||||
if(newauthor)
|
||||
query.author = sanitize(newauthor)
|
||||
else
|
||||
query.author = null
|
||||
|
||||
if(href_list["page"])
|
||||
if(num_pages == 0)
|
||||
page_num = 1
|
||||
else
|
||||
page_num = clamp(text2num(href_list["page"]), 1, num_pages)
|
||||
|
||||
if(href_list["search"])
|
||||
num_results = src.get_num_results()
|
||||
num_pages = CEILING(num_results/LIBRARY_BOOKS_PER_PAGE, 1)
|
||||
page_num = 1
|
||||
|
||||
screenstate = 1
|
||||
|
||||
if(href_list["back"])
|
||||
screenstate = 0
|
||||
|
||||
if(href_list["flag"])
|
||||
if(!SSdbcore.IsConnected())
|
||||
alert("Connection to Archive has been severed. Aborting.")
|
||||
return
|
||||
var/id = href_list["flag"]
|
||||
if(id)
|
||||
var/datum/cachedbook/B = getBookByID(id)
|
||||
if(B)
|
||||
if((input(usr, "Are you sure you want to flag [B.title] as having inappropriate content?", "Flag Book #[B.id]") in list("Yes", "No")) == "Yes")
|
||||
GLOB.library_catalog.flag_book_by_id(usr, id)
|
||||
|
||||
add_fingerprint(usr)
|
||||
updateUsrDialog()
|
||||
return
|
||||
@@ -1,327 +0,0 @@
|
||||
/* Library Items
|
||||
*
|
||||
* Contains:
|
||||
* Bookcase
|
||||
* Book
|
||||
* Barcode Scanner
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Bookcase
|
||||
*/
|
||||
|
||||
/obj/structure/bookcase
|
||||
name = "bookcase"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "book-0"
|
||||
anchored = 1
|
||||
density = 1
|
||||
opacity = 1
|
||||
resistance_flags = FLAMMABLE
|
||||
max_integrity = 200
|
||||
armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 0, RAD = 0, FIRE = 50, ACID = 0)
|
||||
var/tmp/busy = 0
|
||||
var/list/allowed_books = list(/obj/item/book, /obj/item/spellbook, /obj/item/storage/bible, /obj/item/tome) //Things allowed in the bookcase
|
||||
|
||||
/obj/structure/bookcase/Initialize()
|
||||
..()
|
||||
for(var/obj/item/I in loc)
|
||||
if(is_type_in_list(I, allowed_books))
|
||||
I.forceMove(src)
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/attackby(obj/item/O as obj, mob/user as mob, params)
|
||||
if(busy) //So that you can't mess with it while deconstructing
|
||||
return TRUE
|
||||
if(is_type_in_list(O, allowed_books))
|
||||
if(!user.drop_item())
|
||||
return
|
||||
O.forceMove(src)
|
||||
update_icon()
|
||||
return TRUE
|
||||
else if(istype(O, /obj/item/storage/bag/books))
|
||||
var/obj/item/storage/bag/books/B = O
|
||||
for(var/obj/item/T in B.contents)
|
||||
if(istype(T, /obj/item/book) || istype(T, /obj/item/spellbook) || istype(T, /obj/item/tome) || istype(T, /obj/item/storage/bible))
|
||||
B.remove_from_storage(T, src)
|
||||
to_chat(user, "<span class='notice'>You empty [O] into [src].</span>")
|
||||
update_icon()
|
||||
return TRUE
|
||||
else if(istype(O, /obj/item/wrench))
|
||||
user.visible_message("<span class='warning'>[user] starts disassembling \the [src].</span>", \
|
||||
"<span class='notice'>You start disassembling \the [src].</span>")
|
||||
playsound(get_turf(src), O.usesound, 50, 1)
|
||||
busy = TRUE
|
||||
|
||||
if(do_after(user, 50 * O.toolspeed, target = src))
|
||||
playsound(get_turf(src), O.usesound, 75, 1)
|
||||
user.visible_message("<span class='warning'>[user] disassembles \the [src].</span>", \
|
||||
"<span class='notice'>You disassemble \the [src].</span>")
|
||||
busy = FALSE
|
||||
density = 0
|
||||
deconstruct(TRUE)
|
||||
else
|
||||
busy = FALSE
|
||||
return TRUE
|
||||
else if(istype(O, /obj/item/pen))
|
||||
rename_interactive(user, O)
|
||||
return TRUE
|
||||
else
|
||||
return ..()
|
||||
|
||||
/obj/structure/bookcase/attack_hand(mob/living/user)
|
||||
if(contents.len)
|
||||
var/obj/item/book/choice = input("Which book would you like to remove from [src]?") as null|anything in contents
|
||||
if(choice)
|
||||
if(user.incapacitated() || IS_HORIZONTAL(user) || !Adjacent(user))
|
||||
return
|
||||
if(!user.get_active_hand())
|
||||
user.put_in_hands(choice)
|
||||
else
|
||||
choice.forceMove(get_turf(src))
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/deconstruct(disassembled = TRUE)
|
||||
new /obj/item/stack/sheet/wood(loc, 5)
|
||||
for(var/obj/item/I in contents)
|
||||
if(is_type_in_list(I, allowed_books))
|
||||
I.forceMove(get_turf(src))
|
||||
qdel(src)
|
||||
|
||||
/obj/structure/bookcase/update_icon()
|
||||
if(contents.len < 5)
|
||||
icon_state = "book-[contents.len]"
|
||||
else
|
||||
icon_state = "book-5"
|
||||
|
||||
|
||||
/obj/structure/bookcase/manuals/medical
|
||||
name = "Medical Manuals bookcase"
|
||||
|
||||
/obj/structure/bookcase/manuals/medical/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/medical_cloning(src)
|
||||
update_icon()
|
||||
|
||||
|
||||
/obj/structure/bookcase/manuals/engineering
|
||||
name = "Engineering Manuals bookcase"
|
||||
|
||||
/obj/structure/bookcase/manuals/engineering/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/engineering_construction(src)
|
||||
new /obj/item/book/manual/engineering_particle_accelerator(src)
|
||||
new /obj/item/book/manual/engineering_hacking(src)
|
||||
new /obj/item/book/manual/engineering_guide(src)
|
||||
new /obj/item/book/manual/engineering_singularity_safety(src)
|
||||
new /obj/item/book/manual/robotics_cyborgs(src)
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/manuals/research_and_development
|
||||
name = "R&D Manuals bookcase"
|
||||
|
||||
/obj/structure/bookcase/manuals/research_and_development/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/research_and_development(src)
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/sop
|
||||
name = "bookcase (Standard Operating Procedures)"
|
||||
|
||||
/obj/structure/bookcase/sop/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/sop_command(src)
|
||||
new /obj/item/book/manual/sop_engineering(src)
|
||||
new /obj/item/book/manual/sop_general(src)
|
||||
new /obj/item/book/manual/sop_legal(src)
|
||||
new /obj/item/book/manual/sop_medical(src)
|
||||
new /obj/item/book/manual/sop_science(src)
|
||||
new /obj/item/book/manual/sop_security(src)
|
||||
new /obj/item/book/manual/sop_service(src)
|
||||
new /obj/item/book/manual/sop_supply(src)
|
||||
update_icon()
|
||||
|
||||
/*
|
||||
* Book
|
||||
*/
|
||||
/obj/item/book
|
||||
name = "book"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state ="book"
|
||||
throw_speed = 1
|
||||
throw_range = 5
|
||||
force = 2
|
||||
w_class = WEIGHT_CLASS_NORMAL //upped to three because books are, y'know, pretty big. (and you could hide them inside eachother recursively forever)
|
||||
attack_verb = list("bashed", "whacked")
|
||||
resistance_flags = FLAMMABLE
|
||||
drop_sound = 'sound/items/handling/book_drop.ogg'
|
||||
pickup_sound = 'sound/items/handling/book_pickup.ogg'
|
||||
var/dat // Actual page content
|
||||
var/due_date = 0 // Game time in 1/10th seconds
|
||||
var/author // Who wrote the thing, can be changed by pen or PC. It is not automatically assigned
|
||||
var/unique = 0 // 0 - Normal book, 1 - Should not be treated as normal book, unable to be copied, unable to be modified
|
||||
var/title // The real name of the book.
|
||||
var/carved = 0 // Has the book been hollowed out for use as a secret storage item?
|
||||
var/forbidden = 0 // Prevent ordering of this book. (0=no, 1=yes, 2=emag only)
|
||||
var/obj/item/store // What's in the book?
|
||||
/// Book DRM. If this var is TRUE, it cannot be scanned and re-uploaded
|
||||
var/has_drm = FALSE
|
||||
|
||||
/obj/item/book/attack_self(mob/user as mob)
|
||||
if(carved)
|
||||
if(store)
|
||||
to_chat(user, "<span class='notice'>[store] falls out of [title]!</span>")
|
||||
store.forceMove(get_turf(loc))
|
||||
store = null
|
||||
return
|
||||
else
|
||||
to_chat(user, "<span class='notice'>The pages of [title] have been cut out!</span>")
|
||||
return
|
||||
if(src.dat)
|
||||
user << browse("<TT><I>Penned by [author].</I></TT> <BR>" + "[dat]", "window=book")
|
||||
if(!isobserver(user))
|
||||
user.visible_message("[user] opens a book titled \"[title]\" and begins reading intently.")
|
||||
onclose(user, "book")
|
||||
else
|
||||
to_chat(user, "This book is completely blank!")
|
||||
|
||||
/obj/item/book/attackby(obj/item/W as obj, mob/user as mob, params)
|
||||
if(carved)
|
||||
if(!store)
|
||||
if(W.w_class < WEIGHT_CLASS_NORMAL)
|
||||
user.drop_item()
|
||||
W.forceMove(src)
|
||||
store = W
|
||||
to_chat(user, "<span class='notice'>You put [W] in [title].</span>")
|
||||
return 1
|
||||
else
|
||||
to_chat(user, "<span class='notice'>[W] won't fit in [title].</span>")
|
||||
return 1
|
||||
else
|
||||
to_chat(user, "<span class='notice'>There's already something in [title]!</span>")
|
||||
return 1
|
||||
if(istype(W, /obj/item/pen))
|
||||
if(unique)
|
||||
to_chat(user, "These pages don't seem to take the ink well. Looks like you can't modify it.")
|
||||
return 1
|
||||
var/choice = input("What would you like to change?") in list("Title", "Contents", "Author", "Cancel")
|
||||
switch(choice)
|
||||
if("Title")
|
||||
var/newtitle = reject_bad_text(stripped_input(usr, "Write a new title:"))
|
||||
if(!newtitle)
|
||||
to_chat(usr, "The title is invalid.")
|
||||
return 1
|
||||
else
|
||||
src.name = newtitle
|
||||
src.title = newtitle
|
||||
if("Contents")
|
||||
var/content = strip_html(input(usr, "Write your book's contents (HTML NOT allowed):") as message|null, MAX_BOOK_MESSAGE_LEN)
|
||||
if(!content)
|
||||
to_chat(usr, "The content is invalid.")
|
||||
return 1
|
||||
else
|
||||
src.dat += content
|
||||
if("Author")
|
||||
var/newauthor = stripped_input(usr, "Write the author's name:")
|
||||
if(!newauthor)
|
||||
to_chat(usr, "The name is invalid.")
|
||||
return 1
|
||||
else
|
||||
src.author = newauthor
|
||||
return 1
|
||||
else if(istype(W, /obj/item/barcodescanner))
|
||||
var/obj/item/barcodescanner/scanner = W
|
||||
if(!scanner.computer)
|
||||
to_chat(user, "[W]'s screen flashes: 'No associated computer found!'")
|
||||
else
|
||||
switch(scanner.mode)
|
||||
if(0)
|
||||
scanner.book = src
|
||||
to_chat(user, "[W]'s screen flashes: 'Book stored in buffer.'")
|
||||
if(1)
|
||||
scanner.book = src
|
||||
scanner.computer.buffer_book = src.name
|
||||
to_chat(user, "[W]'s screen flashes: 'Book stored in buffer. Book title stored in associated computer buffer.'")
|
||||
if(2)
|
||||
scanner.book = src
|
||||
for(var/datum/borrowbook/b in scanner.computer.checkouts)
|
||||
if(b.bookname == src.name)
|
||||
scanner.computer.checkouts.Remove(b)
|
||||
to_chat(user, "[W]'s screen flashes: 'Book stored in buffer. Book has been checked in.'")
|
||||
return 1
|
||||
to_chat(user, "[W]'s screen flashes: 'Book stored in buffer. No active check-out record found for current title.'")
|
||||
if(3)
|
||||
scanner.book = src
|
||||
for(var/obj/item/book in scanner.computer.inventory)
|
||||
if(book == src)
|
||||
to_chat(user, "[W]'s screen flashes: 'Book stored in buffer. Title already present in inventory, aborting to avoid duplicate entry.'")
|
||||
return 1
|
||||
scanner.computer.inventory.Add(src)
|
||||
to_chat(user, "[W]'s screen flashes: 'Book stored in buffer. Title added to general inventory.'")
|
||||
return 1
|
||||
else if(istype(W, /obj/item/kitchen/knife) && !carved)
|
||||
carve_book(user, W)
|
||||
else
|
||||
return ..()
|
||||
|
||||
/obj/item/book/wirecutter_act(mob/user, obj/item/I)
|
||||
return carve_book(user, I)
|
||||
|
||||
/obj/item/book/attack(mob/M, mob/living/user)
|
||||
if(user.a_intent == INTENT_HELP)
|
||||
force = 0
|
||||
attack_verb = list("educated")
|
||||
else
|
||||
force = initial(force)
|
||||
attack_verb = list("bashed", "whacked")
|
||||
..()
|
||||
|
||||
/obj/item/book/proc/carve_book(mob/user, obj/item/I)
|
||||
if(!I.sharp && I.tool_behaviour != TOOL_WIRECUTTER) //Only sharp and wirecutter things can carve books
|
||||
to_chat(user, "<span class='warning>You can't carve [title] using that!</span>")
|
||||
return
|
||||
if(carved)
|
||||
return
|
||||
to_chat(user, "<span class='notice'>You begin to carve out [title].</span>")
|
||||
if(I.use_tool(src, user, 30, volume = I.tool_volume))
|
||||
to_chat(user, "<span class='notice'>You carve out the pages from [title]! You didn't want to read it anyway.</span>")
|
||||
carved = TRUE
|
||||
return TRUE
|
||||
/*
|
||||
* Barcode Scanner
|
||||
*/
|
||||
/obj/item/barcodescanner
|
||||
name = "barcode scanner"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state ="scanner"
|
||||
throw_speed = 1
|
||||
throw_range = 5
|
||||
w_class = WEIGHT_CLASS_TINY
|
||||
var/obj/machinery/computer/library/checkout/computer // Associated computer - Modes 1 to 3 use this
|
||||
var/obj/item/book/book // Currently scanned book
|
||||
var/mode = 0 // 0 - Scan only, 1 - Scan and Set Buffer, 2 - Scan and Attempt to Check In, 3 - Scan and Attempt to Add to Inventory
|
||||
|
||||
/obj/item/barcodescanner/attack_self(mob/user as mob)
|
||||
mode += 1
|
||||
if(mode > 3)
|
||||
mode = 0
|
||||
to_chat(user, "[src] Status Display:")
|
||||
var/modedesc
|
||||
switch(mode)
|
||||
if(0)
|
||||
modedesc = "Scan book to local buffer."
|
||||
if(1)
|
||||
modedesc = "Scan book to local buffer and set associated computer buffer to match."
|
||||
if(2)
|
||||
modedesc = "Scan book to local buffer, attempt to check in scanned book."
|
||||
if(3)
|
||||
modedesc = "Scan book to local buffer, attempt to add book to general inventory."
|
||||
else
|
||||
modedesc = "ERROR"
|
||||
to_chat(user, " - Mode [mode] : [modedesc]")
|
||||
if(src.computer)
|
||||
to_chat(user, "<font color=green>Computer has been associated with this unit.</font>")
|
||||
else
|
||||
to_chat(user, "<font color=red>No associated computer found. Only local scans will function properly.</font>")
|
||||
to_chat(user, "\n")
|
||||
@@ -1,225 +0,0 @@
|
||||
#define LIBRARY_BOOKS_PER_PAGE 25
|
||||
|
||||
GLOBAL_DATUM_INIT(library_catalog, /datum/library_catalog, new())
|
||||
GLOBAL_LIST_INIT(library_section_names, list("Any", "Fiction", "Non-Fiction", "Adult", "Reference", "Religion"))
|
||||
|
||||
/*
|
||||
* Borrowbook datum
|
||||
*/
|
||||
/datum/borrowbook // Datum used to keep track of who has borrowed what when and for how long.
|
||||
var/bookname
|
||||
var/mobname
|
||||
var/getdate
|
||||
var/duedate
|
||||
|
||||
/*
|
||||
* Cachedbook datum
|
||||
*/
|
||||
/datum/cachedbook // Datum used to cache the SQL DB books locally in order to achieve a performance gain.
|
||||
var/id
|
||||
var/title
|
||||
var/author
|
||||
var/ckey // ADDED 24/2/2015 - N3X
|
||||
var/category
|
||||
var/content
|
||||
var/programmatic=0 // Is the book programmatically added to the catalog?
|
||||
var/forbidden=0
|
||||
var/path = /obj/item/book // Type path of the book to generate
|
||||
var/flagged = 0
|
||||
|
||||
/datum/cachedbook/proc/LoadFromRow(list/row)
|
||||
id = row["id"]
|
||||
author = row["author"]
|
||||
title = row["title"]
|
||||
category = row["category"]
|
||||
ckey = row["ckey"]
|
||||
flagged = row["flagged"]
|
||||
if("content" in row)
|
||||
content = row["content"]
|
||||
programmatic=0
|
||||
|
||||
// Builds a SQL statement
|
||||
/datum/library_query
|
||||
var/author
|
||||
var/category
|
||||
var/title
|
||||
|
||||
// So we can have catalogs of books that are programmatic, and ones that aren't.
|
||||
/datum/library_catalog
|
||||
var/list/cached_books = list()
|
||||
|
||||
/datum/library_catalog/New()
|
||||
var/newid=1
|
||||
for(var/typepath in subtypesof(/obj/item/book/manual))
|
||||
var/obj/item/book/B = new typepath(null)
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.forbidden = B.forbidden
|
||||
CB.title = B.name
|
||||
CB.author = B.author
|
||||
CB.programmatic=1
|
||||
CB.path=typepath
|
||||
CB.id = "M[newid]"
|
||||
newid++
|
||||
cached_books["[CB.id]"]=CB
|
||||
|
||||
/datum/library_catalog/proc/flag_book_by_id(mob/user, id)
|
||||
var/global/books_flagged_this_round[0]
|
||||
|
||||
if("[id]" in cached_books)
|
||||
var/datum/cachedbook/CB = cached_books["[id]"]
|
||||
if(CB.programmatic)
|
||||
to_chat(user, "<span class='danger'>That book cannot be flagged in the system, as it does not actually exist in the database.</span>")
|
||||
return
|
||||
|
||||
if("[id]" in books_flagged_this_round)
|
||||
to_chat(user, "<span class='danger'>This book has already been flagged this shift.</span>")
|
||||
return
|
||||
|
||||
books_flagged_this_round["[id]"] = 1
|
||||
message_admins("[key_name_admin(user)] has flagged book #[id] as inappropriate.")
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("UPDATE library SET flagged = flagged + 1 WHERE id=:id", list(
|
||||
"id" = text2num(id)
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
qdel(query)
|
||||
|
||||
/datum/library_catalog/proc/rmBookByID(mob/user, id)
|
||||
if("[id]" in cached_books)
|
||||
var/datum/cachedbook/CB = cached_books["[id]"]
|
||||
if(CB.programmatic)
|
||||
to_chat(user, "<span class='danger'>That book cannot be removed from the system, as it does not actually exist in the database.</span>")
|
||||
return
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("DELETE FROM library WHERE id=:id", list(
|
||||
"id" = text2num(id)
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
qdel(query)
|
||||
|
||||
/datum/library_catalog/proc/getBookByID(id)
|
||||
if("[id]" in cached_books)
|
||||
return cached_books["[id]"]
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("SELECT id, author, title, category, content, ckey, flagged FROM library WHERE id=:id", list(
|
||||
"id" = text2num(id)
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
var/list/results=list()
|
||||
while(query.NextRow())
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.LoadFromRow(list(
|
||||
"id" =query.item[1],
|
||||
"author" =query.item[2],
|
||||
"title" =query.item[3],
|
||||
"category"=query.item[4],
|
||||
"content" =query.item[5],
|
||||
"ckey" =query.item[6],
|
||||
"flagged" =query.item[7]
|
||||
))
|
||||
results += CB
|
||||
cached_books["[id]"]=CB
|
||||
qdel(query)
|
||||
return CB
|
||||
qdel(query)
|
||||
return results
|
||||
|
||||
/** Scanner **/
|
||||
/obj/machinery/libraryscanner
|
||||
name = "scanner"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "bigscanner"
|
||||
anchored = 1
|
||||
density = 1
|
||||
var/obj/item/book/cache // Last scanned book
|
||||
|
||||
/obj/machinery/libraryscanner/attackby(obj/item/I, mob/user)
|
||||
if(default_unfasten_wrench(user, I))
|
||||
power_change()
|
||||
return
|
||||
if(istype(I, /obj/item/book))
|
||||
// NT with those pesky DRM schemes
|
||||
var/obj/item/book/B = I
|
||||
if(B.has_drm)
|
||||
atom_say("Copyrighted material detected. Scanner is unable to copy book to memory.")
|
||||
return FALSE
|
||||
user.drop_item()
|
||||
I.forceMove(src)
|
||||
return 1
|
||||
else
|
||||
return ..()
|
||||
|
||||
/obj/machinery/libraryscanner/attack_hand(mob/user)
|
||||
if(istype(user,/mob/dead))
|
||||
to_chat(user, "<span class='danger'>Nope.</span>")
|
||||
return
|
||||
usr.set_machine(src)
|
||||
var/dat = "<HEAD><TITLE>Scanner Control Interface</TITLE></HEAD><BODY>\n" // <META HTTP-EQUIV='Refresh' CONTENT='10'>
|
||||
if(cache)
|
||||
dat += "<FONT color=#005500>Data stored in memory.</FONT><BR>"
|
||||
else
|
||||
dat += "No data stored in memory.<BR>"
|
||||
dat += "<A href='?src=[UID()];scan=1'>\[Scan\]</A>"
|
||||
if(cache)
|
||||
dat += " <A href='?src=[UID()];clear=1'>\[Clear Memory\]</A><BR><BR><A href='?src=[UID()];eject=1'>\[Remove Book\]</A>"
|
||||
else
|
||||
dat += "<BR>"
|
||||
user << browse(dat, "window=scanner")
|
||||
onclose(user, "scanner")
|
||||
|
||||
/obj/machinery/libraryscanner/Topic(href, href_list)
|
||||
if(..())
|
||||
usr << browse(null, "window=scanner")
|
||||
onclose(usr, "scanner")
|
||||
return
|
||||
|
||||
if(href_list["scan"])
|
||||
for(var/obj/item/book/B in contents)
|
||||
cache = B
|
||||
break
|
||||
if(href_list["clear"])
|
||||
cache = null
|
||||
if(href_list["eject"])
|
||||
for(var/obj/item/book/B in contents)
|
||||
B.loc = src.loc
|
||||
src.add_fingerprint(usr)
|
||||
src.updateUsrDialog()
|
||||
return
|
||||
|
||||
|
||||
/*
|
||||
* Book binder
|
||||
*/
|
||||
/obj/machinery/bookbinder
|
||||
name = "Book Binder"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "binder"
|
||||
anchored = 1
|
||||
density = 1
|
||||
|
||||
/obj/machinery/bookbinder/attackby(obj/item/I, mob/user)
|
||||
var/obj/item/paper/P = I
|
||||
if(default_unfasten_wrench(user, I))
|
||||
power_change()
|
||||
return
|
||||
if(istype(P))
|
||||
user.drop_item()
|
||||
user.visible_message("[user] loads some paper into [src].", "You load some paper into [src].")
|
||||
src.visible_message("[src] begins to hum as it warms up its printing drums.")
|
||||
sleep(rand(200,400))
|
||||
src.visible_message("[src] whirs as it prints and binds a new book.")
|
||||
var/obj/item/book/b = new(loc)
|
||||
b.dat = P.info
|
||||
b.name = "Print Job #[rand(100, 999)]"
|
||||
b.icon_state = "book[rand(1,16)]"
|
||||
qdel(P)
|
||||
return 1
|
||||
else
|
||||
return ..()
|
||||
@@ -1,61 +0,0 @@
|
||||
//*******************************
|
||||
//
|
||||
// Library SQL Configuration
|
||||
//
|
||||
//*******************************
|
||||
|
||||
// Deprecated! See global.dm for new SQL config vars -- TLE
|
||||
/*
|
||||
#define SQL_ADDRESS ""
|
||||
#define SQL_DB ""
|
||||
#define SQL_PORT "3306"
|
||||
#define SQL_LOGIN ""
|
||||
#define SQL_PASS ""
|
||||
*/
|
||||
|
||||
//*******************************
|
||||
// Requires Dantom.DB library ( http://www.byond.com/developer/Dantom/DB )
|
||||
|
||||
|
||||
/*
|
||||
The Library
|
||||
------------
|
||||
A place for the crew to go, relax, and enjoy a good book.
|
||||
Aspiring authors can even self publish and, if they're lucky
|
||||
convince the on-staff Librarian to submit it to the Archives
|
||||
to be chronicled in history forever - some say even persisting
|
||||
through alternate dimensions.
|
||||
|
||||
|
||||
Written by TLE for /tg/station 13
|
||||
Feel free to use this as you like. Some credit would be cool.
|
||||
Check us out at http://nanotrasen.com/ if you're so inclined.
|
||||
*/
|
||||
|
||||
// CONTAINS:
|
||||
|
||||
// Objects:
|
||||
// - bookcase
|
||||
// - book
|
||||
// - barcode scanner
|
||||
// Machinery:
|
||||
// - library computer
|
||||
// - visitor's computer
|
||||
// - book binder
|
||||
// - book scanner
|
||||
// Datum:
|
||||
// - borrowbook
|
||||
|
||||
|
||||
// Ideas for the future
|
||||
// ---------------------
|
||||
// - Visitor's computer should be able to search the current in-round library inventory (that the Librarian has stocked and checked in)
|
||||
// -- Give computer other features like an Instant Messenger application, or the ability to edit, save, and print documents.
|
||||
// - Admin interface directly tied to the Archive DB. Right now there's no way to delete uploaded books in-game.
|
||||
// -- If this gets implemented, allow Librarians to "tag" or "suggest" books to be deleted. The DB ID of the tagged books gets saved to a text file (or another table in the DB maybe?).
|
||||
// The admin interface would automatically take these IDs and SELECT them all from the DB to be displayed along with a Delete link to drop the row from the table.
|
||||
// - When the game sets up and the round begins, have it automatically pick random books from the DB to populate the library with. Even if the Librarian is a useless fuck there are at least a few books around.
|
||||
// - Allow books to be "hollowed out" like the Chaplain's Bible, allowing you to store one pocket-sized item inside.
|
||||
// - Make books/book cases burn when exposed to flame.
|
||||
// - Make book binder hackable.
|
||||
// - Books shouldn't print straight from the library computer. Make it synch with a machine like the book binder to print instead. This should consume some sort of resource.
|
||||
@@ -0,0 +1,206 @@
|
||||
#define LIBRARY_MENU_MAIN 1
|
||||
#define LIBRARY_MENU_CKEY 2
|
||||
#define LIBRARY_MENU_REPORTS 3
|
||||
|
||||
/client/proc/library_manager()
|
||||
set name = "Manage Library"
|
||||
set category = "Admin"
|
||||
set desc = "Manage Flagged Books and Perform Maintenance on the Library System"
|
||||
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
|
||||
var/datum/ui_module/library_manager/L = new()
|
||||
L.ui_interact(usr)
|
||||
|
||||
/datum/ui_module/library_manager
|
||||
name = "Library Manager"
|
||||
///Where we will store our cachedbook datums
|
||||
var/list/cached_books = list()
|
||||
///list of assoc lists detailing each invidual reports, can contain multiple reports for same book
|
||||
var/list/reports = list()
|
||||
|
||||
///TGUI page we are currently on
|
||||
var/page_state = LIBRARY_MENU_MAIN
|
||||
///Ckey's books we are viewing
|
||||
var/selected_ckey
|
||||
|
||||
///information for the book we are opening in browserui
|
||||
var/datum/cachedbook/view_book
|
||||
///browserui helper variable for turning pages in book
|
||||
var/view_book_page = 0
|
||||
|
||||
/datum/ui_module/library_manager/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.admin_state)
|
||||
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
|
||||
if(!ui)
|
||||
ui = new(user, src, ui_key, "LibraryManager", name, 600, 600, master_ui, state)
|
||||
ui.autoupdate = TRUE
|
||||
ui.open()
|
||||
|
||||
/datum/ui_module/library_manager/ui_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["pagestate"] = page_state
|
||||
data["booklist"] = cached_books
|
||||
data["ckey"] = selected_ckey ? selected_ckey : "ERROR"
|
||||
data["reports"] = reports
|
||||
|
||||
data["modal"] = ui_modal_data(src)
|
||||
return data
|
||||
|
||||
/datum/ui_module/library_manager/ui_act(action, params, datum/tgui/ui)
|
||||
if(..())
|
||||
return
|
||||
|
||||
if(ui_act_modal(action, params))
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("view_reported_books")
|
||||
reports = list()
|
||||
for(var/datum/cachedbook/CB in GLOB.library_catalog.get_flagged_books())
|
||||
for(var/datum/flagged_book/report as anything in CB.reports)
|
||||
if(!report)
|
||||
continue
|
||||
var/datum/library_category/report_category = GLOB.library_catalog.get_report_category_by_id(report.category_id)
|
||||
var/report_info = list(
|
||||
"reporter_ckey" = report.reporter,
|
||||
"uploader_ckey" = CB.ckey,
|
||||
"id" = CB.id,
|
||||
"title" = CB.title,
|
||||
"author" = CB.author,
|
||||
"report_description" = report_category.description,
|
||||
)
|
||||
reports += list(report_info)
|
||||
page_state = LIBRARY_MENU_REPORTS
|
||||
if("delete_book")
|
||||
if(text2num(params["bookid"])) //make sure this is actually a number
|
||||
if(GLOB.library_catalog.remove_book_by_id(text2num(params["bookid"])))
|
||||
log_and_message_admins("has deleted book [params["bookid"]].")
|
||||
if("view_book")
|
||||
if(params["bookid"])
|
||||
view_book_by_id(text2num(params["bookid"]), ui.user)
|
||||
if("unflag_book")
|
||||
if(params["bookid"])
|
||||
if(GLOB.library_catalog.unflag_book_by_id(text2num(params["bookid"])))
|
||||
log_and_message_admins("has unflagged book [params["bookid"]].")
|
||||
if("return")
|
||||
page_state = LIBRARY_MENU_MAIN
|
||||
|
||||
|
||||
/datum/ui_module/library_manager/proc/ui_act_modal(action, list/params)
|
||||
. = TRUE
|
||||
var/id = params["id"] // The modal's ID
|
||||
var/list/arguments = istext(params["arguments"]) ? json_decode(params["arguments"]) : params["arguments"]
|
||||
switch(ui_modal_act(src, action, params))
|
||||
if(UI_MODAL_OPEN)
|
||||
switch(id)
|
||||
if("specify_ssid_delete")
|
||||
ui_modal_input(src, id, "Please input a book SSID:", null, arguments)
|
||||
if("specify_ckey_search")
|
||||
ui_modal_input(src, id, "Please input a CKEY:", null, arguments)
|
||||
if("specify_ckey_delete")
|
||||
ui_modal_input(src, id, "Please input a CKEY:", null, arguments)
|
||||
else
|
||||
return FALSE
|
||||
if(UI_MODAL_ANSWER)
|
||||
var/answer = params["answer"]
|
||||
switch(id)
|
||||
if("specify_ssid_delete")
|
||||
if(!answer || !text2num(answer))
|
||||
return
|
||||
var/confirm = alert("You are about to delete book [text2num(answer)]", "Confirm Deletion", "Yes", "No")
|
||||
if(confirm != "Yes")
|
||||
return //we don't need to sanitize b/c removeBookyByID uses id=:id instead of like statemetns
|
||||
if(GLOB.library_catalog.remove_book_by_id(text2num(answer)))
|
||||
log_and_message_admins("has deleted the book [text2num(answer)].")
|
||||
if("specify_ckey_search")
|
||||
if(!answer)
|
||||
return
|
||||
var/datum/library_user_data/search_terms = new()
|
||||
search_terms.search_ckey = paranoid_sanitize(answer)
|
||||
selected_ckey = paranoid_sanitize(answer)
|
||||
cached_books = list()
|
||||
for(var/datum/cachedbook/CB in GLOB.library_catalog.get_book_by_range(1, 10, search_terms))
|
||||
var/list/book_data = list(
|
||||
"id" = CB.id,
|
||||
"title" = CB.title,
|
||||
"author" = CB.author,
|
||||
"rating" = CB.rating,
|
||||
"summary" = CB.summary,
|
||||
"ckey" = CB.ckey,
|
||||
"reports" = CB.reports,
|
||||
)
|
||||
cached_books += list(book_data)
|
||||
page_state = LIBRARY_MENU_CKEY
|
||||
if("specify_ckey_delete")
|
||||
if(!answer)
|
||||
return
|
||||
var/sanitized_answer = paranoid_sanitize(answer) //the last thing we want happening is someone deleting every book with "%%"
|
||||
var/confirm //We want to be absolutely certain an admin wants to do this
|
||||
confirm = alert("You are about to mass delete potentially up to 10 books", "Confirm Deletion", "Yes", "No")
|
||||
if(confirm != "Yes")
|
||||
return
|
||||
if(GLOB.library_catalog.remove_books_by_ckey(sanitized_answer))
|
||||
log_and_message_admins("has deleted all books uploaded by [answer].")
|
||||
else
|
||||
return FALSE
|
||||
else
|
||||
return FALSE
|
||||
|
||||
/datum/ui_module/library_manager/proc/view_book_by_id(bookid, mob/user)
|
||||
if(!view_book || view_book.id != bookid)
|
||||
view_book = GLOB.library_catalog.get_book_by_id(bookid)
|
||||
view_book_page = 0
|
||||
view_book(user)
|
||||
|
||||
/*
|
||||
* #View Book
|
||||
*
|
||||
* Internal proc for viewing library books as an admin. This absolutely must stay as BrowserUI even though the rest of
|
||||
* the library manager is TGUI. This is because of TGUI sanitization issues.
|
||||
*/
|
||||
/datum/ui_module/library_manager/proc/view_book(mob/user)
|
||||
if(!view_book || !length(view_book.content))
|
||||
return
|
||||
|
||||
var/dat = ""
|
||||
//First, we're going to choose/generate our header buttons for switching pages and store it in var/dat
|
||||
var/header_left = "<div style='float:left; text-align:left; width:49.9%'></div>"
|
||||
var/header_right = "<div style ='float;left; text-align:right; width:49.9%'></div>"
|
||||
if(length(view_book.content)) //No need to have page switching buttons if there's no pages
|
||||
if(view_book_page < length(view_book.content))
|
||||
header_right = "<div style='float:left; text-align:right; width:49.9%'><a href='?src=[UID()];next_page=1'>Next Page</a></div><br><hr>"
|
||||
if(view_book_page)
|
||||
header_left = "<div style='float:left; text-align:left; width:49.9%'><a href='?src=[UID()];prev_page=1'>Previous Page</a></div>"
|
||||
|
||||
dat += header_left + header_right
|
||||
//Now we're going to display the header buttons + the current page selected, if it's page 0, we display the cover_page instead
|
||||
if(!view_book_page)
|
||||
var/cover_page = {"<center><h1>[view_book.title]</h1><br></h2>Written by: [view_book.author]</h2></center><br><hr><b>Summary:</b> [view_book.summary]"}
|
||||
user << browse("<body>[dat]<br>" + "[cover_page]", "window=book[UID()];size=400x400")
|
||||
return
|
||||
else
|
||||
user << browse("<body>[dat]<br>" + "[view_book.content[view_book_page]]", "window=book[UID()]")
|
||||
|
||||
/datum/ui_module/library_manager/Topic(href, href_list)
|
||||
..()
|
||||
if(!check_rights(R_ADMIN))
|
||||
log_admin("[key_name(usr)] tried to use the library manager without authorization.")
|
||||
message_admins("[key_name_admin(usr)] has attempted to override the library manager!")
|
||||
return
|
||||
if(href_list["next_page"])
|
||||
if(view_book_page > length(view_book.content)) //should never be false, but just in-case
|
||||
view_book_page = length(view_book.content)
|
||||
return
|
||||
view_book_page++
|
||||
view_book(usr) //scuffed but this is how you update the UI
|
||||
if(href_list["prev_page"])
|
||||
if(view_book_page < 0) //should never be false, but just in-case
|
||||
view_book_page = 0
|
||||
return
|
||||
view_book_page--
|
||||
view_book(usr) //scuffed but this is how you update the UI
|
||||
|
||||
#undef LIBRARY_MENU_MAIN
|
||||
#undef LIBRARY_MENU_CKEY
|
||||
#undef LIBRARY_MENU_REPORTS
|
||||
@@ -0,0 +1,653 @@
|
||||
///library category datum constructor helper, used to make easier the process of defining new report/book categories
|
||||
#define DEFINE_CATEGORY(C, D) (new /datum/library_category(_category_id = C, _description = D))
|
||||
///Maximum number of books that can be uploaded by a single ckey
|
||||
#define MAX_PLAYER_UPLOADS 5
|
||||
|
||||
/*
|
||||
* # Library Catalog
|
||||
|
||||
This datum forms the basis for the entire library system, one is created at roundstart and stored to a global variable
|
||||
It holds lists for all predefined report and book categories, books flagged during the round, and all programmatic
|
||||
books.
|
||||
|
||||
Additionally, ALL library SQL queries are handled in this datum. This is intentional so that we do not have multiple SQL
|
||||
queries trying to do the same thing but in 4-5 different dm files. This file is split/organized into a specific
|
||||
structure:
|
||||
TOP - Defining Library Lists
|
||||
MIDDLE - Get Procs that get information from the catalog or DB
|
||||
BOTTOM - Send/Update procs that perform changes to the Database. Don't mix their functionalities together.
|
||||
*/
|
||||
/datum/library_catalog
|
||||
///Lists of all reported books in the current round
|
||||
var/list/flagged_books = list()
|
||||
///List of all programmatic books, automatically generated upon New()
|
||||
var/list/books = list()
|
||||
///List of all report categories, automatically generated upon New()
|
||||
var/list/report_types = list()
|
||||
///List of all book categories, automatically generated upon New()
|
||||
var/list/categories = list()
|
||||
|
||||
/datum/library_catalog/New()
|
||||
|
||||
//Building a list of all the reasons that players can report books, used for report menu + logging reports in DB
|
||||
//Cat ID needs to be unique to category, description can be changed here without issues anywhere else
|
||||
report_types = list(
|
||||
DEFINE_CATEGORY(LIB_REPORT_HATESPEECH, "Hatespeech or Slur Usage"),
|
||||
DEFINE_CATEGORY(LIB_REPORT_EROTICA, "Erotica or Sexual Content"),
|
||||
DEFINE_CATEGORY(LIB_REPORT_OOC, "Out of Character Information"),
|
||||
DEFINE_CATEGORY(LIB_REPORT_COPYPASTA, "Copypastas or Spam"),
|
||||
DEFINE_CATEGORY(LIB_REPORT_BLANK , "Blank or No Content"),
|
||||
DEFINE_CATEGORY(LIB_REPORT_NOEFFORT , "Very Low Effort Content"),
|
||||
DEFINE_CATEGORY(LIB_REPORT_OTHER , "Other Reason not Specified"), //required
|
||||
)
|
||||
|
||||
//building a list of all categories, used for searching, Cat ID needs to be unique to category
|
||||
categories = list(
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_FICTION, "Fiction"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_NONFICTION, "Non-Fiction"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_RELIGION, "Religious"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_FANTASY, "Fantasy"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_HORROR, "Horror"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_ROMANCE, "Romance"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_MYSTERY, "Mystery"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_ADVENTURE, "Adventure"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_HISTORY, "History"),
|
||||
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_PHILOSOPHY, "Philosophy"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_DRAMA, "Drama and Thriller"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_EXPERIMENT, "Experiment Notes"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_LEGAL, "Legal Document"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_BIOGRAPHY, "Biography"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_GUIDE, "Guides and References"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_PAPERWORK, "Paperwork"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_COOKING, "Culinary Arts"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_DESIGN, "Decor and Design"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_COMBAT, "Martial Arts and Combat"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_EXPLORATION, "Exploration"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_THEATRE, "Theatre"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_POETRY, "Poetry"),
|
||||
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_LAW, "Law"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_SECURITY, "Security"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_SUPPLY, "Supply"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_ENGINEERING, "Engineering"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_SERVICE , "Service"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_MEDICAL, "Medical"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_RESEARCH, "Science"),
|
||||
DEFINE_CATEGORY(LIB_CATEGORY_COMMAND , "Command"),
|
||||
)
|
||||
|
||||
//Books that we don't want showing up in the programmatic book list
|
||||
//Books should go here if they're non-functional, spawners, or are designed for off-station roles to consume
|
||||
var/list/forbidden_books = list(
|
||||
/obj/item/book/manual/random,
|
||||
/obj/item/book/manual/nuclear,
|
||||
/obj/item/book/manual/wiki,
|
||||
/obj/item/book/manual/hydroponics_pod_people,
|
||||
)
|
||||
|
||||
var/newid = 1
|
||||
//building a list of all programmatic books
|
||||
for(var/typepath in (subtypesof(/obj/item/book/manual) - forbidden_books))
|
||||
var/obj/item/book/B = typepath
|
||||
var/datum/programmatic_book/PB = new()
|
||||
PB.title = initial(B.name)
|
||||
PB.author = initial(B.author)
|
||||
PB.id = "M[newid]"
|
||||
PB.book_type = typepath
|
||||
newid++
|
||||
books += PB
|
||||
|
||||
/*
|
||||
* can_vv_delete override
|
||||
* Admins should not be deleting this willy nilly, if they think it is neccesary,
|
||||
* they can go through the effort of advanced proccall
|
||||
*/
|
||||
/datum/library_catalog/can_vv_delete()
|
||||
message_admins("An admin attempted to VV delete the global library catalog, this will break the library system for the round, if you know what you are doing please use advanced proccal")
|
||||
return FALSE
|
||||
|
||||
/*
|
||||
* Database Select and Get Procs
|
||||
*
|
||||
* Each of these procs facilitate finding, taking,
|
||||
* and prepping information from the database for use elsewhere
|
||||
*/
|
||||
|
||||
///External proc that Returns a report library_category datum that matches the provided cat_id
|
||||
/datum/library_catalog/proc/get_report_category_by_id(category_id)
|
||||
for(var/datum/library_category/category in report_types)
|
||||
if(category.category_id == category_id)
|
||||
return category
|
||||
//proc shouldn't get this far, but if there's an entry in the DB that we don't have added, just default to other cat
|
||||
for(var/datum/library_category/category in report_types)
|
||||
if(category.category_id == LIB_REPORT_OTHER)
|
||||
return category
|
||||
|
||||
///External proc that Returns a book library_category datum that matches the provided cat_id
|
||||
/datum/library_catalog/proc/get_book_category_by_id(category_id)
|
||||
for(var/datum/library_category/category in categories)
|
||||
if(category.category_id == category_id)
|
||||
return category
|
||||
|
||||
///External proc that Returns a report programmaticbook datum that matches the provided bookid
|
||||
/datum/library_catalog/proc/get_programmatic_book_by_id(id)
|
||||
for(var/datum/programmatic_book/PB as anything in books)
|
||||
if(PB.id == id)
|
||||
return PB
|
||||
|
||||
/*
|
||||
* # get_book_by_id
|
||||
*
|
||||
* External proc that takes in an id number and searches the Database for a book with a matching SSID
|
||||
* returns a cached book with the data from that row
|
||||
*
|
||||
* Arguments:
|
||||
* * id - integer value that matches as a book SSID
|
||||
*/
|
||||
/datum/library_catalog/proc/get_book_by_id(id)
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("SELECT id, author, title, content, summary, rating, raters, primary_category, secondary_category, tertiary_category, ckey, reports FROM library WHERE id=:id", list(
|
||||
"id" = id
|
||||
))
|
||||
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
var/list/results = list()
|
||||
while(query.NextRow())
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.LoadFromRow(list(
|
||||
"id" = query.item[1],
|
||||
"author" = query.item[2],
|
||||
"title" = query.item[3],
|
||||
"content" = query.item[4],
|
||||
"summary" = query.item[5],
|
||||
"rating" = query.item[6],
|
||||
"raters" = query.item[7],
|
||||
"primary_category" = query.item[8],
|
||||
"secondary_category" = query.item[9],
|
||||
"tertiary_category" = query.item[10],
|
||||
"ckey" = query.item[11],
|
||||
"reports" = query.item[12],
|
||||
))
|
||||
results += CB
|
||||
qdel(query)
|
||||
return CB
|
||||
qdel(query)
|
||||
return results
|
||||
|
||||
/*
|
||||
* # build_search_query
|
||||
*
|
||||
* Internal proc that builds part of an SQL statement using a datum of search terms/parameters. It will then return
|
||||
* a list of two objects: 1) the built SQL statement and 2) the assoc list of parameters that will accompany it in the query
|
||||
* This should only ever be used to generate WHERE statements
|
||||
*
|
||||
* Arguments:
|
||||
* * datum/library_user_data/search_terms - datum with parameters for what we want to query our DB for
|
||||
*/
|
||||
/datum/library_catalog/proc/build_search_query(datum/library_user_data/search_terms)
|
||||
var/searchquery = ""
|
||||
//We do not want to use WHERE more than once in our query, first usage makes this TRUE and defaults other WHERE's to AND
|
||||
var/where = FALSE
|
||||
var/list/sql_params = list()
|
||||
if(search_terms)
|
||||
if(search_terms.search_title)
|
||||
searchquery += " WHERE title LIKE :title"
|
||||
sql_params["title"] = "%[search_terms.search_title]%"
|
||||
where = TRUE
|
||||
if(search_terms.search_author)
|
||||
searchquery += " [!where ? "WHERE" : "AND"] author LIKE :author"
|
||||
sql_params["author"] = "%[search_terms.search_author]%"
|
||||
where = TRUE
|
||||
if(length(search_terms.search_categories))
|
||||
//yes this sql is cursed, but this is how it must be done and we only ever use this once :)
|
||||
var/category_vars = list()
|
||||
var/category_count = 1
|
||||
for(var/c in search_terms.search_categories)
|
||||
sql_params["category[category_count]"] = c
|
||||
category_vars += ":category[category_count]"
|
||||
category_count++
|
||||
var/query_insert = "([jointext(category_vars, ", ")])"
|
||||
searchquery += " [!where ? "WHERE" : "AND"] (primary_category IN [query_insert] OR secondary_category IN [query_insert] OR tertiary_category IN [query_insert])"
|
||||
where = TRUE
|
||||
if(search_terms.search_rating["min"] && search_terms.search_rating["max"])
|
||||
searchquery += " [!where ? "WHERE" : "AND"] (rating BETWEEN :ratingmin AND :ratingmax)"
|
||||
sql_params["ratingmin"] = search_terms.search_rating["min"]
|
||||
sql_params["ratingmax"] = search_terms.search_rating["max"]
|
||||
where = TRUE
|
||||
if(search_terms.search_ckey)
|
||||
searchquery += " [!where ? "WHERE" : "AND"] ckey =:ckey"
|
||||
sql_params["ckey"] = search_terms.search_ckey
|
||||
where = TRUE
|
||||
|
||||
var/list/results = list(searchquery, sql_params)
|
||||
return results
|
||||
|
||||
/*
|
||||
* # get_book_by_range
|
||||
*
|
||||
* External proc used to get a large amount of books from a specific part of the library DB. Has paremeters to
|
||||
* specify range as well as what kind of books to look for. Will return a list of cachedbook datums.
|
||||
*
|
||||
* Arguments:
|
||||
* * initial - Book we want to start grabbing rows at, THIS IS NOT SSID, based on number of rows in DB
|
||||
* * range - Amount of books we want to grab at once
|
||||
* * datum/library_user_data/search_terms - datum with parameters for what we want to query our DB for
|
||||
*/
|
||||
/datum/library_catalog/proc/get_book_by_range(initial = 1, range = 25, datum/library_user_data/search_terms, doAsync = TRUE)
|
||||
var/list/search_query = build_search_query(search_terms)
|
||||
var/sql = "SELECT id, author, title, content, summary, rating, raters, primary_category, secondary_category, tertiary_category, ckey, reports FROM library" + search_query[1] + " LIMIT :lowerlimit, :upperlimit"
|
||||
var/list/sql_params = search_query[2]
|
||||
|
||||
sql_params["lowerlimit"] = initial
|
||||
sql_params["upperlimit"] = range
|
||||
|
||||
var/datum/db_query/select_query = SSdbcore.NewQuery(sql, sql_params)
|
||||
|
||||
if(!select_query.warn_execute(async = doAsync))
|
||||
qdel(select_query)
|
||||
return
|
||||
|
||||
var/list/results = list()
|
||||
while(select_query.NextRow())
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.LoadFromRow(list(
|
||||
"id" = select_query.item[1],
|
||||
"author" = select_query.item[2],
|
||||
"title" = select_query.item[3],
|
||||
"content" = select_query.item[4],
|
||||
"summary" = select_query.item[5],
|
||||
"rating" = select_query.item[6],
|
||||
"raters" = select_query.item[7],
|
||||
"primary_category" = select_query.item[8],
|
||||
"secondary_category" = select_query.item[9],
|
||||
"tertiary_category" = select_query.item[10],
|
||||
"ckey" = select_query.item[11],
|
||||
"reports" = select_query.item[12],
|
||||
))
|
||||
results += CB
|
||||
qdel(select_query)
|
||||
return results
|
||||
|
||||
/*
|
||||
* # get_flagged_books
|
||||
*
|
||||
* External proc that finds all books that have reports marked in the Database. Returns these books as a list
|
||||
* of cachedbook datums. This proc is not intended to actually handle reports or generate report_book datums
|
||||
*/
|
||||
/datum/library_catalog/proc/get_flagged_books()
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("SELECT id, author, title, content, summary, ckey, reports FROM library WHERE LENGTH(reports) > 5")
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
var/list/flagged_books = list()
|
||||
while(query.NextRow())
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.LoadFromRow(list(
|
||||
"id" = query.item[1],
|
||||
"author" = query.item[2],
|
||||
"title" = query.item[3],
|
||||
"content" = query.item[4],
|
||||
"summary" = query.item[5],
|
||||
"ckey" = query.item[6],
|
||||
"reports" = query.item[7],
|
||||
))
|
||||
flagged_books += CB
|
||||
qdel(query)
|
||||
return flagged_books
|
||||
|
||||
/*
|
||||
* # get_total_books
|
||||
*
|
||||
* External proc that counts the number of books in the DB that match the provided search parameters
|
||||
* calling this with no arguments will return the complete count of books in the DB, if the query fails
|
||||
* this proc will return null, so usages of this proc will need to account for that
|
||||
*
|
||||
* Arguments:
|
||||
* * datum/library_user_data/search_terms - datum with parameters for what we want to query our DB for
|
||||
*/
|
||||
/datum/library_catalog/proc/get_total_books(datum/library_user_data/search_terms)
|
||||
var/list/search_query = build_search_query(search_terms)
|
||||
var/sql = "SELECT COUNT(id) FROM library" + search_query[1]
|
||||
var/list/sql_params = search_query[2]
|
||||
|
||||
var/datum/db_query/count_query = SSdbcore.NewQuery(sql, sql_params)
|
||||
if(!count_query.warn_execute())
|
||||
qdel(count_query)
|
||||
return
|
||||
|
||||
while(count_query.NextRow())
|
||||
var/value = text2num(count_query.item[1])
|
||||
qdel(count_query)
|
||||
return value
|
||||
qdel(count_query)
|
||||
|
||||
/*
|
||||
* # get_book_ratings
|
||||
*
|
||||
* External proc that gets all of the book ratings for a book. Unless the requested SSID doesn't exist in the
|
||||
* database, this proc will return (if the book has ratings) a list with the
|
||||
* first element being the books avg ratings and a list of ratings by players ["ckey", rating_int]
|
||||
*
|
||||
* Arguments:
|
||||
* * bookid - SSID of the book you wish to get ratings for
|
||||
*/
|
||||
/datum/library_catalog/proc/get_book_ratings(bookid)
|
||||
var/list/sql_params = list()
|
||||
sql_params["id"] = bookid
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("SELECT rating, raters FROM library WHERE id=:id", sql_params)
|
||||
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
var/list/book_ratings = list()
|
||||
while(query.NextRow())
|
||||
if(!query.item[2] || length(query.item[2]) < 5) //we don't want to decode something that is null or contains no values
|
||||
book_ratings = list(query.item[1], list())
|
||||
break
|
||||
book_ratings = list(query.item[1], json_decode(query.item[2]))
|
||||
|
||||
qdel(query)
|
||||
return book_ratings
|
||||
|
||||
/*
|
||||
* # get_random_books
|
||||
*
|
||||
* External proc that gets random books from the Database, used by spawners for the most part. RANT: whoever wrote the fucking
|
||||
* old library code made this a global proc that accepted a loc and new'd/spawned in books from THIS PROC, take this as a
|
||||
* lesson never to do this. Anywho, this proc returns a list of cached books.
|
||||
*
|
||||
* Arguments:
|
||||
* * amount - amount of random books to get
|
||||
*/
|
||||
/datum/library_catalog/proc/get_random_book(amount = 1, doAsync = TRUE)
|
||||
if(!amount)
|
||||
return
|
||||
if(!SSdbcore.IsConnected())
|
||||
return
|
||||
var/num_books = clamp(amount, 1, 50) //you don't need more than 50 random books <3
|
||||
var/list/sql_params = list("amount" = num_books )
|
||||
var/sql = "SELECT id, author, title, content, summary, rating, primary_category, secondary_category, tertiary_category, ckey, reports FROM library GROUP BY title ORDER BY rand() LIMIT :amount"
|
||||
var/datum/db_query/query = SSdbcore.NewQuery(sql, sql_params)
|
||||
if(!query.warn_execute(async = doAsync)) //this proc is used in initialize in some objects :)
|
||||
qdel(query)
|
||||
return
|
||||
|
||||
var/list/results = list()
|
||||
while(query.NextRow())
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.LoadFromRow(list(
|
||||
"id" = query.item[1],
|
||||
"author" = query.item[2],
|
||||
"title" = query.item[3],
|
||||
"content" = query.item[4],
|
||||
"summary" = query.item[5],
|
||||
"rating" = query.item[6],
|
||||
"primary_category" = query.item[7],
|
||||
"secondary_category" = query.item[8],
|
||||
"tertiary_category" = query.item[9],
|
||||
"ckey" = query.item[10],
|
||||
"reports" = query.item[11]
|
||||
))
|
||||
results += CB
|
||||
qdel(query)
|
||||
return results
|
||||
/*
|
||||
* Database Update Procs
|
||||
*
|
||||
* Each of these procs facilitate editing/updating the database
|
||||
*/
|
||||
|
||||
/*
|
||||
* # flag_book_by_id
|
||||
*
|
||||
* External proc that Handles reporting of books. Will first get the existing flags for the book from the DB, if the report is
|
||||
* guchi, it will then add it to the list of reports for the book, encode to JSON, and update the DB
|
||||
*
|
||||
* Arguments:
|
||||
* * ckey - ckey of the player who is making the report
|
||||
* * bookid - SSID of the book being reported
|
||||
* * category_id - ID of the report category that is being used in the report
|
||||
*/
|
||||
/datum/library_catalog/proc/flag_book_by_id(ckey, bookid, category_id)
|
||||
//we should never flag a book in the DB without having the Book ID, Report Type, or Who reported it
|
||||
if(!bookid || !category_id || !ckey)
|
||||
return FALSE
|
||||
var/datum/library_category/report_type = get_report_category_by_id(category_id) //lets pull our report category datum
|
||||
if(!report_type) //is this an existing report type? If not somethings gone terribly wrong
|
||||
message_admins("WARNING: a player has attempted to flag book #[bookid] as inappropriate for a reason that does not exist, please investigate further.")
|
||||
return FALSE
|
||||
var/datum/cachedbook/reportedbook = get_book_by_id(bookid) //and now lets get what's currently on the DB
|
||||
if(!reportedbook) //does this book exist in the DB?
|
||||
message_admins("WARNING: a player has attempted to flag book #[bookid] as inappropriate for [report_type.description] but it does not exist in the Database, please investigate further.")
|
||||
return FALSE
|
||||
if(!SSdbcore.IsConnected()) //check our connection to the DB
|
||||
message_admins("WARNING: a player has attempted to flag book #[bookid] as inappropriate for [report_type.description] but the flag was not succesfully saved to the Database. Please investigate further.")
|
||||
alert("Connection to Archive has been severed. Aborting.")
|
||||
return FALSE
|
||||
|
||||
//Alright now that we've triple checked that we're ready to do this:
|
||||
//Has this player reported this book already this round?
|
||||
for(var/datum/flagged_book/book in flagged_books)
|
||||
if (book.bookid == bookid && book.reporter == ckey)
|
||||
return FALSE
|
||||
//If not, have they report this book in a previous round?
|
||||
for(var/datum/flagged_book/book in reportedbook.reports)
|
||||
if(book.reporter == ckey)
|
||||
return FALSE
|
||||
|
||||
//lets add this book to the reported_books list for the round
|
||||
var/datum/flagged_book/f = new()
|
||||
f.bookid = bookid
|
||||
f.reporter = ckey
|
||||
f.category_id = category_id
|
||||
flagged_books += f //adding to global list
|
||||
reportedbook.reports += f //adding to books var for tracking reports
|
||||
//Now we will add the report to the DB, we will build the JSON we're going to upload from our books report list
|
||||
var/list/flag_json = list()
|
||||
//Flagged book json is stored as such: "[[reporter_ckey1, report_id1],[reporter_ckey2, report_id2]]""
|
||||
for(var/datum/flagged_book/book in reportedbook.reports)
|
||||
flag_json += list(list( //yes this is intentional
|
||||
book.reporter,
|
||||
book.category_id,
|
||||
))
|
||||
//uploading our report to the library
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("UPDATE library SET reports=:report WHERE id=:id", list(
|
||||
"id" = text2num(bookid),
|
||||
"report" = json_encode(flag_json),
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
message_admins("WARNING: a player has attempted to flag book #[bookid] as inappropriate for \"[report_type.description]\" but the flag was not succesfully saved to the Database. Please investigate further.")
|
||||
qdel(query)
|
||||
return FALSE
|
||||
message_admins("[ckey] has flagged book #[bookid] as inappropriate for \"[report_type.description]\".")
|
||||
qdel(query)
|
||||
return TRUE
|
||||
|
||||
/*
|
||||
* # unflag_book_by_id
|
||||
*
|
||||
* External proc that removes all reports on a book.
|
||||
*
|
||||
* Arguments:
|
||||
* * bookid - SSID of the book being reported
|
||||
*/
|
||||
/datum/library_catalog/proc/unflag_book_by_id(bookid)
|
||||
//we should never flag a book in the DB without having the Book ID, Report Type, or Who reported it
|
||||
if(!bookid)
|
||||
return FALSE
|
||||
var/datum/cachedbook/reportedbook = get_book_by_id(bookid)
|
||||
if(!reportedbook)
|
||||
return FALSE //it don't exist
|
||||
|
||||
if(!SSdbcore.IsConnected()) //check our connection to the DB
|
||||
message_admins("WARNING: an admin has attempted to unflag book #[bookid] but it was not succesfully saved to the Database. Please investigate further.")
|
||||
return FALSE
|
||||
|
||||
//uploading our report to the library
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("UPDATE library SET reports=:report WHERE id=:id", list(
|
||||
"id" = text2num(bookid),
|
||||
"report" = json_encode(list()),
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
message_admins("WARNING: an admin has attempted to unflag book #[bookid] but it was not succesfully saved to the Database. Please investigate further.")
|
||||
qdel(query)
|
||||
return FALSE
|
||||
qdel(query)
|
||||
return TRUE
|
||||
/*
|
||||
* # remove_book_by_id
|
||||
*
|
||||
* External proc that Handles the deletion of books by SSID
|
||||
*
|
||||
* Arguments:
|
||||
* * bookid - SSID of the book being deleted
|
||||
*/
|
||||
/datum/library_catalog/proc/remove_book_by_id(bookid)
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("DELETE FROM library WHERE id=:id", list(
|
||||
"id" = text2num(bookid)
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return FALSE
|
||||
qdel(query)
|
||||
return TRUE
|
||||
|
||||
/*
|
||||
* # remove_books_by_ckey
|
||||
*
|
||||
* External proc that Handles the mass deletion of all books uploaded by a single ckey
|
||||
*
|
||||
* Arguments:
|
||||
* * ckey - ckey we will use to get all the books we want for deletion
|
||||
*/
|
||||
/datum/library_catalog/proc/remove_books_by_ckey(ckey)
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("DELETE FROM library WHERE ckey=:ckey", list(
|
||||
"ckey" = ckey
|
||||
))
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return FALSE
|
||||
qdel(query)
|
||||
return TRUE
|
||||
|
||||
/*
|
||||
* # upload_book
|
||||
*
|
||||
* External proc that handles creating new rows/uploading books to the DB
|
||||
*
|
||||
* Arguments:
|
||||
* * ckey - author's ckey that will be tied to the book uploaded
|
||||
* * datum/cachedbook/selected_book - cachedbook datum that contains all the book information to added to DB
|
||||
*/
|
||||
/datum/library_catalog/proc/upload_book(ckey, datum/cachedbook/selected_book)
|
||||
if(!ckey)
|
||||
return FALSE
|
||||
if(!selected_book.title || !selected_book.author || !length(selected_book.categories) || !length(selected_book.content))
|
||||
return FALSE
|
||||
|
||||
if(!SSdbcore.IsConnected())
|
||||
return FALSE
|
||||
|
||||
var/datum/library_user_data/search_terms = new()
|
||||
search_terms.search_ckey = ckey
|
||||
if(length(get_total_books(search_terms)) >= MAX_PLAYER_UPLOADS)
|
||||
return FALSE
|
||||
|
||||
var/sql = {"INSERT INTO library (author, title, content, summary, primary_category, secondary_category, tertiary_category, ckey, raters, reports)
|
||||
VALUES (:author, :title, :content, :summary, :primarycategory, :secondarycategory, :tertiarycategory, :ckey, :raters, :reports)"}
|
||||
|
||||
var/sql_params = list(
|
||||
"author" = selected_book.author,
|
||||
"title" = selected_book.title,
|
||||
"content" = json_encode(selected_book.content),
|
||||
"summary" = selected_book.summary ? selected_book.summary : "No Summary",
|
||||
"primarycategory" = length(selected_book.categories) >= 1 ? selected_book.categories[1] : 0,
|
||||
"secondarycategory" = length(selected_book.categories) >= 2 ? selected_book.categories[2] : 0,
|
||||
"tertiarycategory" = length(selected_book.categories) >= 3 ? selected_book.categories[3] : 0,
|
||||
"ckey" = ckey,
|
||||
"raters" = " ", //Entry for both of these columns are NOT NULL
|
||||
"reports" = " ", //so we need to provide an empty string val
|
||||
)
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery(sql, sql_params)
|
||||
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return FALSE
|
||||
|
||||
qdel(query)
|
||||
log_admin("[ckey] has uploaded the book titled [selected_book.title], [length(selected_book.content)] pages in length")
|
||||
message_admins("[ckey] has uploaded the book titled [selected_book.title], [length(selected_book.content)] pages in length")
|
||||
return TRUE
|
||||
|
||||
/*
|
||||
* # rate_book
|
||||
*
|
||||
* External proc that handles adding ratings to books in the DB. Will first get the ratings for the book from the DB
|
||||
* and then rebuild the list/JSON for the ratings. It will also calculate the new average rating for the book.
|
||||
* This proc will automatically clean out duplicate entries (2 or more ratings from the same ckey on 1 book), additionally,
|
||||
* watch out for any user inputs that are not whole numbers/integers
|
||||
*
|
||||
* Arguments:
|
||||
* * ckey - reviewer's ckey
|
||||
* * bookid - SSID of the book being rated
|
||||
* * user_rating - integer from 1 to 10
|
||||
*/
|
||||
/datum/library_catalog/proc/rate_book(ckey, bookid, user_rating)
|
||||
if(!ckey || !bookid || !user_rating || !isnum(user_rating))
|
||||
return
|
||||
if(!SSdbcore.IsConnected())
|
||||
return
|
||||
|
||||
var/list/current_ratings = get_book_ratings(bookid) // = [ratingInt, [[ckey, rating],[ckey, rating],[ckey, rating]]]
|
||||
var/list/new_raters_info = list()
|
||||
var/new_rating_value = round(user_rating, 1) //should only ever be a whole number
|
||||
|
||||
if(length(current_ratings)) //did get_book_ratings actually return something?
|
||||
for(var/rating in current_ratings[2])
|
||||
if(rating[1] == ckey)
|
||||
continue //if your ckey has an existing rating, throw it out to make room for new one
|
||||
new_raters_info += list(rating)
|
||||
new_rating_value += rating[2]
|
||||
else
|
||||
current_ratings = list()
|
||||
|
||||
new_raters_info += list(list(ckey, user_rating)) //intentional
|
||||
var/list/sql_params = list()
|
||||
sql_params["id"] = bookid
|
||||
//aggregate of ratings divided by total ratings to get average
|
||||
var/new_calculated_average = new_rating_value / length(new_raters_info)
|
||||
new_calculated_average = round(new_calculated_average, 0.1)
|
||||
sql_params["newrating"] = new_calculated_average
|
||||
sql_params["raters"] = json_encode(new_raters_info)
|
||||
|
||||
var/datum/db_query/query = SSdbcore.NewQuery("UPDATE library SET rating=:newrating, raters=:raters WHERE id=:id", sql_params)
|
||||
|
||||
if(!query.warn_execute())
|
||||
qdel(query)
|
||||
return
|
||||
qdel(query)
|
||||
return TRUE
|
||||
|
||||
#undef DEFINE_CATEGORY
|
||||
#undef MAX_PLAYER_UPLOADS
|
||||
|
||||
/* here be dragons~~~
|
||||
__ _
|
||||
_/ \ _(\(o
|
||||
/ \ / _ ^^^o
|
||||
/ ! \/ ! '!!!v'
|
||||
! ! \ _' ( \____
|
||||
! . \ _!\ \===^\)
|
||||
\ \_! / __!
|
||||
\! / \
|
||||
(\_ _/ _\ )
|
||||
\ ^^--^^ __-^ /(__
|
||||
^^----^^ "^--v'
|
||||
*/
|
||||
@@ -0,0 +1,583 @@
|
||||
///Defines how many player books appear on the player book archive TGUI tab
|
||||
#define LIBRARY_BOOKS_PER_PAGE 25
|
||||
///Login state for our computer, this state grants full access to functions
|
||||
#define LOGIN_FULL 1
|
||||
///Login state for our computer, this state grants basic access to functions
|
||||
#define LOGIN_PUBLIC 2
|
||||
///Wait time before printing another book, used to prevent spam
|
||||
#define PRINTING_COOLDOWN (5 SECONDS)
|
||||
|
||||
/**
|
||||
* # Library Computer
|
||||
*
|
||||
* This is the player facing machine that handles all library functions
|
||||
*
|
||||
* This holds all procs for handling book checkins/checkout, book fines, book obj creation/modification
|
||||
* the object also holds static lists for book inventory and checkouts. NO SQL CALLS OR QUERIES ARE MADE HERE, all
|
||||
* of those are handled by the global library catalog that we will reference, and it should stay that way :)
|
||||
*/
|
||||
/obj/machinery/computer/library
|
||||
name = "Library Computer"
|
||||
anchored = TRUE
|
||||
density = TRUE
|
||||
icon_keyboard = null
|
||||
icon_screen = "computer_on"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "computer"
|
||||
|
||||
//We define a required access only to lock library specific actions like ordering/managing books to librarian access+
|
||||
req_one_access = list(ACCESS_LIBRARY)
|
||||
///Page Number for going through player book archives
|
||||
var/archive_page_num = 1
|
||||
///report category_id we have selected
|
||||
var/selected_report
|
||||
///Total number of pages for the parameters have set for our booklist
|
||||
var/num_pages = 0
|
||||
///total inventoried books, used for setting book library IDs
|
||||
var/total_books = 0
|
||||
///list for storing player inputs and selections, helpful for cutting down on single variable declarations
|
||||
var/datum/library_user_data/user_data = new()
|
||||
///This list temporarily stores the player books we grab from the DB in datums, we only update it when we need to for performance reasons
|
||||
var/list/cached_booklist = list()
|
||||
///Static List of borrowbook datums, used to track book checkouts acrossed the library system
|
||||
var/static/list/checkouts = list()
|
||||
///Static List of book datums to track what books the librarian has added to the library inventory
|
||||
var/static/list/inventory = list()
|
||||
///How Long a book is allowed to be checked out for
|
||||
var/checkoutperiod = 15 MINUTES
|
||||
///Wait period for printing books
|
||||
var/print_cooldown = 5 SECONDS
|
||||
|
||||
|
||||
/obj/machinery/computer/library/Initialize(mapload)
|
||||
. = ..()
|
||||
populate_booklist(async = FALSE)
|
||||
//since ui_data screws up when SQL calls are made inside it,
|
||||
//we must populate our booklist before ui_act is called for the first time
|
||||
|
||||
/obj/machinery/computer/library/attack_ai(mob/user)
|
||||
return attack_hand(user)
|
||||
|
||||
/obj/machinery/computer/library/attack_hand(mob/user)
|
||||
if(..())
|
||||
return
|
||||
ui_interact(user)
|
||||
|
||||
/obj/machinery/computer/library/attack_ghost(mob/user)
|
||||
ui_interact(user)
|
||||
|
||||
/obj/machinery/computer/library/attackby(obj/item/O, mob/user, params)
|
||||
if(istype(O, /obj/item/book))
|
||||
select_book(O)
|
||||
return
|
||||
if(istype(O, /obj/item/barcodescanner))
|
||||
var/obj/item/barcodescanner/B = O
|
||||
if(!B.connect(src))
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='warning'>ERROR: No Connection Established!</span>")
|
||||
return
|
||||
to_chat(user, "<span class='notice'>Barcode Scanner Succesfully Connected to Computer.</span>")
|
||||
audible_message("[src] lets out a low, short blip.", hearing_distance = 2)
|
||||
playsound(B, 'sound/machines/terminal_select.ogg', 10, TRUE)
|
||||
return
|
||||
if(istype(O, /obj/item/card/id))
|
||||
var/obj/item/card/id/ID = O //at some point, this should be moved over to its own proc (select_patron()???)
|
||||
if(ID.registered_name)
|
||||
user_data.patron_name = ID.registered_name
|
||||
else
|
||||
user_data.patron_name = null
|
||||
user_data.patron_account = null //account number should reset every scan so we don't accidently have an account number but no name
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>ERROR: No name detected!</span>")
|
||||
return //no point in continuing if the ID card has no associated name!
|
||||
playsound(src, 'sound/items/scannerbeep.ogg', 15, TRUE)
|
||||
if(ID.associated_account_number)
|
||||
user_data.patron_account = ID.associated_account_number
|
||||
else
|
||||
user_data.patron_account = null
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'WARNING! Patron without associated account number Selected'</span>")
|
||||
return
|
||||
return ..()
|
||||
|
||||
/obj/machinery/computer/library/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = TRUE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
|
||||
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
|
||||
if(!ui)
|
||||
ui = new(user, src, ui_key, "LibraryComputer", name, 1050, 600, master_ui, state)
|
||||
ui.open()
|
||||
|
||||
/*
|
||||
* # UI Data for TGUI
|
||||
*
|
||||
* Hey friends, this proc is where we stuff our massive amounts of data into our data list to be sent to our TGUI
|
||||
* a few things about the library UI in specific, under no circumstance can any proc be called in ui_data that causes
|
||||
* our code to sleep or wait, this will crash our TGUI interface upon first opening. This means you cannot call any of our
|
||||
* procs that call a library_catalog proc that makes an SQL Query.
|
||||
*/
|
||||
/obj/machinery/computer/library/ui_data(mob/user)
|
||||
var/list/data = list()
|
||||
|
||||
data["archive_pagenumber"] = archive_page_num
|
||||
data["num_pages"] = num_pages
|
||||
var/selected_categories = list()
|
||||
selected_categories = user_data.search_categories
|
||||
|
||||
data["login_state"] = allowed(user)
|
||||
|
||||
data["searchcontent"] = list(
|
||||
"title" = user_data.search_title,
|
||||
"author" = user_data.search_author,
|
||||
"ratingmin" = user_data.search_rating["min"],
|
||||
"ratingmax" = user_data.search_rating["max"],
|
||||
"categories" = selected_categories,
|
||||
"ckey" = user_data.search_ckey,
|
||||
)
|
||||
|
||||
var/list/selected_book_data = list(
|
||||
"title" = user_data.selected_book.title ? user_data.selected_book.title : "not specified",
|
||||
"author" = user_data.selected_book.author ? user_data.selected_book.author : "not specified",
|
||||
"summary" = user_data.selected_book.summary ? user_data.selected_book.summary : "no summary",
|
||||
"copyright" = user_data.selected_book.copyright ? user_data.selected_book.copyright : FALSE,
|
||||
"categories" = user_data.selected_book.categories ? user_data.selected_book.categories : list()
|
||||
)
|
||||
|
||||
data["selectedbook"] = selected_book_data
|
||||
|
||||
//should only be generating the cached booklist when we absolutely need to
|
||||
data["external_booklist"] = cached_booklist
|
||||
data["checkout_data"] = list()
|
||||
|
||||
for(var/datum/borrowbook/b in checkouts)
|
||||
var/remaining_time = (b.duedate - world.time) / 600
|
||||
var/late = FALSE
|
||||
if(remaining_time <= 0) //if remaining time is less than zero, you're late
|
||||
late = TRUE
|
||||
remaining_time = round(remaining_time)
|
||||
|
||||
var/list/checkout_data = list(
|
||||
"timeleft" = remaining_time,
|
||||
"islate" = late,
|
||||
"title" = b.bookname,
|
||||
"libraryid" = b.libraryid,
|
||||
"patron_name" = b.patron_name
|
||||
)
|
||||
data["checkout_data"] += list(checkout_data)
|
||||
|
||||
data["inventory_list"] = list()
|
||||
for(var/book in inventory)
|
||||
var/checked_out = FALSE
|
||||
var/datum/cachedbook/CB = book
|
||||
for(var/datum/borrowbook/checkout in checkouts)
|
||||
if(CB.libraryid == checkout.libraryid)
|
||||
checked_out = TRUE
|
||||
break
|
||||
var/list/book_data = list(
|
||||
"title" = CB.title ? CB.title : "not specified",
|
||||
"author" = CB.author ? CB.author : "not specified",
|
||||
"summary" = CB.summary ? CB.summary : "no summary",
|
||||
"id" = CB.id,
|
||||
"libraryid" = CB.libraryid,
|
||||
"checked_out" = checked_out,
|
||||
)
|
||||
data["inventory_list"] += list(book_data)
|
||||
data["user_ckey"] = user?.ckey
|
||||
data["selected_report"] = selected_report
|
||||
data["selected_rating"] = user_data.selected_rating
|
||||
data["modal"] = ui_modal_data(src)
|
||||
|
||||
return data
|
||||
|
||||
/obj/machinery/computer/library/ui_static_data(mob/user)
|
||||
var/list/static_data = list()
|
||||
//Book Categories will never change within a round so they don't need to sent more than once
|
||||
static_data["book_categories"] = list()
|
||||
for(var/datum/library_category/category in GLOB.library_catalog.categories)
|
||||
var/category_info = list(
|
||||
"category_id" = category.category_id,
|
||||
"description" = category.description,
|
||||
)
|
||||
static_data["book_categories"] += list(category_info)
|
||||
|
||||
//Report Categories will never change within a round so they don't need to sent more than once
|
||||
static_data["report_categories"] = list()
|
||||
for(var/r in GLOB.library_catalog.report_types)
|
||||
var/datum/library_category/report = r
|
||||
var/report_info = list(
|
||||
"category_id" = report.category_id,
|
||||
"description" = report.description,
|
||||
)
|
||||
static_data["report_categories"] += list(report_info)
|
||||
|
||||
static_data["programmatic_booklist"] = list()
|
||||
for(var/book in GLOB.library_catalog.books)
|
||||
var/datum/programmatic_book/PB = book
|
||||
var/list/book_data = list(
|
||||
"title" = PB.title ? PB.title : "not specified",
|
||||
"author "= PB.author ? PB.author : "Nanotrasen",
|
||||
"id" = PB.id,
|
||||
)
|
||||
static_data["programmatic_booklist"] += list(book_data)
|
||||
|
||||
return static_data
|
||||
|
||||
/obj/machinery/computer/library/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
||||
if(..())
|
||||
return
|
||||
|
||||
if(ui_act_modal(action, params))
|
||||
return
|
||||
|
||||
add_fingerprint(usr)
|
||||
|
||||
switch(action)
|
||||
//Page Switching
|
||||
if("incrementpage")
|
||||
archive_page_num = clamp(archive_page_num + 1, 1, num_pages)
|
||||
populate_booklist()
|
||||
if("incrementpagemax")
|
||||
archive_page_num = num_pages
|
||||
populate_booklist()
|
||||
if("deincrementpage")
|
||||
archive_page_num = clamp(archive_page_num - 1, 1, num_pages)
|
||||
populate_booklist()
|
||||
if("deincrementpagemax")
|
||||
archive_page_num = 1
|
||||
populate_booklist()
|
||||
//Search Tools' Buttons
|
||||
if("toggle_search_category")
|
||||
var/category_id = text2num(params["category_id"])
|
||||
if(category_id in user_data.search_categories)
|
||||
user_data.search_categories -= category_id
|
||||
populate_booklist()
|
||||
else
|
||||
user_data.search_categories += category_id
|
||||
populate_booklist()
|
||||
if("clear_search")
|
||||
user_data.clear_search()
|
||||
populate_booklist()
|
||||
if("find_users_books")
|
||||
user_data.clear_search() //we need to clear out other search params first
|
||||
user_data.search_ckey = params["user_ckey"]
|
||||
populate_booklist()
|
||||
if("clear_ckey_search")
|
||||
user_data.search_ckey = null
|
||||
populate_booklist()
|
||||
|
||||
//Order Buttons
|
||||
if("order_external_book")
|
||||
var/datum/cachedbook/orderedbook = GLOB.library_catalog.get_book_by_id(params["bookid"])
|
||||
if(orderedbook && print_cooldown <= world.time)
|
||||
make_external_book(orderedbook)
|
||||
print_cooldown = world.time + PRINTING_COOLDOWN
|
||||
if("order_programmatic_book")
|
||||
var/datum/programmatic_book/PB = GLOB.library_catalog.get_programmatic_book_by_id(params["bookid"])
|
||||
if(PB && print_cooldown <= world.time)
|
||||
make_programmatic_book(PB)
|
||||
print_cooldown = world.time + PRINTING_COOLDOWN
|
||||
//book author actions
|
||||
if("delete_book")
|
||||
if(params["bookid"])
|
||||
var/datum/cachedbook/selectedbook = GLOB.library_catalog.get_book_by_id(params["bookid"])
|
||||
if(!selectedbook)
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
atom_say("Deletion Failed!")
|
||||
return
|
||||
if(selectedbook.ckey != params["user_ckey"])
|
||||
message_admins("[params["user_ckey"]] attempted to delete a book that wasn't theirs, this shouldn't happen, please investigate.")
|
||||
return
|
||||
if(GLOB.library_catalog.remove_book_by_id(params["bookid"])) //this doesn't need to be logged
|
||||
playsound(loc, 'sound/machines/ping.ogg', 25, 0)
|
||||
atom_say("Deletion Succesful!")
|
||||
return
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
atom_say("Deletion Failed!")
|
||||
|
||||
|
||||
//rating acts
|
||||
if("set_rating")
|
||||
if(params["rating_value"])
|
||||
user_data.selected_rating = text2num(params["rating_value"])
|
||||
if("rate_book")
|
||||
if(GLOB.library_catalog.rate_book(params["user_ckey"], params["bookid"], user_data.selected_rating))
|
||||
playsound(loc, 'sound/machines/ping.ogg', 25, 0)
|
||||
atom_say("Rating Succesful!")
|
||||
populate_booklist()
|
||||
//Report Acts
|
||||
if("submit_report")
|
||||
if(GLOB.library_catalog.flag_book_by_id(params["user_ckey"], params["bookid"], selected_report))
|
||||
playsound(loc, 'sound/machines/ping.ogg', 50, 0)
|
||||
atom_say("Report Submitted!")
|
||||
return
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
atom_say("Report Submission Failed!")
|
||||
if("set_report")
|
||||
selected_report = text2num(params["report_type"])
|
||||
//Book Uploader
|
||||
if("toggle_upload_category")
|
||||
if(text2num(params["category_id"]) in user_data.selected_book.categories)
|
||||
user_data.selected_book.categories -= text2num(params["category_id"])
|
||||
populate_booklist()
|
||||
else
|
||||
if(length(user_data.selected_book.categories) >= 3)
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
return
|
||||
user_data.selected_book.categories += text2num(params["category_id"])
|
||||
populate_booklist()
|
||||
if("uploadbook")
|
||||
if(GLOB.library_catalog.upload_book(params["user_ckey"], user_data.selected_book))
|
||||
playsound(src, 'sound/machines/ping.ogg', 50, 0)
|
||||
atom_say("Book Uploaded!")
|
||||
return
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
atom_say("Book Upload Failed!")
|
||||
num_pages = getmaxpages()
|
||||
if("reportlost")
|
||||
inventoryRemove(text2num(params["libraryid"]))
|
||||
for(var/datum/borrowbook/book in checkouts)
|
||||
if(book.libraryid == text2num(params["libraryid"]))
|
||||
checkouts -= book
|
||||
|
||||
|
||||
/obj/machinery/computer/library/proc/ui_act_modal(action, list/params)
|
||||
. = TRUE
|
||||
var/id = params["id"] // The modal's ID
|
||||
var/list/arguments = istext(params["arguments"]) ? json_decode(params["arguments"]) : params["arguments"]
|
||||
switch(ui_modal_act(src, action, params))
|
||||
if(UI_MODAL_OPEN)
|
||||
switch(id)
|
||||
if("setpagenumber")
|
||||
ui_modal_input(src, id, "Please input a page number:", null, arguments, archive_page_num)
|
||||
//search inputs
|
||||
if("edit_search_title")
|
||||
ui_modal_input(src, id, "Please input the new title:", null, arguments, user_data.search_title)
|
||||
if("edit_search_author")
|
||||
ui_modal_input(src, id, "Please input the new author:", null, arguments, user_data.search_author)
|
||||
if("edit_search_ratingmax")
|
||||
ui_modal_input(src, id, "Please input the new upper rating bound:", null, arguments, user_data.search_rating["max"])
|
||||
if("edit_search_ratingmin")
|
||||
ui_modal_input(src, id, "Please input the new lower rating bound:", null, arguments, user_data.search_rating["min"])
|
||||
//book uploader inputs
|
||||
if("edit_selected_title")
|
||||
ui_modal_input(src, id, "Please input the new title:", null, arguments, user_data.selected_book.title)
|
||||
if("edit_selected_author")
|
||||
ui_modal_input(src, id, "Please input the new author:", null, arguments, user_data.selected_book.author)
|
||||
if("edit_selected_summary")
|
||||
ui_modal_input(src, id, "Please input the new summary:", null, arguments, user_data.selected_book.summary)
|
||||
//book list buttons
|
||||
if("expand_info")
|
||||
var/datum/programmatic_book/PB = GLOB.library_catalog.get_programmatic_book_by_id(arguments["bookid"])
|
||||
if(PB)
|
||||
ui_modal_message(src, id, "", arguments = list(
|
||||
"isProgrammatic" = TRUE,
|
||||
"title" = PB.title,
|
||||
"author" = PB.author,
|
||||
"summary" = PB.summary ? PB.summary : "No Summary Provided",
|
||||
"rating" = "N for Nanotrasen",
|
||||
))
|
||||
|
||||
return //If we've succesfully opened the modal for our programmatic book, we don't need to do more logic
|
||||
var/datum/cachedbook/CB = GLOB.library_catalog.get_book_by_id(arguments["bookid"])
|
||||
if(CB)
|
||||
var/category_names = list()
|
||||
for(var/datum/library_category/category in CB.categories)
|
||||
category_names += category.description
|
||||
user_data.selected_report = null
|
||||
user_data.selected_rating = 0
|
||||
|
||||
ui_modal_message(src, id, "", arguments = list(
|
||||
"isProgrammatic" = FALSE,
|
||||
"id" = CB.id,
|
||||
"ckey" = CB.ckey,
|
||||
"title" = CB.title,
|
||||
"author" = CB.author,
|
||||
"summary" = CB.summary ? CB.summary : "No Summary Provided",
|
||||
"rating" = CB.rating ? CB.rating : 0,
|
||||
"categories" = category_names,
|
||||
))
|
||||
if("report_book")
|
||||
var/datum/cachedbook/CB = GLOB.library_catalog.get_book_by_id(arguments["bookid"])
|
||||
ui_modal_message(src, id, "", arguments = list(
|
||||
id = CB.id,
|
||||
title = CB.title,
|
||||
ckey = CB.ckey,
|
||||
))
|
||||
if("rate_info")
|
||||
var/datum/cachedbook/CB = GLOB.library_catalog.get_book_by_id(arguments["bookid"])
|
||||
var/list/book_ratings = GLOB.library_catalog.get_book_ratings(arguments["bookid"])
|
||||
ui_modal_message(src, id, "", arguments = list(
|
||||
"id" = CB.id,
|
||||
"title" = CB.title,
|
||||
"author" = CB.author,
|
||||
"ckey" = CB.ckey,
|
||||
"current_rating" = length(book_ratings) ? book_ratings[1] : 0,
|
||||
"total_ratings" = length(book_ratings) ? length(book_ratings[2]) : 0,
|
||||
))
|
||||
else
|
||||
return FALSE
|
||||
if(UI_MODAL_ANSWER)
|
||||
var/answer = sanitize(params["answer"]) //xss attacks bad
|
||||
switch(id)
|
||||
if("edit_search_title")
|
||||
if(!length(answer))
|
||||
user_data.search_title = null
|
||||
populate_booklist()
|
||||
return
|
||||
if(length(answer) >= MAX_NAME_LEN)
|
||||
return
|
||||
user_data.search_title = answer
|
||||
populate_booklist()
|
||||
if("edit_search_author")
|
||||
if(!length(answer))
|
||||
user_data.search_author = null
|
||||
populate_booklist()
|
||||
return
|
||||
if(length(answer) >= MAX_NAME_LEN)
|
||||
return
|
||||
user_data.search_author = answer
|
||||
populate_booklist()
|
||||
if("edit_search_ratingmax")
|
||||
if(!text2num(answer))
|
||||
return
|
||||
user_data.search_rating["max"] = clamp(text2num(answer), user_data.search_rating["min"], 10)
|
||||
populate_booklist()
|
||||
if("edit_search_ratingmin")
|
||||
if(!text2num(answer))
|
||||
return
|
||||
user_data.search_rating["min"] = clamp(text2num(answer), 0, user_data.search_rating["max"])
|
||||
populate_booklist()
|
||||
if("edit_selected_title")
|
||||
if(length(answer) >= MAX_NAME_LEN)
|
||||
return
|
||||
user_data.selected_book.title = answer
|
||||
if("edit_selected_author")
|
||||
if(length(answer) >= MAX_NAME_LEN)
|
||||
return
|
||||
user_data.selected_book.author = answer
|
||||
if("edit_selected_summary")
|
||||
if(length(answer) >= MAX_SUMMARY_LEN)
|
||||
return
|
||||
user_data.selected_book.summary = answer
|
||||
if("setpagenumber")
|
||||
if(!text2num(answer))
|
||||
return
|
||||
archive_page_num = clamp(text2num(answer), 1, getmaxpages())
|
||||
populate_booklist()
|
||||
else
|
||||
return FALSE
|
||||
else
|
||||
return FALSE
|
||||
|
||||
/obj/machinery/computer/library/proc/select_book(obj/item/book/B)
|
||||
if(B.carved == TRUE)
|
||||
return
|
||||
user_data.selected_book.title = B.title ? B.title : "No Title"
|
||||
user_data.selected_book.author = B.author ? B.author : "No Author"
|
||||
user_data.selected_book.summary = B.summary ? B.summary : "No Summary"
|
||||
user_data.selected_book.copyright = B.copyright ? B.copyright : FALSE
|
||||
user_data.selected_book.content = B.pages ? B.pages : list()
|
||||
user_data.selected_book.categories = B.categories ? B.categories : list()
|
||||
|
||||
/obj/machinery/computer/library/proc/inventoryAdd(obj/item/book/B) //add book to library inventory
|
||||
for(var/datum/cachedbook/I in inventory)
|
||||
if(I.libraryid == B.libraryid)
|
||||
return FALSE
|
||||
if(!B.libraryid)
|
||||
total_books++
|
||||
B.libraryid = total_books
|
||||
var/datum/cachedbook/CB = new()
|
||||
CB.serialize_book(B)
|
||||
if(!CB)
|
||||
return
|
||||
inventory.Add(CB)
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/computer/library/proc/inventoryRemove(libraryID) //remove book from library inventory
|
||||
for(var/datum/cachedbook/O in inventory)
|
||||
if(O.libraryid == libraryID)
|
||||
inventory.Remove(O)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/obj/machinery/computer/library/proc/checkout(obj/item/book/B) //checkout book
|
||||
if(!B.libraryid || !user_data.patron_name) //If book isn't a library book or there isn't a selected patron: return
|
||||
return FALSE
|
||||
for(var/datum/borrowbook/O in checkouts) //is this book already checked out?
|
||||
if(O.libraryid == B.libraryid)
|
||||
return FALSE
|
||||
var/datum/borrowbook/P = new /datum/borrowbook
|
||||
P.bookname = sanitize(B.title)
|
||||
P.libraryid = B.libraryid
|
||||
P.patron_name = sanitize(user_data.patron_name)
|
||||
P.patron_account = sanitize(user_data.patron_account)
|
||||
P.duedate = world.time + (checkoutperiod)
|
||||
checkouts.Add(P)
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/computer/library/proc/checkin(obj/item/book/B) //check back in a book
|
||||
if(!B.libraryid)
|
||||
return FALSE
|
||||
for(var/datum/borrowbook/O in checkouts) //is this book checked out?
|
||||
if(O.libraryid == B.libraryid)
|
||||
checkouts.Remove(O)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/*
|
||||
* # populate_booklist
|
||||
*
|
||||
* internal proc that will refresh our cached booklist, it needs to be called everytime we are switching parameters
|
||||
* that will affect what books will be displayed in our TGUI player book archive.
|
||||
*/
|
||||
/obj/machinery/computer/library/proc/populate_booklist(async = TRUE)
|
||||
cached_booklist = list() //clear old list
|
||||
var/starting_book = (archive_page_num - 1) * LIBRARY_BOOKS_PER_PAGE
|
||||
var/range = LIBRARY_BOOKS_PER_PAGE
|
||||
for(var/datum/cachedbook/CB in GLOB.library_catalog.get_book_by_range(starting_book, range, user_data, async))
|
||||
//instead of just adding the datum to the cached_booklist, we want to make it an assoc list so we can just give it to the TGUI
|
||||
|
||||
var/list/book_data = list(
|
||||
"id" = CB.id,
|
||||
"title" = CB.title,
|
||||
"author" = CB.author,
|
||||
"rating" = CB.rating,
|
||||
"summary" = CB.summary,
|
||||
"ckey" = CB.ckey,
|
||||
"reports" = CB.reports,
|
||||
)
|
||||
book_data["categories"] = list()
|
||||
for(var/category in CB.categories)
|
||||
var/datum/library_category/book_category = GLOB.library_catalog.get_book_category_by_id(category)
|
||||
if(book_category)
|
||||
book_data["categories"] += book_category.description //we're displaying the cats onlys, so we don't need the ids
|
||||
|
||||
cached_booklist += list(book_data)
|
||||
num_pages = getmaxpages()
|
||||
archive_page_num = clamp(archive_page_num, 1, num_pages)
|
||||
|
||||
///Returns the amount of pages we will need to hold all the book our DB has found
|
||||
/obj/machinery/computer/library/proc/getmaxpages()
|
||||
//if get_total_books doesn't return anything, just set pages to 1 so we don't break stuff
|
||||
var/book_count = max(1, GLOB.library_catalog.get_total_books(user_data))
|
||||
var/page_count = round(book_count / LIBRARY_BOOKS_PER_PAGE)
|
||||
//Since 'round' gets the floor value it's likely there will be 1 page more than
|
||||
//the page count amount (almost guaranteed), we check for a remainder because of this
|
||||
if(book_count % LIBRARY_BOOKS_PER_PAGE)
|
||||
page_count++
|
||||
return page_count
|
||||
|
||||
/obj/machinery/computer/library/proc/make_external_book(datum/cachedbook/newbook)
|
||||
if(!newbook?.id)
|
||||
return
|
||||
new /obj/item/book(loc, newbook, TRUE, FALSE)
|
||||
visible_message("<span class='notice'>[src]'s printer hums as it produces a completely bound book. How did it do that?</span>")
|
||||
|
||||
/obj/machinery/computer/library/proc/make_programmatic_book(datum/programmatic_book/newbook)
|
||||
if(!newbook?.book_type)
|
||||
return
|
||||
|
||||
new newbook.book_type(loc)
|
||||
visible_message("<span class='notice'>[src]'s printer hums as it produces a completely bound book. How did it do that?</span>")
|
||||
|
||||
#undef LIBRARY_BOOKS_PER_PAGE
|
||||
#undef LOGIN_FULL
|
||||
#undef LOGIN_PUBLIC
|
||||
#undef PRINTING_COOLDOWN
|
||||
@@ -0,0 +1,134 @@
|
||||
|
||||
/*
|
||||
* # Library User Data Datum
|
||||
*
|
||||
* Because facilitating an entire library system that needs to be able to search a DB + move lots and lots of data
|
||||
* the temporary data used for functions has been condensed into a single datum
|
||||
*/
|
||||
/datum/library_user_data
|
||||
var/search_title
|
||||
var/search_author
|
||||
var/search_ckey
|
||||
var/search_rating = list(
|
||||
"min" = 0,
|
||||
"max" = 10,
|
||||
)
|
||||
var/search_categories = list()
|
||||
var/selected_rating = 0
|
||||
var/patron_name
|
||||
var/patron_account
|
||||
var/datum/cachedbook/selected_book = new()
|
||||
var/datum/library_category/selected_report
|
||||
|
||||
/datum/library_user_data/proc/clear_search()
|
||||
search_title = null
|
||||
search_author = null
|
||||
search_ckey = null
|
||||
search_rating["min"] = 0
|
||||
search_rating["max"] = 10
|
||||
search_categories = list()
|
||||
|
||||
/*
|
||||
* # Borrowbook datum
|
||||
*
|
||||
* Used for tracking books that have been checked out from the library by players. Created and stored upon a book being
|
||||
* checked out and deleted upon the book being succesfully checked back in or the librarian marking a book as "lost"
|
||||
*/
|
||||
/datum/borrowbook // Datum used to keep track of who has borrowed what when and for how long.
|
||||
var/bookname
|
||||
var/libraryid
|
||||
var/patron_name
|
||||
var/patron_account //Patron's Account ID, used for deducting $credits$ from their account
|
||||
var/duedate
|
||||
|
||||
/*
|
||||
* # Cachedbook datum
|
||||
*
|
||||
* Used for holding book data sourced from the Database in limbo to be used whenever the library computer needs it, these
|
||||
* are designed to only temporarily hold book data
|
||||
* checked out and deleted upon the book being succesfully checked back in or the librarian marking a book as "lost"
|
||||
*/
|
||||
/datum/cachedbook // Datum used to cache the SQL DB books locally in order to achieve a performance gain.
|
||||
var/id
|
||||
var/libraryid
|
||||
var/title
|
||||
var/list/content = list()
|
||||
var/summary
|
||||
var/author
|
||||
var/rating
|
||||
var/copyright
|
||||
var/ckey //administrative tracking/tooling purposes
|
||||
var/list/categories = list()
|
||||
var/reports = list()
|
||||
|
||||
///helper proc to turn our returned query rows into a cachedbook datum
|
||||
/datum/cachedbook/proc/LoadFromRow(list/row)
|
||||
id = row["id"]
|
||||
author = row["author"]
|
||||
title = row["title"]
|
||||
content = json_decode(row["content"])
|
||||
summary = row["summary"]
|
||||
rating = row["rating"]
|
||||
if(text2num(row["primary_category"]))
|
||||
categories += text2num(row["primary_category"])
|
||||
if(text2num(row["primary_category"]))
|
||||
categories += text2num(row["secondary_category"])
|
||||
if(text2num(row["primary_category"]))
|
||||
categories += text2num(row["tertiary_category"])
|
||||
ckey = row["ckey"]
|
||||
var/list/reports_json = list()
|
||||
if(length(row["reports"]) > 5) //do we actually have a string with content??
|
||||
reports_json = json_decode(row["reports"])
|
||||
for(var/r in reports_json)
|
||||
var/datum/library_category/report_category = GLOB.library_catalog.get_report_category_by_id(r[2])
|
||||
var/datum/flagged_book/report = new()
|
||||
report.bookid = id
|
||||
report.category_id = report_category.category_id
|
||||
report.reporter = r[1]
|
||||
reports += report
|
||||
|
||||
/datum/cachedbook/proc/serialize_book(obj/item/book/B)
|
||||
title = B.title ? B.title : "Unnamed"
|
||||
author = B.author ? B.author : "Anonymous"
|
||||
if(length(B.pages)) //just incase we run a book with no pages
|
||||
content = B.pages
|
||||
else
|
||||
content = list()
|
||||
summary = B.summary ? B.summary : "No summary provided"
|
||||
rating = B.rating ? B.rating : 0
|
||||
copyright = B.copyright ? B.copyright : FALSE
|
||||
libraryid = B.libraryid
|
||||
|
||||
/*
|
||||
* # Programmaticbook datum
|
||||
*
|
||||
* Used for holding book data from books that have been "hardcoded" such as manuals.
|
||||
*/
|
||||
/datum/programmatic_book
|
||||
var/id
|
||||
var/title
|
||||
var/author
|
||||
var/book_type
|
||||
var/summary
|
||||
|
||||
/datum/flagged_book
|
||||
///book id of the book this flag is attached to
|
||||
var/bookid
|
||||
///The ckey of the player who reported it
|
||||
var/reporter
|
||||
///the id of the report category
|
||||
var/category_id
|
||||
|
||||
/*
|
||||
* # library_category datum
|
||||
*
|
||||
* Used for storing information about library categories. This is used both for "book categories" like genre/purpose
|
||||
* and also for defining OOC Report types to facilitate the reporting and deleting of bad books
|
||||
*/
|
||||
/datum/library_category
|
||||
var/category_id
|
||||
var/description //The front-facing text that the user sees
|
||||
|
||||
/datum/library_category/New(_category_id, _description)
|
||||
category_id = _category_id
|
||||
description = _description
|
||||
@@ -0,0 +1,424 @@
|
||||
#define BARCODE_MODE_SCAN_SELECT 1
|
||||
#define BARCODE_MODE_SCAN_INVENTORY 2
|
||||
#define BARCODE_MODE_CHECKOUT 3
|
||||
#define BARCODE_MODE_CHECKIN 4
|
||||
|
||||
/*
|
||||
* Bookcase
|
||||
*/
|
||||
|
||||
/obj/structure/bookcase
|
||||
name = "bookcase"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "bookshelf-0"
|
||||
anchored = TRUE
|
||||
density = TRUE
|
||||
opacity = TRUE
|
||||
resistance_flags = FLAMMABLE
|
||||
max_integrity = 200
|
||||
armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 0, RAD = 0, FIRE = 50, ACID = 0)
|
||||
var/list/allowed_books = list(/obj/item/book, /obj/item/spellbook, /obj/item/storage/bible, /obj/item/tome) //Things allowed in the bookcase
|
||||
|
||||
/obj/structure/bookcase/attackby(obj/item/O, mob/user)
|
||||
if(is_type_in_list(O, allowed_books))
|
||||
if(!user.drop_item())
|
||||
return
|
||||
O.forceMove(src)
|
||||
update_icon()
|
||||
return TRUE
|
||||
if(istype(O, /obj/item/storage/bag/books))
|
||||
var/obj/item/storage/bag/books/B = O
|
||||
for(var/obj/item/T in B.contents)
|
||||
if(is_type_in_list(T, allowed_books))
|
||||
B.remove_from_storage(T, src)
|
||||
to_chat(user, "<span class='notice'>You empty [O] into [src].</span>")
|
||||
update_icon()
|
||||
return TRUE
|
||||
if(istype(O, /obj/item/pen))
|
||||
rename_interactive(user, O)
|
||||
return TRUE
|
||||
|
||||
return ..()
|
||||
|
||||
/obj/structure/bookcase/attack_hand(mob/user)
|
||||
if(!length(contents))
|
||||
return
|
||||
|
||||
var/obj/item/book/choice = input(user, "Which book would you like to remove from [src]?") as null|anything in contents
|
||||
if(!choice)
|
||||
return
|
||||
if(user.incapacitated() || !Adjacent(user))
|
||||
return
|
||||
if(!user.get_active_hand())
|
||||
user.put_in_hands(choice)
|
||||
else
|
||||
choice.forceMove(get_turf(src))
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/deconstruct(disassembled = TRUE)
|
||||
new /obj/item/stack/sheet/wood(loc, 5)
|
||||
for(var/obj/item/I in contents)
|
||||
if(is_type_in_list(I, allowed_books))
|
||||
I.forceMove(get_turf(src))
|
||||
..()
|
||||
|
||||
/obj/structure/bookcase/update_icon()
|
||||
icon_state = "bookshelf-[min(length(contents), 5)]"
|
||||
|
||||
|
||||
/obj/structure/bookcase/screwdriver_act(mob/user, obj/item/I)
|
||||
if(flags & NODECONSTRUCT)
|
||||
return
|
||||
. = TRUE
|
||||
if(!I.tool_use_check(user, 0))
|
||||
return
|
||||
TOOL_ATTEMPT_DISMANTLE_MESSAGE
|
||||
if(!I.use_tool(src, user, 20, volume = I.tool_volume))
|
||||
return
|
||||
TOOL_DISMANTLE_SUCCESS_MESSAGE
|
||||
deconstruct(TRUE)
|
||||
|
||||
/obj/structure/bookcase/wrench_act(mob/user, obj/item/I)
|
||||
. = TRUE
|
||||
|
||||
default_unfasten_wrench(user, I, 0)
|
||||
|
||||
/obj/structure/bookcase/manuals/medical
|
||||
name = "Medical Manuals bookcase"
|
||||
|
||||
/obj/structure/bookcase/manuals/medical/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/medical_cloning(src)
|
||||
update_icon()
|
||||
|
||||
|
||||
/obj/structure/bookcase/manuals/engineering
|
||||
name = "Engineering Manuals bookcase"
|
||||
|
||||
/obj/structure/bookcase/manuals/engineering/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/wiki/engineering_construction(src)
|
||||
new /obj/item/book/manual/engineering_particle_accelerator(src)
|
||||
new /obj/item/book/manual/wiki/hacking(src)
|
||||
new /obj/item/book/manual/wiki/engineering_guide(src)
|
||||
new /obj/item/book/manual/engineering_singularity_safety(src)
|
||||
new /obj/item/book/manual/wiki/robotics_cyborgs(src)
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/manuals/research_and_development
|
||||
name = "R&D Manuals bookcase"
|
||||
|
||||
/obj/structure/bookcase/manuals/research_and_development/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/research_and_development(src)
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/sop
|
||||
name = "bookcase (Standard Operating Procedures)"
|
||||
|
||||
/obj/structure/bookcase/sop/Initialize()
|
||||
. = ..()
|
||||
new /obj/item/book/manual/wiki/sop_command(src)
|
||||
new /obj/item/book/manual/wiki/sop_engineering(src)
|
||||
new /obj/item/book/manual/wiki/sop_general(src)
|
||||
new /obj/item/book/manual/wiki/sop_legal(src)
|
||||
new /obj/item/book/manual/wiki/sop_medical(src)
|
||||
new /obj/item/book/manual/wiki/sop_science(src)
|
||||
new /obj/item/book/manual/wiki/sop_security(src)
|
||||
new /obj/item/book/manual/wiki/sop_service(src)
|
||||
new /obj/item/book/manual/wiki/sop_supply(src)
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bookcase/random
|
||||
var/category = null
|
||||
var/book_count = 5
|
||||
icon_state = "random_bookcase"
|
||||
anchored = TRUE
|
||||
|
||||
/obj/structure/bookcase/random/Initialize(mapload)
|
||||
. = ..()
|
||||
var/list/books = GLOB.library_catalog.get_random_book(book_count, doAsync = FALSE)
|
||||
for(var/datum/cachedbook/book as anything in books)
|
||||
new /obj/item/book(src, book, TRUE, FALSE)
|
||||
update_icon()
|
||||
|
||||
/*
|
||||
* Book binder
|
||||
*/
|
||||
/obj/machinery/bookbinder
|
||||
name = "Book Binder"
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state = "binder"
|
||||
anchored = TRUE
|
||||
density = TRUE
|
||||
|
||||
var/datum/cachedbook/selected_content = new()
|
||||
var/printing = FALSE
|
||||
|
||||
/obj/machinery/bookbinder/attack_ai(mob/user)
|
||||
return attack_hand(user)
|
||||
|
||||
/obj/machinery/bookbinder/attack_hand(mob/user)
|
||||
if(..())
|
||||
return
|
||||
ui_interact(user)
|
||||
|
||||
/obj/machinery/bookbinder/attack_ghost(mob/user)
|
||||
ui_interact(user)
|
||||
|
||||
/obj/machinery/bookbinder/wrench_act(mob/living/user, obj/item/I)
|
||||
. = ..()
|
||||
if(default_unfasten_wrench(user, I))
|
||||
power_change()
|
||||
|
||||
/obj/machinery/bookbinder/attackby(obj/item/I, mob/user)
|
||||
if(istype(I, /obj/item/paper))
|
||||
select_paper(I)
|
||||
if(istype(I, /obj/item/paper_bundle))
|
||||
select_paper_stack(I)
|
||||
if(istype(I, /obj/item/book))
|
||||
select_book(I)
|
||||
else
|
||||
return ..()
|
||||
|
||||
/obj/machinery/bookbinder/proc/select_paper(obj/item/paper/P)
|
||||
selected_content.title = P.name
|
||||
selected_content.author = null
|
||||
selected_content.content = list(P.info)
|
||||
|
||||
/obj/machinery/bookbinder/proc/select_paper_stack(obj/item/paper_bundle/P)
|
||||
selected_content.title = P.name
|
||||
selected_content.author = null
|
||||
selected_content.content = list()
|
||||
for(var/obj/item/paper/I in P.contents)
|
||||
selected_content.content += I.info
|
||||
|
||||
/obj/machinery/bookbinder/proc/select_book(obj/item/book/B)
|
||||
if(!B || B.protected || B.carved)
|
||||
return
|
||||
selected_content.title = B.title
|
||||
selected_content.author = B.author
|
||||
selected_content.author = B.summary
|
||||
selected_content.content = B.pages
|
||||
for(var/c in B.categories)
|
||||
if(c)
|
||||
selected_content.categories += c
|
||||
|
||||
/obj/machinery/bookbinder/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = TRUE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
|
||||
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
|
||||
if(!ui)
|
||||
ui = new(user, src, ui_key, "BookBinder", name, 700, 400, master_ui, state)
|
||||
ui.open()
|
||||
|
||||
/obj/machinery/bookbinder/ui_data(mob/user)
|
||||
var/list/data = list()
|
||||
|
||||
var/list/selected_book_data = list(
|
||||
"title" = selected_content.title ? selected_content.title : "not specified",
|
||||
"author" = selected_content.author ? selected_content.author : "not specified",
|
||||
"summary" = selected_content.summary ? selected_content.summary : "no summary",
|
||||
"copyright" = selected_content.copyright ? selected_content.copyright : FALSE,
|
||||
"categories" = selected_content.categories ? selected_content.categories : list()
|
||||
)
|
||||
data["selectedbook"] = selected_book_data
|
||||
data["modal"] = ui_modal_data(src)
|
||||
return data
|
||||
|
||||
/obj/machinery/bookbinder/ui_static_data(mob/user)
|
||||
var/list/static_data = list()
|
||||
|
||||
static_data["book_categories"] = list()
|
||||
for(var/datum/library_category/category in GLOB.library_catalog.categories)
|
||||
var/category_info = list(
|
||||
"category_id" = category.category_id,
|
||||
"description" = category.description,
|
||||
)
|
||||
static_data["book_categories"] += list(category_info)
|
||||
|
||||
return static_data
|
||||
|
||||
/obj/machinery/bookbinder/ui_act(action, list/params, datum/tgui/ui)
|
||||
if(..())
|
||||
return
|
||||
|
||||
if(ui_act_modal(action, params))
|
||||
return
|
||||
|
||||
add_fingerprint(ui.user)
|
||||
|
||||
switch(action)
|
||||
if("print_book")
|
||||
if(!printing)
|
||||
printing = TRUE
|
||||
visible_message("<span class='notice'>[src] begins to hum as it warms up its printing drums.</span>")
|
||||
addtimer(CALLBACK(src, .proc/print_book), 5 SECONDS)
|
||||
else
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
if("toggle_binder_category")
|
||||
var/category_id = text2num(params["category_id"])
|
||||
if(category_id in selected_content.categories)
|
||||
selected_content.categories -= category_id
|
||||
else
|
||||
if(length(selected_content.categories) >= 3)
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
return
|
||||
selected_content.categories += category_id
|
||||
|
||||
/obj/machinery/bookbinder/proc/ui_act_modal(action, list/params)
|
||||
. = TRUE
|
||||
var/id = params["id"] // The modal's ID
|
||||
var/list/arguments = istext(params["arguments"]) ? json_decode(params["arguments"]) : params["arguments"]
|
||||
switch(ui_modal_act(src, action, params))
|
||||
if(UI_MODAL_OPEN)
|
||||
switch(id)
|
||||
if("edit_selected_title")
|
||||
ui_modal_input(src, id, "Please input the new title:", null, arguments, selected_content.title)
|
||||
if("edit_selected_author")
|
||||
ui_modal_input(src, id, "Please input the new author:", null, arguments, selected_content.author)
|
||||
if("edit_selected_summary")
|
||||
ui_modal_input(src, id, "Please input the new summary:", null, arguments, selected_content.summary)
|
||||
else
|
||||
return FALSE
|
||||
if(UI_MODAL_ANSWER)
|
||||
var/answer = params["answer"]
|
||||
switch(id)
|
||||
if("edit_selected_title")
|
||||
if(length(answer) >= MAX_NAME_LEN)
|
||||
return
|
||||
selected_content.title = strip_html(answer)
|
||||
if("edit_selected_author")
|
||||
if(length(answer) >= MAX_NAME_LEN)
|
||||
return
|
||||
selected_content.author = strip_html(answer)
|
||||
if("edit_selected_summary")
|
||||
if(length(answer) >= MAX_SUMMARY_LEN)
|
||||
return
|
||||
selected_content.summary = strip_html(answer)
|
||||
else
|
||||
return FALSE
|
||||
else
|
||||
return FALSE
|
||||
|
||||
/obj/machinery/bookbinder/proc/print_book()
|
||||
visible_message("<span class='notice'>[src] whirs as it prints and binds a new book.</span>")
|
||||
new /obj/item/book(loc, selected_content, FALSE, FALSE)
|
||||
printing = FALSE
|
||||
|
||||
/*
|
||||
* Barcode Scanner
|
||||
*/
|
||||
/obj/item/barcodescanner
|
||||
name = "barcode scanner"
|
||||
desc = "A scanner used for managing library books, one can connect it the library system by tapping on a computer with it in hand."
|
||||
icon = 'icons/obj/library.dmi'
|
||||
icon_state ="scanner"
|
||||
throw_speed = 1
|
||||
throw_range = 5
|
||||
w_class = WEIGHT_CLASS_TINY
|
||||
var/list/modes = list(BARCODE_MODE_SCAN_SELECT, BARCODE_MODE_SCAN_INVENTORY, BARCODE_MODE_CHECKOUT, BARCODE_MODE_CHECKIN)
|
||||
/// Associated Library Computer, needed to perform actions
|
||||
var/obj/machinery/computer/library/computer
|
||||
var/mode = BARCODE_MODE_SCAN_SELECT
|
||||
|
||||
/obj/item/barcodescanner/attack_self(mob/user)
|
||||
if(!check_connection(user))
|
||||
return
|
||||
mode++
|
||||
if(mode > length(modes))
|
||||
mode = modes[1]
|
||||
var/modedesc
|
||||
switch(mode)
|
||||
if(BARCODE_MODE_SCAN_SELECT)
|
||||
modedesc = "Scan book to computer."
|
||||
if(BARCODE_MODE_SCAN_INVENTORY)
|
||||
modedesc = "Scan book into to general inventory."
|
||||
if(BARCODE_MODE_CHECKOUT)
|
||||
modedesc = "Checkout Book"
|
||||
if(BARCODE_MODE_CHECKIN)
|
||||
modedesc = "Checkin Book"
|
||||
else
|
||||
modedesc = "ERROR"
|
||||
playsound(src, 'sound/machines/terminal_select.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src] mode: [modedesc]</span>")
|
||||
|
||||
/obj/item/barcodescanner/proc/connect(obj/machinery/computer/library/library_computer)
|
||||
if(!istype(library_computer))
|
||||
return FALSE
|
||||
if(computer == library_computer)
|
||||
return TRUE //we're succesfully connected already, let player know it was a "succesful connection"
|
||||
|
||||
UnregisterSignal(computer, COMSIG_PARENT_QDELETING)
|
||||
computer = library_computer
|
||||
RegisterSignal(library_computer, COMSIG_PARENT_QDELETING, .proc/disconnect)
|
||||
return TRUE
|
||||
|
||||
/obj/item/barcodescanner/proc/disconnect()
|
||||
computer = null
|
||||
|
||||
/obj/item/barcodescanner/proc/scanID(obj/item/card/id/ID, mob/user)
|
||||
if(!check_connection(user))
|
||||
return
|
||||
|
||||
if(!ID.registered_name)
|
||||
computer.user_data.patron_name = null
|
||||
computer.user_data.patron_account = null //account number should reset every scan so we don't accidently have an account number but no name
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='warning'>[src]'s screen flashes: 'ERROR! No name associated with this ID Card'</span>")
|
||||
return //no point in continuing if the ID card has no associated name!
|
||||
|
||||
computer.user_data.patron_name = ID.registered_name
|
||||
playsound(src, 'sound/items/scannerbeep.ogg', 15, TRUE)
|
||||
if(!ID.associated_account_number)
|
||||
computer.user_data.patron_account = null
|
||||
to_chat(user, "<span class='warning'>[src]'s screen flashes: 'WARNING! Patron without associated account number Selected'</span>")
|
||||
return
|
||||
|
||||
computer.user_data.patron_account = ID.associated_account_number
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'Patron Selected'</span>")
|
||||
|
||||
/obj/item/barcodescanner/proc/scanBook(obj/item/book/B, mob/user as mob)
|
||||
if(!check_connection(user))
|
||||
return
|
||||
|
||||
switch(mode)
|
||||
if(BARCODE_MODE_SCAN_SELECT)
|
||||
computer.select_book(B)
|
||||
playsound(src, 'sound/machines/terminal_select.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'Book selected in library computer.'</span>")
|
||||
if(BARCODE_MODE_SCAN_INVENTORY)
|
||||
if(computer.inventoryAdd(B))
|
||||
playsound(src, 'sound/items/scannerbeep.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'Title added to general inventory.'</span>")
|
||||
else
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'Title already in general inventory.'</span>")
|
||||
if(BARCODE_MODE_CHECKOUT)
|
||||
var/confirm
|
||||
if(!computer.user_data.patron_account)
|
||||
confirm = alert("Warning: patron does not have an associated account number! Are you sure you want to checkout [B] to [computer.user_data.patron_name]?", "Confirm Checkout", "Yes", "No")
|
||||
else
|
||||
confirm = alert("Are you sure you want to checkout [B] to [computer.user_data.patron_name]?", "Confirm Checkout", "Yes", "No")
|
||||
|
||||
if(confirm == "No")
|
||||
return
|
||||
if(computer.checkout(B))
|
||||
playsound(src, 'sound/items/scannerbeep.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'Title checked out to [computer.user_data.patron_name].'</span>")
|
||||
else
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'ERROR! Book Checkout Unsuccesful.'</span>")
|
||||
if(BARCODE_MODE_CHECKIN)
|
||||
if(computer.checkin(B))
|
||||
playsound(src, 'sound/items/scannerbeep.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'Title checked back into general inventory.'</span>")
|
||||
else
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>[src]'s screen flashes: 'ERROR! Book Checkout Unsuccesful.'</span>")
|
||||
|
||||
/obj/item/barcodescanner/proc/check_connection(mob/user as mob) //fuck you null references!
|
||||
if(computer)
|
||||
return TRUE
|
||||
else
|
||||
playsound(src, 'sound/machines/synth_no.ogg', 15, TRUE)
|
||||
to_chat(user, "<span class='notice'>Please reconnect [src] to a library computer.</span>")
|
||||
return FALSE
|
||||
@@ -0,0 +1,40 @@
|
||||
//*******************************
|
||||
//
|
||||
// Library System Breakdown
|
||||
//
|
||||
//*******************************
|
||||
/*
|
||||
The Library
|
||||
------------
|
||||
A place for the crew to go, relax, and enjoy a good book.
|
||||
Aspiring authors can even self publish and submit it to the Archives
|
||||
to be chronicled in history forever - some say even persisting
|
||||
through alternate dimensions.
|
||||
*/
|
||||
|
||||
/* DB Notes:
|
||||
-We have three seperate categories columns because in a relation database you can either store things as a JSON list
|
||||
or you can be able to search them. You can't have both, which is why we have a primary, secondary, and tertiary column
|
||||
*/
|
||||
// CONTAINS:
|
||||
|
||||
// Objects:
|
||||
// - bookcase
|
||||
// - book
|
||||
// - barcode scanner
|
||||
// Machinery:
|
||||
// - library computer
|
||||
// - book binder
|
||||
// Datum:
|
||||
// - borrowbook
|
||||
// - CachedBook
|
||||
// - Library Catalog
|
||||
|
||||
|
||||
// Ideas for the future
|
||||
// ---------------------
|
||||
// - Make library equipment emaggable
|
||||
// - Books shouldn't print straight from the library computer. Make it synch with a machine like the book binder to print instead. This should consume some sort of resource.
|
||||
// - Consider porting All wiki/iframe manuals to using MediaWiki API Calls and display using TGUI
|
||||
// - DB: put in checks to automatically prevent duplicate books from being uploaded to the Database
|
||||
// - Fully implement book fining system
|
||||
@@ -1,96 +0,0 @@
|
||||
/obj/item/book/manual/random
|
||||
icon_state = "random_book"
|
||||
|
||||
/obj/item/book/manual/random/New()
|
||||
..()
|
||||
var/static/banned_books = list(/obj/item/book/manual/random, /obj/item/book/manual/nuclear)
|
||||
var/newtype = pick(subtypesof(/obj/item/book/manual) - banned_books)
|
||||
new newtype(loc)
|
||||
qdel(src)
|
||||
|
||||
/obj/item/book/random
|
||||
icon_state = "random_book"
|
||||
var/amount = 1
|
||||
var/category = null
|
||||
|
||||
/obj/item/book/random/Initialize()
|
||||
..()
|
||||
create_random_books(amount, src.loc, TRUE, category)
|
||||
qdel(src)
|
||||
|
||||
/obj/item/book/random/triple
|
||||
amount = 3
|
||||
|
||||
/obj/structure/bookcase/random
|
||||
var/category = null
|
||||
var/book_count = 2
|
||||
icon_state = "random_bookcase"
|
||||
anchored = TRUE
|
||||
|
||||
/obj/structure/bookcase/random/Initialize()
|
||||
. = ..()
|
||||
if(!book_count || !isnum(book_count))
|
||||
update_icon()
|
||||
return
|
||||
book_count += pick(-1,-1,0,1,1)
|
||||
create_random_books(book_count, src, FALSE, category)
|
||||
update_icon()
|
||||
|
||||
// why is this a global proc
|
||||
/proc/create_random_books(amount = 2, location, fail_loud = FALSE, category = null)
|
||||
. = list()
|
||||
if(!isnum(amount) || amount<1)
|
||||
return
|
||||
if(!SSdbcore.IsConnected())
|
||||
if(fail_loud || prob(5))
|
||||
var/obj/item/paper/P = new(location)
|
||||
P.info = "There once was a book from Nantucket<br>But the database failed us, so f*$! it.<br>I tried to be good to you<br>Now this is an I.O.U<br>If you're feeling entitled, well, stuff it!<br><br><font color='gray'>~</font>"
|
||||
P.update_icon()
|
||||
return
|
||||
if(prob(25))
|
||||
category = null
|
||||
var/c = ""
|
||||
var/list/sql_params = list()
|
||||
if(category)
|
||||
c = " AND category=:category"
|
||||
sql_params["category"] = category
|
||||
|
||||
sql_params["amount"] = amount
|
||||
var/datum/db_query/query_get_random_books = SSdbcore.NewQuery("SELECT author, title, content FROM library WHERE (isnull(flagged) OR flagged = 0)[c] GROUP BY title ORDER BY rand() LIMIT :amount", sql_params)
|
||||
if(!query_get_random_books.warn_execute())
|
||||
qdel(query_get_random_books)
|
||||
return
|
||||
|
||||
while(query_get_random_books.NextRow())
|
||||
var/obj/item/book/B = new(location)
|
||||
. += B
|
||||
B.author = query_get_random_books.item[1]
|
||||
B.title = query_get_random_books.item[2]
|
||||
B.dat = query_get_random_books.item[3]
|
||||
B.name = "Book: [B.title]"
|
||||
B.icon_state= "book[rand(1,8)]"
|
||||
qdel(query_get_random_books)
|
||||
|
||||
/obj/structure/bookcase/random/fiction
|
||||
name = "bookcase (Fiction)"
|
||||
category = "Fiction"
|
||||
/obj/structure/bookcase/random/nonfiction
|
||||
name = "bookcase (Non-Fiction)"
|
||||
category = "Non-fiction"
|
||||
/obj/structure/bookcase/random/religion
|
||||
name = "bookcase (Religion)"
|
||||
category = "Religion"
|
||||
/obj/structure/bookcase/random/adult
|
||||
name = "bookcase (Adult)"
|
||||
category = "Adult"
|
||||
|
||||
/obj/structure/bookcase/random/reference
|
||||
name = "bookcase (Reference)"
|
||||
category = "Reference"
|
||||
var/ref_book_prob = 20
|
||||
|
||||
/obj/structure/bookcase/random/reference/Initialize(mapload)
|
||||
. = ..()
|
||||
while(book_count > 0 && prob(ref_book_prob))
|
||||
book_count--
|
||||
new /obj/item/book/manual/random(src)
|
||||
@@ -23,7 +23,8 @@
|
||||
if(istype(O,/obj/item/book))
|
||||
if(volume >= 5)
|
||||
var/obj/item/book/affectedbook = O
|
||||
affectedbook.dat = null
|
||||
for(var/page in affectedbook.pages)
|
||||
affectedbook.pages[page] = " " //we're blanking the pages not making em null
|
||||
affectedbook.visible_message("<span class='notice'>The solution melts away the ink on the book.</span>")
|
||||
else
|
||||
O.visible_message("<span class='warning'>It wasn't enough...</span>")
|
||||
|
||||
Reference in New Issue
Block a user