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:
Sirryan2002
2022-07-09 09:55:12 -04:00
committed by GitHub
parent 49817b4122
commit f2813a6730
62 changed files with 4366 additions and 2712 deletions
+2 -4
View File
@@ -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,
-58
View File
@@ -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
-64
View File
@@ -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)
+377
View File
@@ -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
-11
View File
@@ -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
-110
View File
@@ -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)
-477
View File
@@ -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?")
-127
View File
@@ -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
-327
View File
@@ -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")
-225
View File
@@ -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 ..()
-61
View File
@@ -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.
+206
View File
@@ -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
+653
View File
@@ -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'
*/
+583
View File
@@ -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
+134
View File
@@ -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
+424
View File
@@ -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
+40
View File
@@ -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
-96
View File
@@ -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>")