diff --git a/SQL/paradise_schema.sql b/SQL/paradise_schema.sql
index 9af8a0e261d..36381013065 100644
--- a/SQL/paradise_schema.sql
+++ b/SQL/paradise_schema.sql
@@ -401,6 +401,7 @@ CREATE TABLE `library` (
`content` text NOT NULL,
`category` text NOT NULL,
`ckey` varchar(45) NOT NULL,
+ `flagged` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=929 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
diff --git a/SQL/paradise_schema_prefixed.sql b/SQL/paradise_schema_prefixed.sql
index e2ea56d7582..e599326ba8e 100644
--- a/SQL/paradise_schema_prefixed.sql
+++ b/SQL/paradise_schema_prefixed.sql
@@ -401,6 +401,7 @@ CREATE TABLE `SS13_library` (
`content` text NOT NULL,
`category` text NOT NULL,
`ckey` varchar(45) NOT NULL,
+ `flagged` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=929 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
diff --git a/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm b/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm
index 6324c5fb02d..79e904ed62f 100644
--- a/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm
@@ -152,7 +152,7 @@
"acV" = (/obj/structure/window/reinforced{dir = 4},/obj/structure/cable/yellow{d1 = 4; d2 = 8; icon_state = "4-8"},/obj/structure/sink/kitchen{desc = "A sink used for washing one's hands and face. It looks rusty and home-made"; name = "old sink"; pixel_y = 28},/turf/simulated/floor/plasteel{icon_state = "freezerfloor"},/area/security/prison)
"acW" = (/obj/structure/cable/yellow{d1 = 4; d2 = 8; icon_state = "4-8"},/obj/structure/flora/kirbyplants{tag = "icon-plant-13"; icon_state = "plant-13"},/turf/simulated/floor/plasteel{icon_state = "floorgrime"},/area/security/prison)
"acX" = (/obj/structure/table,/obj/item/weapon/folder,/obj/item/weapon/paper/hydroponics,/obj/structure/cable/yellow{d1 = 1; d2 = 2; icon_state = "1-2"},/obj/item/weapon/pen,/turf/simulated/floor/plasteel{icon_state = "floorgrime"},/area/security/prison)
-"acY" = (/obj/structure/table,/obj/machinery/librarypubliccomp,/turf/simulated/floor/plasteel{icon_state = "floorgrime"},/area/security/prison)
+"acY" = (/obj/structure/table,/obj/machinery/computer/library/public,/turf/simulated/floor/plasteel{icon_state = "floorgrime"},/area/security/prison)
"acZ" = (/obj/machinery/computer/arcade,/obj/machinery/computer/security/telescreen/entertainment{pixel_x = 0; pixel_y = 32},/turf/simulated/floor/plasteel{icon_state = "floorgrime"},/area/security/prison)
"ada" = (/obj/machinery/camera{c_tag = "Prison Chamber"; network = list("SS13","Prison")},/obj/item/device/radio/intercom/locked/prison{dir = 2; name = "Prison Intercom (General)"; pixel_x = 0; pixel_y = 24},/turf/simulated/floor/plasteel,/area/security/prison)
"adb" = (/obj/structure/cable/yellow{d1 = 1; d2 = 2; icon_state = "1-2"},/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,/obj/structure/cable/yellow{d1 = 2; d2 = 4; icon_state = "2-4"},/turf/simulated/floor/plasteel{icon_state = "floorgrime"},/area/security/prison)
@@ -4147,7 +4147,7 @@
"bBM" = (/obj/structure/cable/yellow{d1 = 1; d2 = 2; icon_state = "1-2"},/obj/machinery/atmospherics/pipe/simple/hidden/supply,/turf/simulated/floor/plating{icon_state = "warnplate"; dir = 1},/area/maintenance/fpmaint2{name = "Port Maintenance"})
"bBN" = (/obj/structure/closet,/obj/effect/decal/cleanable/cobweb2,/obj/item/weapon/contraband/poster,/obj/effect/spawner/lootdrop/maintenance,/turf/simulated/floor/plating,/area/maintenance/fpmaint2{name = "Port Maintenance"})
"bBO" = (/obj/structure/table/woodentable,/obj/item/device/flashlight/lamp/green{pixel_x = 1; pixel_y = 5},/obj/machinery/computer/security/telescreen/entertainment{pixel_x = -32; pixel_y = 0},/turf/simulated/floor/wood,/area/library)
-"bBP" = (/obj/structure/table/woodentable,/obj/machinery/librarypubliccomp,/turf/simulated/floor/wood,/area/library)
+"bBP" = (/obj/structure/table/woodentable,/obj/machinery/computer/library/public,/turf/simulated/floor/wood,/area/library)
"bBQ" = (/turf/simulated/floor/carpet,/area/library)
"bBR" = (/obj/machinery/atmospherics/pipe/simple/hidden/supply{req_access_txt = 1},/turf/simulated/floor/carpet,/area/library)
"bBS" = (/obj/structure/stool/bed/chair/comfy/black{dir = 8},/turf/simulated/floor/wood,/area/library)
@@ -4886,7 +4886,7 @@
"bPX" = (/turf/simulated/floor/wood{tag = "icon-wood-broken5"; icon_state = "wood-broken5"},/area/library)
"bPY" = (/obj/machinery/door/window/northright{base_state = "left"; dir = 8; icon_state = "left"; name = "Library Desk Door"; pixel_x = 3; req_access_txt = "37"},/turf/simulated/floor/wood,/area/library)
"bPZ" = (/obj/effect/landmark/start{name = "Librarian"},/obj/structure/stool/bed/chair/office/dark{dir = 1},/turf/simulated/floor/wood,/area/library)
-"bQa" = (/obj/structure/table/woodentable,/obj/structure/noticeboard{desc = "A board for pinning important notices upon. Probably helpful for keeping track of requests."; name = "requests board"; pixel_x = 32; pixel_y = 0},/obj/machinery/librarycomp,/turf/simulated/floor/wood,/area/library)
+"bQa" = (/obj/structure/table/woodentable,/obj/structure/noticeboard{desc = "A board for pinning important notices upon. Probably helpful for keeping track of requests."; name = "requests board"; pixel_x = 32; pixel_y = 0},/obj/machinery/computer/library/checkout,/turf/simulated/floor/wood,/area/library)
"bQb" = (/obj/structure/closet/firecloset,/turf/simulated/floor/plasteel{icon_state = "dark"},/area/library)
"bQc" = (/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,/obj/structure/extinguisher_cabinet{pixel_x = 27; pixel_y = 0},/turf/simulated/floor/plasteel{dir = 4; icon_state = "neutralcorner"},/area/hallway/primary/central)
"bQd" = (/obj/machinery/suit_storage_unit/standard_unit,/turf/simulated/floor/plasteel{tag = "icon-vault (WEST)"; icon_state = "vault"; dir = 8},/area/ai_monitored/storage/eva{name = "E.V.A. Storage"})
diff --git a/_maps/map_files/cyberiad/cyberiad.dmm b/_maps/map_files/cyberiad/cyberiad.dmm
index 1ed7089ef1b..ee8e6682456 100644
--- a/_maps/map_files/cyberiad/cyberiad.dmm
+++ b/_maps/map_files/cyberiad/cyberiad.dmm
@@ -3040,7 +3040,7 @@
"bgx" = (/obj/machinery/portable_atmospherics/hydroponics,/turf/simulated/floor/plasteel{icon_state = "dark"},/area/hydroponics)
"bgy" = (/obj/machinery/door/window/northright{base_state = "right"; dir = 8; icon_state = "right"; name = "Library Desk Door"; req_access_txt = "37"},/obj/machinery/status_display{density = 0; layer = 4; pixel_x = 0; pixel_y = 32},/turf/simulated/floor/wood,/area/library)
"bgz" = (/obj/machinery/newscaster{pixel_y = 32},/turf/simulated/floor/wood,/area/library)
-"bgA" = (/obj/structure/table/woodentable,/obj/machinery/librarycomp{pixel_y = 0},/obj/machinery/light/small{dir = 4},/obj/machinery/light_switch{pixel_x = 27},/turf/simulated/floor/wood,/area/library)
+"bgA" = (/obj/structure/table/woodentable,/obj/machinery/computer/library/checkout{pixel_y = 0},/obj/machinery/light/small{dir = 4},/obj/machinery/light_switch{pixel_x = 27},/turf/simulated/floor/wood,/area/library)
"bgB" = (/obj/machinery/power/apc{dir = 8; name = "Chapel APC"; pixel_x = -25},/obj/structure/cable{icon_state = "0-2"; pixel_y = 1; d2 = 2},/obj/machinery/light{dir = 8},/turf/simulated/floor/plasteel{icon_state = "dark"},/area/chapel/main)
"bgC" = (/obj/structure/stool,/obj/effect/landmark/start{name = "Civilian"},/turf/simulated/floor/plasteel{dir = 8; icon_state = "chapel"},/area/chapel/main)
"bgD" = (/obj/structure/stool,/turf/simulated/floor/plasteel{icon_state = "chapel"},/area/chapel/main)
@@ -3111,7 +3111,7 @@
"bhQ" = (/obj/machinery/portable_atmospherics/hydroponics,/obj/item/device/radio/intercom{frequency = 1459; name = "station intercom (General)"; pixel_x = 28},/turf/simulated/floor/plasteel{icon_state = "dark"},/area/hydroponics)
"bhR" = (/obj/structure/bookcase{name = "bookcase (Adult)"},/obj/machinery/atmospherics/pipe/simple/hidden/supply{level = 1},/turf/simulated/floor/wood,/area/library)
"bhS" = (/obj/structure/bookcase{name = "bookcase (Adult)"},/turf/simulated/floor/wood,/area/library)
-"bhT" = (/obj/machinery/librarypubliccomp,/obj/structure/table/woodentable,/turf/simulated/floor/wood,/area/library)
+"bhT" = (/obj/machinery/computer/library/public,/obj/structure/table/woodentable,/turf/simulated/floor/wood,/area/library)
"bhU" = (/obj/structure/stool/bed/chair/comfy/black,/turf/simulated/floor/wood,/area/library)
"bhV" = (/obj/structure/table/woodentable,/obj/item/device/flashlight/lamp/green{pixel_x = 1; pixel_y = 5},/turf/simulated/floor/wood,/area/library)
"bhW" = (/obj/effect/landmark/start{name = "Librarian"},/obj/structure/stool/bed/chair/office/dark,/turf/simulated/floor/wood,/area/library)
diff --git a/_maps/map_files/cyberiad/z2.dmm b/_maps/map_files/cyberiad/z2.dmm
index d1aa08e97d4..a473111071f 100644
--- a/_maps/map_files/cyberiad/z2.dmm
+++ b/_maps/map_files/cyberiad/z2.dmm
@@ -425,7 +425,7 @@
"ii" = (/obj/structure/bookcase{name = "Forbidden Knowledge"},/obj/effect/decal/cleanable/cobweb,/turf/unsimulated/floor{dir = 8; icon_state = "wood"},/area/wizard_station)
"ij" = (/obj/structure/bookcase{name = "Forbidden Knowledge"},/turf/unsimulated/floor{dir = 8; icon_state = "wood"},/area/wizard_station)
"ik" = (/turf/unsimulated/floor{dir = 8; icon_state = "wood"},/area/wizard_station)
-"il" = (/obj/machinery/librarycomp,/obj/structure/table/woodentable,/turf/unsimulated/floor{dir = 8; icon_state = "wood"},/area/wizard_station)
+"il" = (/obj/machinery/computer/library/checkout,/obj/structure/table/woodentable,/turf/unsimulated/floor{dir = 8; icon_state = "wood"},/area/wizard_station)
"im" = (/obj/machinery/vending/magivend,/obj/machinery/light/spot{tag = "icon-tube1 (NORTH)"; icon_state = "tube1"; dir = 1},/turf/unsimulated/floor{dir = 8; icon_state = "wood"},/area/wizard_station)
"in" = (/obj/machinery/vending/snack,/turf/unsimulated/floor{dir = 8; icon_state = "wood"},/area/wizard_station)
"io" = (/obj/structure/closet{icon_closed = "cabinet_closed"; icon_opened = "cabinet_open"; icon_state = "cabinet_closed"},/obj/item/weapon/storage/backpack/satchel,/turf/unsimulated/floor{dir = 9; icon_state = "carpetside"},/area/wizard_station)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index a2d62b29c8a..a16a419d35b 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -62,6 +62,7 @@ var/list/admin_verbs_admin = list(
/client/proc/man_up,
/client/proc/global_man_up,
/client/proc/delbook,
+ /client/proc/view_flagged_books,
/client/proc/empty_ai_core_toggle_latejoin,
/client/proc/aooc,
/client/proc/freeze,
@@ -125,6 +126,7 @@ var/list/admin_verbs_server = list(
/client/proc/cmd_debug_del_all,
/datum/admins/proc/toggle_aliens,
/client/proc/delbook,
+ /client/proc/view_flagged_books,
/client/proc/toggle_antagHUD_use,
/client/proc/toggle_antagHUD_restrictions,
/client/proc/set_ooc,
diff --git a/code/modules/library/admin.dm b/code/modules/library/admin.dm
new file mode 100644
index 00000000000..7e3abd91db9
--- /dev/null
+++ b/code/modules/library/admin.dm
@@ -0,0 +1,41 @@
+/client/proc/delbook()
+ set name = "Delete Book"
+ set desc = "Permamently deletes a book from the database."
+ set category = "Admin"
+
+ if(!holder)
+ to_chat(src, "Only administrators may use this command.")
+ return
+
+ var/isbn = input("ISBN number?", "Delete Book") as num | null
+ if(!isbn)
+ return
+
+ var/DBQuery/query_delbook = dbcon.NewQuery("DELETE FROM [format_table_name("library")] WHERE id=[isbn]")
+ if(!query_delbook.Execute())
+ var/err = query_delbook.ErrorMsg()
+ log_game("SQL ERROR deleting book. Error : \[[err]\]\n")
+ return
+
+ 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(!holder)
+ to_chat(src, "Only administrators may use this command.")
+ return
+
+ var/DBQuery/query = dbcon.NewQuery("SELECT id, title, flagged FROM [format_table_name("library")] WHERE flagged > 0 ORDER BY flagged DESC")
+ if(!query.Execute())
+ var/err = query.ErrorMsg()
+ log_game("SQL ERROR getting flagged books. Error : \[[err]\]\n")
+ return
+
+ var/i
+ for(i = 0, query.NextRow(), i++)
+ to_chat(usr, "[add_zero(query.item[1], 4)] ([query.item[2]]) - flagged [query.item[3]] times.")
+ to_chat(usr, "[i] flagged books total.")
\ No newline at end of file
diff --git a/code/modules/library/computers/base.dm b/code/modules/library/computers/base.dm
new file mode 100644
index 00000000000..fa0a4186191
--- /dev/null
+++ b/code/modules/library/computers/base.dm
@@ -0,0 +1,93 @@
+#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(var/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(var/page_num)
+ var/searchquery = ""
+ var/where = 0
+ if(query)
+ if(query.title && query.title != "")
+ searchquery += " WHERE title LIKE '%[query.title]%'"
+ where = 1
+ if(query.author && query.author != "")
+ searchquery += " [!where ? "WHERE" : "AND"] author LIKE '%[query.author]%'"
+ where = 1
+ if(query.category && query.category != "")
+ searchquery += " [!where ? "WHERE" : "AND"] category LIKE '%[query.category]%'"
+ if(query.category == "Fiction")
+ searchquery += " AND category NOT LIKE '%Non-Fiction%'"
+ where = 1
+ searchquery += " [!where ? "WHERE" : "AND"] flagged < [MAX_BOOK_FLAGS]"
+ var/sql = "SELECT id, author, title, category, ckey, flagged FROM [format_table_name("library")] [searchquery] LIMIT [(page_num - 1) * LIBRARY_BOOKS_PER_PAGE], [LIBRARY_BOOKS_PER_PAGE]"
+
+ // Pagination
+ var/DBQuery/_query = dbcon.NewQuery(sql)
+ _query.Execute()
+ if(_query.ErrorMsg())
+ log_to_dd(_query.ErrorMsg())
+
+ 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],
+ "ckey" =_query.item[5],
+ "flagged" =text2num(_query.item[6])
+ ))
+ results += CB
+ return results
+
+/obj/machinery/computer/library/proc/get_num_results()
+ var/sql = "SELECT COUNT(*) FROM [format_table_name("library")]"
+
+ var/DBQuery/_query = dbcon.NewQuery(sql)
+ _query.Execute()
+ while(_query.NextRow())
+ return text2num(_query.item[1])
+ return 0
+
+/obj/machinery/computer/library/proc/get_pagelist()
+ var/pagelist = "
"
+ var/start = max(1, page_num - 3)
+ var/end = min(num_pages, page_num + 3)
+ for(var/i = start to end)
+ var/dat = "
[i]"
+ if(i == page_num)
+ dat = "
[dat]"
+ if(i != end)
+ dat += " "
+ pagelist += dat
+ pagelist += "
"
+ return pagelist
+
+/obj/machinery/computer/library/proc/getBookByID(var/id as text)
+ return library_catalog.getBookByID(id)
\ No newline at end of file
diff --git a/code/modules/library/computers/checkout.dm b/code/modules/library/computers/checkout.dm
new file mode 100644
index 00000000000..2746aed857e
--- /dev/null
+++ b/code/modules/library/computers/checkout.dm
@@ -0,0 +1,465 @@
+/*
+ * 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(var/mob/user as mob)
+ if(..())
+ return
+ interact(user)
+
+/obj/machinery/computer/library/checkout/interact(var/mob/user)
+ if(interact_check(user))
+ return
+
+ var/dat = ""
+ switch(screenstate)
+ if(0)
+ // Main Menu
+
+ dat += {"
+ - View General Inventory
+ - View Checked Out Inventory
+ - Check out a Book
+ - Connect to External Archive
+ - Upload New Title to Archive
+ - Print a Bible
+ - Print a Manual
"}
+ if(src.emagged)
+ dat += "- Access the Forbidden Lore Vault
"
+ dat += "
"
+
+ if(src.arcanecheckout)
+ new /obj/item/weapon/tome(src.loc)
+ to_chat(user, "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 dusty old tome sitting on the desk. You don't really remember printing it.")
+ user.visible_message("[user] stares at the blank screen for a few moments, his expression frozen in fear. When he finally awakens from it, he looks a lot older.", 2)
+ src.arcanecheckout = 0
+ if(1)
+ // Inventory
+ dat += "Inventory
"
+ for(var/obj/item/weapon/book/b in inventory)
+ dat += "[b.name] (Delete)
"
+ dat += "(Return to main menu)
"
+ if(2)
+ // Checked Out
+ dat += "Checked Out Books
"
+ 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 = "(OVERDUE) [timedue]"
+ else
+ timedue = round(timedue)
+
+ dat += {"\"[b.bookname]\", Checked out to: [b.mobname]
--- Taken: [timetaken] minutes ago, Due: in [timedue] minutes
+ (Check In)
"}
+ dat += "(Return to main menu)
"
+ if(3)
+ // Check Out a Book
+
+ dat += {"Check Out a Book
+ Book: [src.buffer_book]
+ \[Edit\]
+ Recipient: [src.buffer_mob]
+ \[Edit\]
+ Checkout Date : [world.time/600]
+ Due Date: [(world.time + checkoutperiod)/600]
+ (Checkout Period: [checkoutperiod] minutes) (+/-)
+ (Commit Entry)
+ (Return to main menu)
"}
+ if(4)
+ dat += "External Archive
"
+ if(!dbcon.IsConnected())
+ dat += "ERROR: Unable to contact External Archive. Please contact your system administrator for assistance."
+ else
+ num_results = src.get_num_results()
+ num_pages = Ceiling(num_results/LIBRARY_BOOKS_PER_PAGE)
+ dat += {""}
+ var/pagelist = get_pagelist()
+
+ dat += {"Search Settings
+ Filter by Title: [query.title]
+ Filter by Category: [query.category]
+ Filter by Author: [query.author]
+ \[Start Search\]
"}
+ dat += pagelist
+
+ dat += {""}
+
+ dat += {"
+
+ | Author |
+ Title |
+ Category |
+ Controls |
+
"}
+
+ for(var/datum/cachedbook/CB in get_page(page_num))
+ var/author = CB.author
+ var/controls = "\[Order\]"
+ controls += {" \[Flag[CB.flagged ? "ged" : ""]\]"}
+ if(check_rights(R_ADMIN, user = user))
+ controls += " \[Delete\]"
+ author += " ([ckey(CB.ckey)]))"
+ dat += {"
+ | [author] |
+ [CB.title] |
+ [CB.category] |
+ [controls] |
+
"}
+
+ dat += "
[pagelist]"
+
+ dat += "
(Return to main menu)
"
+ if(5)
+ dat += "Upload a New Title
"
+ if(!scanner)
+ for(var/obj/machinery/libraryscanner/S in range(9))
+ scanner = S
+ break
+ if(!scanner)
+ dat += "No scanner found within wireless network range.
"
+ else if(!scanner.cache)
+ dat += "No data found in scanner memory.
"
+ else
+
+ dat += {"Data marked for upload...
+ Title: [scanner.cache.name]
"}
+ if(!scanner.cache.author)
+ scanner.cache.author = "Anonymous"
+
+ dat += {"Author: [scanner.cache.author]
+ Category: [upload_category]
+ \[Upload\]
"}
+ dat += "(Return to main menu)
"
+ if(7)
+ dat += "Print a Manual
"
+ dat += ""
+
+ var/list/forbidden = list(
+ /obj/item/weapon/book/manual
+ )
+
+ if(!emagged)
+ forbidden |= /obj/item/weapon/book/manual/nuclear
+
+ var/manualcount = 1
+ var/obj/item/weapon/book/manual/M = null
+
+ for(var/manual_type in (typesof(/obj/item/weapon/book/manual) - forbidden))
+ M = new manual_type()
+ dat += "| [M.title] |
"
+ manualcount++
+ qdel(M)
+ M = null
+ dat += "
"
+ dat += "
(Return to main menu)
"
+
+ if(8)
+
+ dat += {"Accessing Forbidden Lore Vault v 1.3
+ Are you absolutely sure you want to proceed? EldritchTomes Inc. takes no responsibilities for loss of sanity resulting from this action.
+ Yes.
+ No.
"}
+
+ 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, "You override the library computer's printing restrictions.")
+
+/obj/machinery/computer/library/checkout/attackby(obj/item/weapon/W as obj, mob/user as mob)
+ if(istype(W, /obj/item/weapon/barcodescanner))
+ var/obj/item/weapon/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.", 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") + 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)
+ page_num = 1
+
+ screenstate = 4
+ if(href_list["del"])
+ if(!check_rights(R_ADMIN))
+ to_chat(usr, "You aren't an admin, piss off.")
+ 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/DBQuery/query = dbcon.NewQuery("DELETE FROM [format_table_name("library")] WHERE id=[target.id]")
+ var/response = query.Execute()
+ if(!response)
+ to_chat(usr, query.ErrorMsg())
+ return
+ log_admin("LIBRARY: [usr.name]/[usr.key] 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))
+ to_chat(usr, "You aren't an admin, piss off.")
+ 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/DBQuery/query = dbcon.NewQuery("DELETE FROM [format_table_name("library")] WHERE ckey='[sanitizeSQL(tckey)]'")
+ var/response = query.Execute()
+ if(!response)
+ to_chat(usr, query.ErrorMsg())
+ return
+ var/affected=query.RowsAffected()
+ if(affected==0)
+ to_chat(usr, "Unable to find any matching rows.")
+ return
+ log_admin("LIBRARY: [usr.name]/[usr.key] has deleted [affected] books written by [tckey]!")
+ message_admins("[key_name_admin(usr)] has deleted [affected] books written by [tckey]!")
+ src.updateUsrDialog()
+ return
+
+ if(href_list["flag"])
+ if(!dbcon.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")
+ 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/weapon/storage/bible/B = new /obj/item/weapon/storage/bible(src.loc)
+ if(ticker && ( ticker.Bible_icon_state && ticker.Bible_item_state) )
+ B.icon_state = ticker.Bible_icon_state
+ B.item_state = ticker.Bible_item_state
+ B.name = ticker.Bible_name
+ B.deity_name = ticker.Bible_deity_name
+
+ bibledelay = 1
+ spawn(60)
+ bibledelay = 0
+
+ else
+ visible_message("[src]'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/weapon/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")
+ establish_db_connection()
+ if(!dbcon.IsConnected())
+ alert("Connection to Archive has been severed. Aborting.")
+ else
+ var/sqltitle = sanitizeSQL(scanner.cache.name)
+ var/sqlauthor = sanitizeSQL(scanner.cache.author)
+ var/sqlcontent = sanitizeSQL(scanner.cache.dat)
+ var/sqlcategory = sanitizeSQL(upload_category)
+ var/DBQuery/query = dbcon.NewQuery("INSERT INTO [format_table_name("library")] (author, title, content, category, ckey, flagged) VALUES ('[sqlauthor]', '[sqltitle]', '[sqlcontent]', '[sqlcategory]', '[ckey(usr.key)]', 0)")
+ var/response = query.Execute()
+ if(!response)
+ to_chat(usr, query.ErrorMsg())
+ else
+ 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(!dbcon.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)
+ audible_message("[src]'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(!dbcon.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("[src]'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(var/datum/cachedbook/newbook)
+ if(!newbook || !newbook.id)
+ return
+ var/obj/item/weapon/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,9)]"
+ visible_message("[src]'s printer hums as it produces a completely bound book. How did it do that?")
diff --git a/code/modules/library/computers/public.dm b/code/modules/library/computers/public.dm
new file mode 100644
index 00000000000..d2b06e08baf
--- /dev/null
+++ b/code/modules/library/computers/public.dm
@@ -0,0 +1,122 @@
+/obj/machinery/computer/library/public
+ name = "visitor computer"
+
+/obj/machinery/computer/library/public/attack_hand(var/mob/user as mob)
+ if(..())
+ return
+ interact(user)
+
+/obj/machinery/computer/library/public/interact(var/mob/user)
+ if(interact_check(user))
+ return
+
+ var/dat = ""
+ switch(screenstate)
+ if(0)
+
+ dat += {"
Search Settings
+ Filter by Title: [query.title]
+ Filter by Category: [query.category]
+ Filter by Author: [query.author]
+ \[Start Search\]
"}
+ if(1)
+ establish_db_connection()
+ if(!dbcon.IsConnected())
+ dat += "ERROR: Unable to contact External Archive. Please contact your system administrator for assistance.
"
+ else if(num_results == 0)
+ dat += "No results found."
+ else
+ var/pagelist = get_pagelist()
+
+ dat += pagelist
+ dat += {""}
+ dat += {"
+
+ | Author |
+ Title |
+ Category |
+ SS13BN |
+ Controls |
+
"}
+ for(var/datum/cachedbook/CB in get_page(page_num))
+ dat += {"
+ | [CB.author] |
+ [CB.title] |
+ [CB.category] |
+ [CB.id] |
+ \[Flag[CB.flagged ? "ged" : ""]\] |
+
"}
+
+ dat += "
[pagelist]"
+ dat += "\[Go Back\]
"
+ 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") + 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)
+ page_num = 1
+
+ screenstate = 1
+
+ if(href_list["back"])
+ screenstate = 0
+
+ if(href_list["flag"])
+ if(!dbcon.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")
+ library_catalog.flag_book_by_id(usr, id)
+
+ add_fingerprint(usr)
+ updateUsrDialog()
+ return
\ No newline at end of file
diff --git a/code/modules/library/lib_items.dm b/code/modules/library/lib_items.dm
index 498b7b62a37..f7729af3578 100644
--- a/code/modules/library/lib_items.dm
+++ b/code/modules/library/lib_items.dm
@@ -19,86 +19,103 @@
density = 1
opacity = 1
var/health = 50
+ var/tmp/busy = 0
+ var/list/valid_types = list(/obj/item/weapon/book, \
+ /obj/item/weapon/tome, \
+ /obj/item/weapon/spellbook, \
+ /obj/item/weapon/storage/bible)
/obj/structure/bookcase/initialize()
for(var/obj/item/I in loc)
- if(istype(I, /obj/item/weapon/book))
- I.loc = src
+ if(is_type_in_list(I, valid_types))
+ I.forceMove(src)
update_icon()
/obj/structure/bookcase/attackby(obj/O as obj, mob/user as mob, params)
- if(istype(O, /obj/item/weapon/book) || istype(O, /obj/item/weapon/spellbook))
+ if(busy) //So that you can't mess with it while deconstructing
+ return 1
+ if(is_type_in_list(O, valid_types))
user.drop_item()
- O.loc = src
+ O.forceMove(src)
update_icon()
+ return 1
else if(istype(O, /obj/item/weapon/wrench))
- to_chat(user, "\blue Now disassembling bookcase")
+ user.visible_message("[user] starts disassembling \the [src].", \
+ "You start disassembling \the [src].")
playsound(get_turf(src), 'sound/items/Ratchet.ogg', 50, 1)
+ busy = 1
+
if(do_after(user,50, target = src))
- new /obj/item/stack/sheet/wood(get_turf(src))
- new /obj/item/stack/sheet/wood(get_turf(src))
- new /obj/item/stack/sheet/wood(get_turf(src))
- new /obj/item/stack/sheet/wood(get_turf(src))
- new /obj/item/stack/sheet/wood(get_turf(src))
+ playsound(get_turf(src), 'sound/items/Deconstruct.ogg', 75, 1)
+ user.visible_message("[user] disassembles \the [src].", \
+ "You disassemble \the [src].")
+ busy = 0
+ for(var/i = 1 to 5)
+ new /obj/item/stack/sheet/wood(get_turf(src))
density = 0
qdel(src)
- return
- else if(istype(O, /obj/item/weapon/pen))
- var/newname = stripped_input(usr, "What would you like to title this bookshelf?")
- if(!newname)
- return
else
+ busy = 0
+ return 1
+ else if(istype(O, /obj/item/weapon/pen))
+ var/newname = stripped_input(user, "What would you like to title this [name]?")
+ if(newname)
name = ("bookcase ([sanitize(newname)])")
+ return 1
else
switch(O.damtype)
if("fire")
- src.health -= O.force * 1
+ health -= O.force * 1
if("brute")
- src.health -= O.force * 0.75
+ health -= O.force * 0.75
else
- if (src.health <= 0)
+ if (health <= 0)
visible_message("The bookcase is smashed apart!")
new /obj/item/stack/sheet/wood(get_turf(src))
new /obj/item/stack/sheet/wood(get_turf(src))
new /obj/item/stack/sheet/wood(get_turf(src))
qdel(src)
- ..()
+ return ..()
/obj/structure/bookcase/attack_hand(var/mob/user as mob)
if(contents.len)
- var/obj/item/weapon/book/choice = input("Which book would you like to remove from the shelf?") in contents as obj|null
+ var/obj/item/weapon/book/choice = input("Which book would you like to remove from \the [src]?") in contents as obj|null
if(choice)
- if(!usr.canmove || usr.stat || usr.restrained() || !in_range(loc, usr))
+ if(user.incapacitated() || user.lying || !Adjacent(user))
return
- if(ishuman(user))
- if(!user.get_active_hand())
- user.put_in_hands(choice)
+ if(!user.get_active_hand())
+ user.put_in_hands(choice)
else
- choice.loc = get_turf(src)
+ choice.forceMove(get_turf(src))
update_icon()
/obj/structure/bookcase/ex_act(severity)
switch(severity)
if(1.0)
- for(var/obj/item/weapon/book/b in contents)
- qdel(b)
+ for(var/obj/item/I in contents)
+ qdel(I)
qdel(src)
return
if(2.0)
- for(var/obj/item/weapon/book/b in contents)
- if (prob(50)) b.loc = (get_turf(src))
- else qdel(b)
+ for(var/obj/item/I in contents)
+ if(prob(50))
+ qdel(I)
qdel(src)
return
if(3.0)
- if (prob(50))
- for(var/obj/item/weapon/book/b in contents)
- b.loc = (get_turf(src))
+ if(prob(50))
qdel(src)
return
- else
return
+/obj/structure/bookcase/Destroy()
+ for(var/i = 1 to 3)
+ new /obj/item/stack/sheet/wood(get_turf(src))
+ for(var/obj/item/I in contents)
+ if(is_type_in_list(I, valid_types))
+ I.forceMove(get_turf(src))
+ return ..()
+
/obj/structure/bookcase/update_icon()
if(contents.len < 5)
icon_state = "book-[contents.len]"
@@ -155,13 +172,14 @@
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/obj/item/store //What's in the book?
+ var/forbidden = 0 // Prevent ordering of this book. (0=no, 1=yes, 2=emag only)
+ var/obj/item/store // What's in the book?
/obj/item/weapon/book/attack_self(var/mob/user as mob)
if(carved)
if(store)
to_chat(user, "[store] falls out of [title]!")
- store.loc = get_turf(src.loc)
+ store.forceMove(get_turf(loc))
store = null
return
else
@@ -169,7 +187,8 @@
return
if(src.dat)
user << browse("Penned by [author].
" + "[dat]", "window=book")
- user.visible_message("[user] opens a book titled \"[src.title]\" and begins reading intently.")
+ 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!")
@@ -179,46 +198,45 @@
if(!store)
if(W.w_class < 3)
user.drop_item()
- W.loc = src
+ W.forceMove(src)
store = W
to_chat(user, "You put [W] in [title].")
- return
+ return 1
else
to_chat(user, "[W] won't fit in [title].")
- return
+ return 1
else
to_chat(user, "There's already something in [title]!")
- return
+ return 1
if(istype(W, /obj/item/weapon/pen))
if(unique)
to_chat(user, "These pages don't seem to take the ink well. Looks like you can't modify it.")
- return
+ 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
+ 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):"),8192) as message|null
+ var/content = strip_html(input(usr, "Write your book's contents (HTML NOT allowed):"), MAX_BOOK_MESSAGE_LEN) as message|null
if(!content)
to_chat(usr, "The content is invalid.")
- return
+ 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
+ return 1
else
src.author = newauthor
- else
- return
+ return 1
else if(istype(W, /obj/item/weapon/barcodescanner))
var/obj/item/weapon/barcodescanner/scanner = W
if(!scanner.computer)
@@ -238,25 +256,27 @@
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
+ 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/weapon/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
+ return 1
scanner.computer.inventory.Add(src)
to_chat(user, "[W]'s screen flashes: 'Book stored in buffer. Title added to general inventory.'")
- else if(istype(W, /obj/item/weapon/kitchen/knife) || istype(W, /obj/item/weapon/wirecutters))
- if(carved) return
+ return 1
+ else if(istype(W, /obj/item/weapon/kitchen/knife) || iswirecutter(W))
+ if(carved)
+ return 1
to_chat(user, "You begin to carve out [title].")
if(do_after(user, 30, target = src))
to_chat(user, "You carve out the pages from [title]! You didn't want to read it anyway.")
carved = 1
- return
+ return 1
else
- ..()
+ return ..()
/*
@@ -269,7 +289,7 @@
throw_speed = 1
throw_range = 5
w_class = 1.0
- var/obj/machinery/librarycomp/computer // Associated computer - Modes 1 to 3 use this
+ var/obj/machinery/computer/library/checkout/computer // Associated computer - Modes 1 to 3 use this
var/obj/item/weapon/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
diff --git a/code/modules/library/lib_machines.dm b/code/modules/library/lib_machines.dm
index 57f1b85b227..2974fcf0264 100644
--- a/code/modules/library/lib_machines.dm
+++ b/code/modules/library/lib_machines.dm
@@ -1,407 +1,146 @@
-/* Library Machines
- *
- * Contains:
- * Borrowbook datum
- * Library Public Computer
- * Library Computer
- * Library Scanner
- * Book Binder
- */
+#define LIBRARY_BOOKS_PER_PAGE 25
+
+var/global/datum/library_catalog/library_catalog = new()
+var/global/list/library_section_names = list("Any", "Fiction", "Non-Fiction", "Adult", "Reference", "Religion")
+
+
+/hook/startup/proc/load_manuals()
+ library_catalog.initialize()
+ return 1
/*
* Borrowbook datum
*/
-datum/borrowbook // Datum used to keep track of who has borrowed what when and for how long.
+/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
/*
- * Library Public Computer
+ * Cachedbook datum
*/
-/obj/machinery/librarypubliccomp
- name = "visitor computer"
- icon = 'icons/obj/library.dmi'
- icon_state = "computer"
- anchored = 1
- density = 1
- var/screenstate = 0
+/datum/cachedbook // Datum used to cache the SQL DB books locally in order to achieve a performance gain.
+ var/id
var/title
- var/category = "Any"
var/author
- var/SQLquery
+ 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/weapon/book // Type path of the book to generate
+ var/flagged = 0
-/obj/machinery/librarypubliccomp/attack_hand(var/mob/user as mob)
- usr.set_machine(src)
- var/dat = "Library Visitor\n" //
- switch(screenstate)
- if(0)
- dat += {"Search Settings
- Filter by Title: [title]
- Filter by Category: [category]
"
- Filter by Author: [author]
- \[Start Search\]
"}
- if(1)
- if(!dbcon.IsConnected())
- dat += "ERROR: Unable to contact External Archive. Please contact your system administrator for assistance.
"
- else if(!SQLquery)
- dat += "ERROR: Malformed search request. Please contact your system administrator for assistance.
"
- else
- dat += ""
- dat += "| AUTHOR | TITLE | CATEGORY | SS13BN |
"
+/datum/cachedbook/proc/LoadFromRow(var/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
- var/DBQuery/query = dbcon.NewQuery(SQLquery)
- query.Execute()
+// Builds a SQL statement
+/datum/library_query
+ var/author
+ var/category
+ var/title
- while(query.NextRow())
- var/author = query.item[1]
- var/title = query.item[2]
- var/category = query.item[3]
- var/id = query.item[4]
- dat += "| [author] | [title] | [category] | [id] |
"
- dat += "
"
- dat += "\[Go Back\]
"
- user << browse(dat, "window=publiclibrary")
- onclose(user, "publiclibrary")
+/datum/library_query/proc/toSQL()
+ var/list/where = list()
+ if(author || title || category)
+ if(author)
+ where.Add("author LIKE '%[sanitizeSQL(author)]%'")
+ if(category)
+ where.Add("category = '[sanitizeSQL(category)]'")
+ if(title)
+ where.Add("title LIKE '%[sanitizeSQL(title)]%'")
+ return " WHERE " + jointext(where, " AND ")
+ return ""
-/obj/machinery/librarypubliccomp/Topic(href, href_list)
- if(..())
- usr << browse(null, "window=publiclibrary")
- onclose(usr, "publiclibrary")
+// 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/proc/initialize()
+ var/newid=1
+ for(var/typepath in subtypesof(/obj/item/weapon/book/manual))
+ var/obj/item/weapon/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, "That book cannot be flagged in the system, as it does not actually exist in the database.")
+ return
+
+ if("[id]" in books_flagged_this_round)
+ to_chat(user, "This book has already been flagged this shift.")
return
- if(href_list["settitle"])
- var/newtitle = input("Enter a title to search for:") as text|null
- if(newtitle)
- title = sanitize(newtitle)
- else
- title = null
- title = sanitizeSQL(title)
- if(href_list["setcategory"])
- var/newcategory = input("Choose a category to search for:") in list("Any", "Fiction", "Non-Fiction", "Adult", "Reference", "Religion")
- if(newcategory)
- category = sanitize(newcategory)
- else
- category = "Any"
- category = sanitizeSQL(category)
- if(href_list["setauthor"])
- var/newauthor = input("Enter an author to search for:") as text|null
- if(newauthor)
- author = sanitize(newauthor)
- else
- author = null
- author = sanitizeSQL(author)
- if(href_list["search"])
- SQLquery = "SELECT author, title, category, id FROM [format_table_name("library")] WHERE "
- if(category == "Any")
- SQLquery += "author LIKE '%[author]%' AND title LIKE '%[title]%'"
- else
- SQLquery += "author LIKE '%[author]%' AND title LIKE '%[title]%' AND category='[category]'"
- screenstate = 1
+ books_flagged_this_round["[id]"] = 1
+ message_admins("[key_name_admin(user)] has flagged book #[id] as inappropriate.")
- if(href_list["back"])
- screenstate = 0
-
- src.add_fingerprint(usr)
- src.updateUsrDialog()
- return
-
-
-/*
- * Library Computer
- */
-// TODO: Make this an actual /obj/machinery/computer that can be crafted from circuit boards and such
-// It is August 22nd, 2012... This TODO has already been here for months.. I wonder how long it'll last before someone does something about it.
-/obj/machinery/librarycomp
- 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
-
-/obj/machinery/librarycomp/attack_hand(var/mob/user as mob)
- usr.set_machine(src)
- var/dat = "Book Inventory Management\n" //
- switch(screenstate)
- if(0)
- // Main Menu
- dat += {"1. View General Inventory
- 2. View Checked Out Inventory
- 3. Check out a Book
- 4. Connect to External Archive
- 5. Upload New Title to Archive
- 6. Print a Bible
"}
- if(src.emagged)
- dat += "7. Access the Forbidden Lore Vault
"
- if(src.arcanecheckout)
- new /obj/item/weapon/tome(src.loc)
- to_chat(user, "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 dusty old tome sitting on the desk. You don't really remember printing it.")
- user.visible_message("[user] stares at the blank screen for a few moments, his expression frozen in fear. When he finally awakens from it, he looks a lot older.", 2)
- src.arcanecheckout = 0
- if(1)
- // Inventory
- dat += "Inventory
"
- for(var/obj/item/weapon/book/b in inventory)
- dat += "[b.name] (Delete)
"
- dat += "(Return to main menu)
"
- if(2)
- // Checked Out
- dat += "Checked Out Books
"
- 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 = "(OVERDUE) [timedue]"
- else
- timedue = round(timedue)
- dat += "\"[b.bookname]\", Checked out to: [b.mobname]
--- Taken: [timetaken] minutes ago, Due: in [timedue] minutes
"
- dat += "(Check In)
"
- dat += "(Return to main menu)
"
- if(3)
- // Check Out a Book
- dat += {"Check Out a Book
- Book: [src.buffer_book]
- \[Edit\]
- Recipient: [src.buffer_mob]
- \[Edit\]
- Checkout Date : [world.time/600]
- Due Date: [(world.time + checkoutperiod)/600]
- (Checkout Period: [checkoutperiod] minutes) (+/-)
- (Commit Entry)
- (Return to main menu)
"}
- if(4)
- dat += "External Archive
"
- if(!dbcon.IsConnected())
- dat += "ERROR: Unable to contact External Archive. Please contact your system administrator for assistance."
- else
- dat += "(Order book by SS13BN)
"
- dat += ""
- dat += "| AUTHOR | TITLE | CATEGORY | ID | |
"
-
- var/DBQuery/query = dbcon.NewQuery("SELECT id, author, title, category FROM [format_table_name("library")]")
- if(!query.Execute())
- var/err = query.ErrorMsg()
- log_game("SQL ERROR accessing book. Error : \[[err]\]\n")
- return
-
- while(query.NextRow())
- var/id = query.item[1]
- var/author = query.item[2]
- var/title = query.item[3]
- var/category = query.item[4]
- dat += "| [author] | [title] | [category] | [id] | \[Order\] |
"
- dat += "
"
- dat += "
(Return to main menu)
"
- if(5)
- dat += "Upload a New Title
"
- if(!scanner)
- for(var/obj/machinery/libraryscanner/S in range(9))
- scanner = S
- break
- if(!scanner)
- dat += "No scanner found within wireless network range.
"
- else if(!scanner.cache)
- dat += "No data found in scanner memory.
"
- else
- dat += "Data marked for upload...
"
- dat += "Title: [scanner.cache.name]
"
- if(!scanner.cache.author)
- scanner.cache.author = "Anonymous"
- dat += "Author: [scanner.cache.author]
"
- dat += "Category: [upload_category]
"
- dat += "\[Upload\]
"
- dat += "OOC Upload Rules: No erotica, No real world literature. Only IC guides, stories and accounts."
- dat += "Failure to follow these simple rules will lead to bans."
- dat += "(Return to main menu)
"
- if(7)
- dat += "Accessing Forbidden Lore Vault v 1.3
"
- dat += "Are you absolutely sure you want to proceed? EldritchTomes Inc. takes no responsibilities for loss of sanity resulting from this action."
- dat += "Yes.
"
- dat += "No.
"
-
- //dat += "Close
"
- user << browse(dat, "window=library")
- onclose(user, "library")
-
-/obj/machinery/librarycomp/attackby(obj/item/weapon/W as obj, mob/user as mob, params)
- if(istype(W, /obj/item/weapon/barcodescanner))
- var/obj/item/weapon/barcodescanner/scanner = W
- scanner.computer = src
- to_chat(user, "[scanner]'s associated machine has been set to [src].")
- for (var/mob/V in hearers(src))
- V.show_message("[src] lets out a low, short blip.", 2)
- else
- ..()
-
-/obj/machinery/librarycomp/emag_act(user as mob)
- if (src.density)
- src.emagged = 1
-
-/obj/machinery/librarycomp/Topic(href, href_list)
- if(..())
- usr << browse(null, "window=library")
- onclose(usr, "library")
+ var/sqlid = text2num(id)
+ if(!sqlid)
return
+ var/DBQuery/query = dbcon.NewQuery("UPDATE [format_table_name("library")] SET flagged = flagged + 1 WHERE id=[sqlid]")
+ query.Execute()
- 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)
+/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, "That book cannot be removed from the system, as it does not actually exist in the database.")
+ return
- playsound(loc, "sound/goonstation/machines/printer_dotmatrix.ogg", 50, 1)
- var/obj/item/weapon/storage/bible/B = new /obj/item/weapon/storage/bible(src.loc)
- if(ticker && ( ticker.Bible_icon_state && ticker.Bible_item_state) )
- B.icon_state = ticker.Bible_icon_state
- B.item_state = ticker.Bible_item_state
- B.name = ticker.Bible_name
- B.deity_name = ticker.Bible_deity_name
+ var/sqlid = text2num(id)
+ if(!sqlid)
+ return
+ var/DBQuery/query = dbcon.NewQuery("DELETE FROM [format_table_name("library")] WHERE id=[sqlid]")
+ query.Execute()
- bibledelay = 1
- spawn(60)
- bibledelay = 0
+/datum/library_catalog/proc/getBookByID(id)
+ if("[id]" in cached_books)
+ return cached_books["[id]"]
- else
- for (var/mob/V in hearers(src))
- V.show_message("[src]'s monitor flashes, \"Bible printer currently unavailable, please wait a moment.\"")
+ var/sqlid = text2num(id)
+ if(!sqlid)
+ return
+ var/DBQuery/query = dbcon.NewQuery("SELECT id, author, title, category, ckey, flagged FROM [format_table_name("library")] WHERE id=[sqlid]")
+ query.Execute()
- if("7")
- screenstate = 7
- 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 = sanitize(copytext(input("Enter the book's title:") as text|null,1,MAX_MESSAGE_LEN))
- if(href_list["editmob"])
- buffer_mob = sanitize(copytext(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/weapon/book/b = locate(href_list["delbook"])
- inventory.Remove(b)
- if(href_list["setauthor"])
- var/newauthor = sanitize(copytext(input("Enter the author's name: ") as text|null,1,MAX_MESSAGE_LEN))
- if(newauthor)
- scanner.cache.author = newauthor
- if(href_list["setcategory"])
- 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(!dbcon.IsConnected())
- alert("Connection to Archive has been severed. Aborting.")
- else
- /*
- var/sqltitle = dbcon.Quote(scanner.cache.name)
- var/sqlauthor = dbcon.Quote(scanner.cache.author)
- var/sqlcontent = dbcon.Quote(scanner.cache.dat)
- var/sqlcategory = dbcon.Quote(upload_category)
- */
- var/sqltitle = sanitizeSQL(scanner.cache.name)
- var/sqlauthor = sanitizeSQL(scanner.cache.author)
- var/sqlcontent = sanitizeSQL(scanner.cache.dat)
- var/sqlcategory = sanitizeSQL(upload_category)
- var/DBQuery/query = dbcon.NewQuery("INSERT INTO [format_table_name("library")] (author, title, content, category, ckey) VALUES ('[sqlauthor]', '[sqltitle]', '[sqlcontent]', '[sqlcategory]', '[usr.key]')")
- if(!query.Execute())
- var/err = query.ErrorMsg()
- log_game("SQL ERROR adding new book. Error : \[[err]\]\n")
- return
- else
- log_game("[usr.name]/[usr.key] has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] signs")
- message_admins("[key_name_admin(usr)] has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] signs")
- alert("Upload Complete.")
+ 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],
+ "ckey" =query.item[5],
+ "flagged" =query.item[6]
+ ))
+ results += CB
+ cached_books["[id]"]=CB
+ return CB
+ return results
- if(href_list["targetid"])
- var/sqlid = sanitizeSQL(href_list["targetid"])
- if(!dbcon.IsConnected())
- alert("Connection to Archive has been severed. Aborting.")
-
-// reused working printing call from bible code.. needs testing ((Lazureus))
-
- if(!bibledelay)
- bibledelay = 1
- spawn(60)
- bibledelay = 0
- var/DBQuery/query = dbcon.NewQuery("SELECT * FROM [format_table_name("library")] WHERE id=[sqlid]")
- query.Execute()
-
- while(query.NextRow())
- var/author = query.item[2]
- var/title = query.item[3]
- var/content = query.item[4]
- playsound(loc, "sound/goonstation/machines/printer_dotmatrix.ogg", 50, 1)
- var/obj/item/weapon/book/B = new(src.loc)
- B.name = "Book: [title]"
- B.title = title
- B.author = author
- B.dat = content
- B.icon_state = "book[rand(1,7)]"
- src.visible_message("[src]'s printer hums as it produces a completely bound book. How did it do that?")
- break
- else
- for (var/mob/V in hearers(src))
- V.show_message("[src]'s monitor flashes, \"Printer unavailable. Please allow a short time before attempting to print.\"")
-
- if(href_list["orderbyid"])
- var/orderid = input("Enter your order:") as num|null
- if(orderid)
- if(isnum(orderid))
- var/nhref = "src=\ref[src];targetid=[orderid]"
- spawn() src.Topic(nhref, params2list(nhref), src)
- src.add_fingerprint(usr)
- src.updateUsrDialog()
- return
-
-/*
- * Library Scanner
- */
+/** Scanner **/
/obj/machinery/libraryscanner
name = "scanner"
icon = 'icons/obj/library.dmi'
@@ -410,12 +149,18 @@ datum/borrowbook // Datum used to keep track of who has borrowed what when and f
density = 1
var/obj/item/weapon/book/cache // Last scanned book
-/obj/machinery/libraryscanner/attackby(var/obj/O as obj, var/mob/user as mob, params)
- if(istype(O, /obj/item/weapon/book))
+/obj/machinery/libraryscanner/attackby(obj/item/I, mob/user)
+ if(istype(I, /obj/item/weapon/book))
user.drop_item()
- O.loc = src
+ I.forceMove(src)
+ return 1
+ else
+ return ..()
-/obj/machinery/libraryscanner/attack_hand(var/mob/user as mob)
+/obj/machinery/libraryscanner/attack_hand(mob/user)
+ if(istype(user,/mob/dead))
+ to_chat(user, "Nope.")
+ return
usr.set_machine(src)
var/dat = "
Scanner Control Interface\n" //
if(cache)
@@ -460,39 +205,19 @@ datum/borrowbook // Datum used to keep track of who has borrowed what when and f
anchored = 1
density = 1
-/obj/machinery/bookbinder/attackby(var/obj/O as obj, var/mob/user as mob, params)
- if(istype(O, /obj/item/weapon/paper))
+/obj/machinery/bookbinder/attackby(obj/item/I, mob/user)
+ var/obj/item/weapon/paper/P = I
+ if(istype(P))
user.drop_item()
- O.loc = src
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/weapon/book/b = new(src.loc)
- b.dat = O:info
- b.name = "Print Job #" + "[rand(100, 999)]"
+ var/obj/item/weapon/book/b = new(loc)
+ b.dat = P.info
+ b.name = "Print Job #[rand(100, 999)]"
b.icon_state = "book[rand(1,7)]"
- qdel(O)
+ qdel(P)
+ return 1
else
- ..()
-
-/client/proc/delbook()
- set name = "Delete Book"
- set desc = "Permamently deletes a book from the database."
- set category = "Admin"
- if(!src.holder)
- to_chat(src, "Only administrators may use this command.")
- return
-
- var/isbn = input("ISBN number?", "Delete Book") as num | null
- if(!isbn)
- return
-
- var/DBQuery/query_delbook = dbcon.NewQuery("DELETE FROM [format_table_name("library")] WHERE id=[isbn]")
- if(!query_delbook.Execute())
- var/err = query_delbook.ErrorMsg()
- log_game("SQL ERROR deleting book. Error : \[[err]\]\n")
- return
-
- log_admin("[key_name(usr)] has deleted the book [isbn].")
- message_admins("[key_name_admin(usr)] has deleted the book [isbn].")
+ return ..()
\ No newline at end of file
diff --git a/icons/obj/library.dmi b/icons/obj/library.dmi
index 2501c48f23a..5404c91b3b9 100644
Binary files a/icons/obj/library.dmi and b/icons/obj/library.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 9736e6d070c..d4f61d749d3 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -1311,9 +1311,13 @@
#include "code\modules\hydroponics\trays\tray_tools.dm"
#include "code\modules\hydroponics\trays\tray_update_icons.dm"
#include "code\modules\karma\karma.dm"
+#include "code\modules\library\admin.dm"
#include "code\modules\library\lib_items.dm"
#include "code\modules\library\lib_machines.dm"
#include "code\modules\library\lib_readme.dm"
+#include "code\modules\library\computers\base.dm"
+#include "code\modules\library\computers\checkout.dm"
+#include "code\modules\library\computers\public.dm"
#include "code\modules\lighting\__lighting_docs.dm"
#include "code\modules\lighting\light_source.dm"
#include "code\modules\lighting\lighting_atom.dm"