diff --git a/aurorastation.dme b/aurorastation.dme index 36a4ac3d44a..4cf8d5c0685 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -2551,8 +2551,11 @@ #include "code\modules\law\laws\high_severity.dm" #include "code\modules\law\laws\low_severity.dm" #include "code\modules\law\laws\med_severity.dm" +#include "code\modules\library\lib_binder.dm" +#include "code\modules\library\lib_computer.dm" #include "code\modules\library\lib_items.dm" -#include "code\modules\library\lib_machines.dm" +#include "code\modules\library\lib_public_computer.dm" +#include "code\modules\library\lib_scanner.dm" #include "code\modules\lighting\emissive_blocker.dm" #include "code\modules\lighting\lighting_area.dm" #include "code\modules\lighting\lighting_atom.dm" diff --git a/code/modules/library/lib_binder.dm b/code/modules/library/lib_binder.dm new file mode 100644 index 00000000000..65402009455 --- /dev/null +++ b/code/modules/library/lib_binder.dm @@ -0,0 +1,58 @@ +/* + * Book Binder + */ +/obj/machinery/bookbinder + name = "book binder" + desc = "A machine that takes paper and binds them into books. Fascinating!" + icon = 'icons/obj/library.dmi' + icon_state = "binder" + anchored = TRUE + density = TRUE + var/binding = FALSE + +/obj/machinery/bookbinder/attackby(obj/item/attacking_item, mob/user) + if(istype(attacking_item, /obj/item/paper)) + var/obj/item/paper/paper = attacking_item + if(!anchored) + to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!")) + return + if(binding) + to_chat(user, SPAN_WARNING("You must wait for \the [src] to finish its current operation!")) + return + var/turf/work_turf = get_turf(src) + user.drop_from_inventory(paper, src) + user.visible_message( + SPAN_NOTICE("\The [user] loads some paper into \the [src]."), + SPAN_NOTICE("You load some paper into \the [src].")) + visible_message(SPAN_NOTICE("\The [src] begins to hum as it warms up its printing drums.")) + playsound(work_turf, 'sound/items/bureaucracy/binder.ogg', 75, 1) + binding = TRUE + sleep(rand(20 SECONDS, 40 SECONDS)) + binding = FALSE + if(!anchored) + visible_message(SPAN_WARNING("\The [src] buzzes and flashes an error light.")) + paper.forceMove(work_turf) + return + visible_message(SPAN_NOTICE("\The [src] whirs as it prints and binds a new book.")) + playsound(work_turf, 'sound/items/bureaucracy/print.ogg', 75, 1) + var/obj/item/book/bound_book = new(work_turf) + bound_book.dat = paper.info + bound_book.name = "blank book" + bound_book.icon_state = "book[rand(1,7)]" + bound_book.item_state = icon_state + qdel(paper) + return + + if(attacking_item.tool_behaviour == TOOL_WRENCH) + attacking_item.play_tool_sound(get_turf(src), 75) + if(anchored) + user.visible_message( + SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."), + SPAN_NOTICE("You unsecure \the [src] from the floor."), + SPAN_WARNING("You hear a ratcheting noise.")) + else + user.visible_message( + SPAN_NOTICE("\The [user] secures \the [src] to the floor."), + SPAN_NOTICE("You secure \the [src] to the floor."), + SPAN_WARNING("You hear a ratcheting noise.")) + anchored = !anchored diff --git a/code/modules/library/lib_computer.dm b/code/modules/library/lib_computer.dm new file mode 100644 index 00000000000..51a98667214 --- /dev/null +++ b/code/modules/library/lib_computer.dm @@ -0,0 +1,425 @@ +/* + * Borrowbook datum + */ +/// Tracks a single book checkout: what was borrowed, by whom, and when it is due. +/datum/borrowbook + var/book_name + var/mob_name + var/get_date + var/due_date + +/* + * Library Computer + */ +/obj/machinery/librarycomp + name = "library computer" + desc = "A computer." + icon = 'icons/obj/library.dmi' + icon_state = "computer" + anchored = TRUE + density = TRUE + var/upload_category = "Fiction" + /// Active book checkouts + var/list/datum/borrowbook/checkouts = list() + /// Weakrefs to physical books currently in the library's physical inventory + var/list/datum/weakref/inventory = list() + /// How long a checkout lasts, in minutes + var/checkout_period_minutes = 5 + var/bible_on_cooldown = FALSE + var/is_public = FALSE + var/buffer_book // Set by barcode scanner mode 1 + /// Title of the last successfully uploaded book, shown in the UI as confirmation + var/last_uploaded_title + var/datum/weakref/scanner_ref // Weakref to the nearest anchored book scanner + /// Current page of archive results (one page = 20 entries) + var/list/archive_results = list() + var/archive_loading = FALSE + var/archive_error = FALSE + var/archive_page = 1 + /// Total number of archive entries matching the current search + var/archive_total = 0 + var/archive_search = "" + var/archive_sort_field = "title" + var/archive_sort_dir = "asc" + +/obj/machinery/librarycomp/attack_hand(var/mob/user) + . = ..() + if(.) + return TRUE + ui_interact(user) + +/obj/machinery/librarycomp/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + var/title = is_public ? "[SSatlas.current_map.station_name] Library" : "[SSatlas.current_map.station_name] Library Management" + ui = new(user, src, "LibraryComputer", title, 750, 600) + ui.open() + +/obj/machinery/librarycomp/ui_data(mob/user) + var/obj/machinery/libraryscanner/scanner = scanner_ref?.resolve() + if(!scanner) + scanner_ref = null + for(var/obj/machinery/libraryscanner/nearby_scanner in range(9)) + if(nearby_scanner.anchored) + scanner_ref = WEAKREF(nearby_scanner) + scanner = nearby_scanner + break + + var/list/data = list() + data["is_public"] = is_public + data["is_emagged"] = emagged + data["bible_on_cooldown"] = bible_on_cooldown + data["checkout_period_minutes"] = checkout_period_minutes + data["archive_loading"] = archive_loading + data["archive_error"] = archive_error + data["archive_results"] = archive_results + data["archive_page"] = archive_page + data["archive_total"] = archive_total + data["archive_search"] = archive_search + data["archive_sort_field"] = archive_sort_field + data["archive_sort_dir"] = archive_sort_dir + data["archive_page_size"] = 20 + data["upload_category"] = upload_category + data["buffer_book"] = buffer_book + data["last_uploaded_title"] = last_uploaded_title + + var/list/inv = list() + var/list/stale = list() + for(var/datum/weakref/wref in inventory) + var/obj/item/book/book = wref.resolve() + if(book) + inv += list(list("ref" = REF(book), "name" = book.name)) + else + stale += wref + inventory -= stale + data["inventory"] = inv + + var/list/co = list() + for(var/datum/borrowbook/checkout in checkouts) + var/taken_minutes = round((world.time - checkout.get_date) / 1 MINUTES) + var/due_raw = (checkout.due_date - world.time) / 1 MINUTES + co += list(list( + "ref" = REF(checkout), + "book_name" = checkout.book_name, + "mob_name" = checkout.mob_name, + "taken_minutes" = taken_minutes, + "due_minutes" = abs(round(due_raw)), + "overdue" = (due_raw <= 0) + )) + data["checkouts"] = co + + var/list/scanner_data = list() + scanner_data["found"] = !!(scanner?.anchored) + scanner_data["title"] = scanner?.cache ? scanner.cache.name : null + scanner_data["author"] = scanner?.cache ? (scanner.cache.author ? scanner.cache.author : "Anonymous") : null + data["scanner"] = scanner_data + + var/list/crew_names = list() + var/list/manifest = SSrecords.get_manifest_list() + for(var/dept in manifest) + for(var/list/entry in manifest[dept]) + if(!entry["ooc_role"] && entry["name"]) + crew_names |= entry["name"] + data["crew_names"] = crew_names + + return data + +/obj/machinery/librarycomp/emag_act(var/remaining_charges, var/mob/user) + if(src.density && !src.emagged) + src.emagged = TRUE + return 1 + +/obj/machinery/librarycomp/attackby(obj/item/attacking_item, mob/user) + if(istype(attacking_item, /obj/item/barcodescanner)) + var/obj/item/barcodescanner/barcode_scanner = attacking_item + barcode_scanner.computer_ref = REF(src) + to_chat(user, "[barcode_scanner]'s associated machine has been set to [src].") + for(var/mob/hearer in hearers(src)) + hearer.show_message("[src] lets out a low, short blip.", 2) + else + ..() + +/obj/machinery/librarycomp/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + add_fingerprint(usr) + switch(action) + if("print_bible") + if(!bible_on_cooldown) + var/obj/item/storage/bible/bible = new /obj/item/storage/bible(src.loc) + bible.verbs += /obj/item/storage/bible/verb/Set_Religion + var/rand_book = "book" + pick("1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16") + bible.icon_state = rand_book + bible.item_state = rand_book + bible.name = "religious book" + bible_on_cooldown = TRUE + addtimer(CALLBACK(src, PROC_REF(end_bible_cooldown)), 6 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE) + . = TRUE + if("arcane_confirm") + if(emagged && !bible_on_cooldown) + new /obj/item/book/tome(get_turf(src)) + to_chat(usr, SPAN_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 dusty old tome sitting on the desk. You don't really remember printing it.")) + usr.visible_message( + SPAN_NOTICE("\The [usr] stares at the blank screen for a few moments, [usr.get_pronoun("his")] expression frozen in fear. When [usr.get_pronoun("he")] finally awakens from it, [usr.get_pronoun("he")] looks a lot older."), + range = 2) + bible_on_cooldown = TRUE + addtimer(CALLBACK(src, PROC_REF(end_bible_cooldown)), 6 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE) + . = TRUE + if("increase_checkout_period") + checkout_period_minutes += 1 + . = TRUE + if("decrease_checkout_period") + checkout_period_minutes = max(1, checkout_period_minutes - 1) + . = TRUE + if("checkout_book") + var/book_title = sanitizeSafe(params["book_title"]) + var/recipient = sanitize(params["recipient"], MAX_NAME_LEN) + if(book_title && recipient) + var/datum/borrowbook/new_checkout = new /datum/borrowbook + new_checkout.book_name = book_title + new_checkout.mob_name = recipient + new_checkout.get_date = world.time + new_checkout.due_date = world.time + (checkout_period_minutes * 1 MINUTES) + checkouts.Add(new_checkout) + . = TRUE + if("checkin_book") + var/datum/borrowbook/checkout = locate(params["ref"]) + if(checkout && (checkout in checkouts)) + checkouts.Remove(checkout) + . = TRUE + if("delete_inventory_book") + var/obj/item/book/book = locate(params["ref"]) + if(book && !QDELETED(book)) + inventory.Remove(WEAKREF(book)) + . = TRUE + if("set_upload_category") + var/new_category = params["value"] + if(new_category in list("Fiction", "Non-Fiction", "Reference", "Religion")) + upload_category = new_category + . = TRUE + if("set_upload_author") + var/obj/machinery/libraryscanner/sc = get_scanner() + if(sc?.cache) + sc.cache.author = sanitize(params["value"]) + . = TRUE + if("clear_scanner_cache") + var/obj/machinery/libraryscanner/sc = get_scanner() + if(sc) + sc.cache = null + SStgui.update_uis(sc) + last_uploaded_title = null + . = TRUE + if("upload_book") + var/obj/machinery/libraryscanner/sc = get_scanner() + if(!is_public && sc?.anchored && sc.cache) + if(sc.cache.unique) + to_chat(usr, SPAN_WARNING("This book has been rejected from the database.")) + else + INVOKE_ASYNC(src, PROC_REF(async_upload_book), usr) + . = TRUE + if("fetch_archive") + if(!archive_loading) + archive_page = 1 + archive_search = "" + INVOKE_ASYNC(src, PROC_REF(async_fetch_archive)) + . = TRUE + if("archive_go_to_page") + var/new_page = text2num(params["page"]) + if(isnum(new_page) && new_page >= 1 && !archive_loading) + archive_page = round(new_page) + INVOKE_ASYNC(src, PROC_REF(async_fetch_archive)) + . = TRUE + if("archive_set_search") + if(!archive_loading) + archive_search = sanitize(params["query"]) + archive_page = 1 + INVOKE_ASYNC(src, PROC_REF(async_fetch_archive)) + . = TRUE + if("archive_set_sort") + var/field = params["field"] + if(field in list("title", "author", "category")) + var/direction = params["dir"] + archive_sort_field = field + archive_sort_dir = (direction == "desc") ? "desc" : "asc" + archive_page = 1 + if(!archive_loading) + INVOKE_ASYNC(src, PROC_REF(async_fetch_archive)) + . = TRUE + if("order_book") + var/book_id = text2num(params["id"]) + if(isnum(book_id) && book_id > 0) + if(!bible_on_cooldown) + INVOKE_ASYNC(src, PROC_REF(async_order_book), book_id) + else + visible_message("\The [src]'s monitor flashes: \"Printer unavailable. Please allow a short time before attempting to print.\"") + . = TRUE + if("extend_checkout") + var/datum/borrowbook/checkout = locate(params["ref"]) + if(checkout && (checkout in checkouts)) + checkout.due_date += checkout_period_minutes * 1 MINUTES + . = TRUE + if("print_inventory") + var/dat = "Library Inventory

" + var/printed = 0 + for(var/datum/weakref/wref in inventory) + var/obj/item/book/book = wref.resolve() + if(book) + dat += "- [book.name]
" + printed++ + if(!printed) + dat += "No books in inventory.
" + var/obj/item/paper/printout = new /obj/item/paper(src.loc) + printout.info = dat + printout.name = "paper- 'Library Inventory'" + . = TRUE + if("print_checkouts") + var/dat = "Active Checkouts

" + if(checkouts.len) + for(var/datum/borrowbook/checkout in checkouts) + var/taken_minutes = round((world.time - checkout.get_date) / 1 MINUTES) + var/due_raw = (checkout.due_date - world.time) / 1 MINUTES + var/status = due_raw <= 0 ? "OVERDUE by [abs(round(due_raw))] min" : "due in [round(due_raw)] min" + dat += "- [checkout.book_name] ([checkout.mob_name]) — taken [taken_minutes] min ago, [status]
" + else + dat += "No books currently checked out.
" + var/obj/item/paper/printout = new /obj/item/paper(src.loc) + printout.info = dat + printout.name = "paper- 'Active Checkouts'" + . = TRUE + +/obj/machinery/librarycomp/proc/end_bible_cooldown() + bible_on_cooldown = FALSE + SStgui.update_uis(src) + +/obj/machinery/librarycomp/proc/get_scanner() + var/obj/machinery/libraryscanner/scanner = scanner_ref?.resolve() + if(!scanner) + scanner_ref = null + for(var/obj/machinery/libraryscanner/nearby_scanner in range(9)) + if(nearby_scanner.anchored) + scanner_ref = WEAKREF(nearby_scanner) + return nearby_scanner + return null + return scanner + +/obj/machinery/librarycomp/proc/async_fetch_archive() + archive_loading = TRUE + archive_error = FALSE + archive_total = 0 + archive_results = list() + SStgui.update_uis(src) + + // Build WHERE clause — keep % wildcards out of params via CONCAT so + // the param value is the raw search term without special characters + var/sql_where = "" + var/list/count_args + var/list/select_args + if(length(archive_search)) + sql_where = "WHERE (title LIKE CONCAT('%', :search, '%') OR author LIKE CONCAT('%', :search, '%') OR category LIKE CONCAT('%', :search, '%'))" + count_args = list("search" = archive_search) + select_args = list("search" = archive_search) + else + count_args = list() + select_args = list() + + // Validate sort field and direction before interpolating into SQL + var/safe_field + switch(archive_sort_field) + if("author") safe_field = "author" + if("category") safe_field = "category" + else safe_field = "title" + var/safe_dir = (archive_sort_dir == "desc") ? "DESC" : "ASC" + + // COUNT query to get total matching entries + var/datum/db_query/count_query = SSdbcore.NewQuery( + "SELECT COUNT(*) FROM ss13_library [sql_where]", + count_args) + if(!count_query.Execute()) + archive_error = TRUE + qdel(count_query) + archive_loading = FALSE + SStgui.update_uis(src) + return + archive_total = count_query.NextRow() ? text2num(count_query.item[1]) : 0 + qdel(count_query) + + // Clamp page to valid range now that we know the total + var/total_pages = max(1, CEILING(archive_total, 20) / 20) + archive_page = clamp(archive_page, 1, total_pages) + var/sql_offset = (archive_page - 1) * 20 + + // Paginated SELECT + var/datum/db_query/query = SSdbcore.NewQuery( + "SELECT id, author, title, category FROM ss13_library [sql_where] ORDER BY [safe_field] [safe_dir] LIMIT 20 OFFSET [sql_offset]", + select_args) + if(!query.Execute()) + archive_error = TRUE + qdel(query) + archive_loading = FALSE + SStgui.update_uis(src) + return + + var/list/results = list() + while(query.NextRow()) + results += list(list( + "id" = text2num(query.item[1]), + "author" = query.item[2], + "title" = query.item[3], + "category" = query.item[4] + )) + qdel(query) + archive_results = results + archive_loading = FALSE + SStgui.update_uis(src) + +/obj/machinery/librarycomp/proc/async_upload_book(mob/uploader) + var/obj/machinery/libraryscanner/scanner = get_scanner() + var/obj/item/book/book = scanner?.cache + if(!book) + return + var/datum/db_query/query = SSdbcore.NewQuery( + "INSERT INTO ss13_library (author, title, content, category, uploadtime, uploader) VALUES (:author, :title, :content, :category, NOW(), :uploader)", + list( + "author" = book.author ? book.author : "Anonymous", + "title" = book.name, + "content" = book.dat, + "category" = upload_category, + "uploader" = ckey(uploader.client.ckey) + )) + if(!query.Execute()) + to_chat(uploader, SPAN_WARNING("Upload failed: [query.last_error]")) + qdel(query) + return + qdel(query) + log_and_message_admins("has uploaded the book titled [book.name], [length(book.dat)] signs") + log_game("[uploader.name]/[uploader.key] has uploaded the book titled [book.name], [length(book.dat)] signs") + to_chat(uploader, SPAN_NOTICE("Upload complete.")) + last_uploaded_title = book.name + SStgui.update_uis(src) + +/obj/machinery/librarycomp/proc/async_order_book(var/book_id) + bible_on_cooldown = TRUE + addtimer(CALLBACK(src, PROC_REF(end_bible_cooldown)), 6 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE) + SStgui.update_uis(src) + + var/datum/db_query/query = SSdbcore.NewQuery( + "SELECT id, author, title, content FROM ss13_library WHERE id = :id", + list("id" = book_id)) + if(!query.Execute() || !query.NextRow()) + qdel(query) + return + var/obj/item/book/ordered_book = new(src.loc) + ordered_book.author = query.item[2] + ordered_book.title = query.item[3] + ordered_book.name = "Book: [query.item[3]]" + ordered_book.dat = query.item[4] + ordered_book.icon_state = "book[rand(1,16)]" + ordered_book.item_state = ordered_book.icon_state + qdel(query) + src.visible_message("\The [src]'s printer hums as it produces a book.") + +// Public Related Code +/obj/machinery/librarycomp/public + name = "public library computer" + is_public = TRUE diff --git a/code/modules/library/lib_items.dm b/code/modules/library/lib_items.dm index 8d71b3380ca..47bbf60631d 100644 --- a/code/modules/library/lib_items.dm +++ b/code/modules/library/lib_items.dm @@ -183,6 +183,14 @@ update_icon() +/* + * Barcode Scanner mode defines — used in book/attackby and barcodescanner/attack_self + */ +#define BARCODE_SCAN_BUFFER_ONLY 0 +#define BARCODE_SCAN_BUFFER_COMPUTER 1 +#define BARCODE_SCAN_CHECKIN 2 +#define BARCODE_SCAN_ADD_INVENTORY 3 + /* * Book */ @@ -273,35 +281,37 @@ return else if(istype(attacking_item, /obj/item/barcodescanner)) var/obj/item/barcodescanner/scanner = attacking_item - if(!scanner.computer) + var/obj/machinery/librarycomp/comp = locate(scanner.computer_ref) + if(!comp || QDELETED(comp)) to_chat(user, "[attacking_item]'s screen flashes: 'No associated computer found!'") else switch(scanner.mode) - if(0) - scanner.book = src + if(BARCODE_SCAN_BUFFER_ONLY) + scanner.book_ref = REF(src) to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer.'") - if(1) - scanner.book = src - scanner.computer.buffer_book = src.name + if(BARCODE_SCAN_BUFFER_COMPUTER) + scanner.book_ref = REF(src) + comp.buffer_book = src.name to_chat(user, "[attacking_item]'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) + if(BARCODE_SCAN_CHECKIN) + scanner.book_ref = REF(src) + for(var/datum/borrowbook/checkout in comp.checkouts) + if(checkout.book_name == src.name) + comp.checkouts.Remove(checkout) to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Book has been checked in.'") return to_chat(user, "[attacking_item]'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, "[attacking_item]'s screen flashes: 'Book stored in buffer. Title already present in inventory, aborting to avoid duplicate entry.'") - return - scanner.computer.inventory.Add(src) + if(BARCODE_SCAN_ADD_INVENTORY) + scanner.book_ref = REF(src) + var/datum/weakref/src_wref = WEAKREF(src) + if(src_wref in comp.inventory) + to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Title already present in inventory, aborting to avoid duplicate entry.'") + return + comp.inventory.Add(src_wref) to_chat(user, "[attacking_item]'s screen flashes: 'Book stored in buffer. Title added to general inventory.'") else if(istype(attacking_item, /obj/item/material/knife) || attacking_item.tool_behaviour == TOOL_WIRECUTTER) - if(carved) return + if(carved) + return to_chat(user, SPAN_NOTICE("You begin to carve out [title].")) if(attacking_item.use_tool(src, user, 30, volume = 50)) to_chat(user, SPAN_NOTICE("You carve out the pages from [title]! You didn't want to read it anyway.")) @@ -326,34 +336,40 @@ /obj/item/barcodescanner name = "book scanner" icon = 'icons/obj/library.dmi' - icon_state ="scanner" + icon_state = "scanner" throw_speed = 1 throw_range = 5 w_class = WEIGHT_CLASS_SMALL - var/obj/machinery/librarycomp/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 + var/computer_ref // REF string of the associated library computer + var/book_ref // REF string of the currently scanned book + var/mode = BARCODE_SCAN_BUFFER_ONLY -/obj/item/barcodescanner/attack_self(mob/user as mob) +/obj/item/barcodescanner/attack_self(mob/user) mode += 1 - if(mode > 3) - mode = 0 + if(mode > BARCODE_SCAN_ADD_INVENTORY) + mode = BARCODE_SCAN_BUFFER_ONLY to_chat(user, "[src] Status Display:") - var/modedesc + var/mode_desc 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." + if(BARCODE_SCAN_BUFFER_ONLY) + mode_desc = "Scan book to local buffer." + if(BARCODE_SCAN_BUFFER_COMPUTER) + mode_desc = "Scan book to local buffer and set associated computer buffer to match." + if(BARCODE_SCAN_CHECKIN) + mode_desc = "Scan book to local buffer, attempt to check in scanned book." + if(BARCODE_SCAN_ADD_INVENTORY) + mode_desc = "Scan book to local buffer, attempt to add book to general inventory." else - modedesc = "ERROR" - to_chat(user, " - Mode [mode] : [modedesc]") - if(src.computer) + mode_desc = "ERROR" + to_chat(user, " - Mode [mode] : [mode_desc]") + var/obj/machinery/librarycomp/comp = locate(src.computer_ref) + if(comp && !QDELETED(comp)) to_chat(user, SPAN_NOTICE("Computer has been associated with this unit.")) else to_chat(user, SPAN_WARNING("No associated computer found. Only local scans will function properly.")) to_chat(user, "\n") + +#undef BARCODE_SCAN_BUFFER_ONLY +#undef BARCODE_SCAN_BUFFER_COMPUTER +#undef BARCODE_SCAN_CHECKIN +#undef BARCODE_SCAN_ADD_INVENTORY diff --git a/code/modules/library/lib_machines.dm b/code/modules/library/lib_machines.dm deleted file mode 100644 index 88c05cad061..00000000000 --- a/code/modules/library/lib_machines.dm +++ /dev/null @@ -1,517 +0,0 @@ -/* Library Machines - * - * Contains: - * * Borrowbook datum - * * Library Public Computer - * * Library Computer - * * Library Scanner - * * Book Binder - */ - -/* - * 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 - -/* - * Library Public Computer - */ -/obj/machinery/librarypubliccomp - name = "public library computer" - desc = "A computer." - icon = 'icons/obj/library.dmi' - icon_state = "computer" - anchored = TRUE - density = TRUE - var/screenstate = 0 - var/title - var/category = "Any" - var/author - var/SQLquery - -/obj/machinery/librarypubliccomp/attack_hand(var/mob/user) - usr.set_machine(src) - var/dat = "" - switch(screenstate) - if(0) - dat += {"

Search Settings


- Filter by Title: [title]
- Filter by Category: [category]
- Filter by Author: [author]
- \[Start Search\]
"} - if(1) - if(!establish_db_connection(GLOB.dbcon)) - 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 += {" - "} - - var/DBQuery/query = GLOB.dbcon.NewQuery(SQLquery) - query.Execute() - - while(query.NextRow()) - var/author = query.item[1] - var/title = query.item[2] - var/category = query.item[3] - var/id = query.item[4] - dat += "" - dat += "
AUTHORTITLECATEGORYSS13BN
[author][title][category][id]

" - dat += "\[Go Back\]
" - user << browse(HTML_SKELETON_TITLE("Library Visitor", dat), "window=publiclibrary") - onclose(user, "publiclibrary") - -/obj/machinery/librarypubliccomp/Topic(href, href_list) - if(..()) - usr << browse(null, "window=publiclibrary") - onclose(usr, "publiclibrary") - 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", "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 ss13_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 - - if(href_list["back"]) - screenstate = 0 - - src.add_fingerprint(usr) - src.updateUsrDialog() - return - -/* - * Library Computer - */ -/obj/machinery/librarycomp - name = "library computer" - desc = "A computer." - icon = 'icons/obj/library.dmi' - icon_state = "computer" - anchored = TRUE - density = TRUE - var/arcanecheckout = FALSE - var/screenstate = 0 // 0: Main Menu - 1: Inventory - 2: Checked Out - 3: Check Out - var/sortby = "author" - var/buffer_book - var/buffer_mob - var/upload_category = "Fiction" - var/list/checkouts = list() - var/list/inventory = list() - var/checkoutperiod = 5 // In minutes - var/bibledelay = 0 - var/is_public = FALSE - var/obj/machinery/libraryscanner/scanner // Book scanner that will be used when uploading books to the Archive - -/obj/machinery/librarycomp/attack_hand(var/mob/user) - user.set_machine(src) - var/dat = "" - switch(screenstate) - if(0) - // Main Menu - dat += "View Stock
" - dat += "View Checked Out Books
" - dat += "Check out a Book
" - dat += "Order From Library Database
" - if(!is_public) - dat += "Upload New Title to Library Database
" - dat += "Print a Bible
" - if(emagged) - dat += "7. Access the Forbidden Lore Vault
" - if(arcanecheckout) - new /obj/item/book/tome(get_turf(src)) - to_chat(user, SPAN_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 dusty old tome sitting on the desk. You don't really remember printing it.")) - user.visible_message(SPAN_NOTICE("\The [user] stares at the blank screen for a few moments, [user.get_pronoun("his")] expression frozen in fear. When [user.get_pronoun("he")] finally awakens from it, [user.get_pronoun("he")] looks a lot older."), range = 2) - arcanecheckout = FALSE - if(1) - // Inventory - dat += "

Inventory


" - for(var/obj/item/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(!establish_db_connection(GLOB.dbcon)) - dat += "ERROR: Unable to contact External Archive. Please contact your system administrator for assistance." - else - dat += {"(Order Book by ISBN)

- - "} - var/DBQuery/query = GLOB.dbcon.NewQuery("SELECT id, author, title, category FROM ss13_library ORDER BY [sortby]") - query.Execute() - - while(query.NextRow()) - var/id = query.item[1] - var/author = query.item[2] - var/title = query.item[3] - var/category = query.item[4] - dat += "" - dat += "
AUTHORTITLECATEGORY
[author][title][category]\[Order\]
" - dat += "
(<-- Return to Main Menu)
" - if(5) - dat += "

Upload a New Title

" - if(!scanner) - for(var/obj/machinery/libraryscanner/S in range(9)) - if(S.anchored) - scanner = S - break - if(!(scanner?.anchored)) - 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 += {"

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.
"} - - //dat += "Close

" - user << browse(HTML_SKELETON_TITLE(is_public ? "[SSatlas.current_map.station_name] Library" : "[SSatlas.current_map.station_name] Library Management", dat), "window=library") - onclose(user, "library") - -/obj/machinery/librarycomp/emag_act(var/remaining_charges, var/mob/user) - if (src.density && !src.emagged) - src.emagged = 1 - return 1 - -/obj/machinery/librarycomp/attackby(obj/item/attacking_item, mob/user) - if(istype(attacking_item, /obj/item/barcodescanner)) - var/obj/item/barcodescanner/scanner = attacking_item - 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/Topic(href, href_list) - if(..()) - usr << browse(null, "window=library") - onclose(usr, "library") - return - - 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) - B.verbs += /obj/item/storage/bible/verb/Set_Religion - var/randbook = "book" + pick("1", "2", "3", "4", "5", "6" , "7", "8", "9", "10", "11", "12", "13" , "14", "15" , "16") - B.icon_state = randbook - B.item_state = randbook - B.name = "religious book" - - bibledelay = 1 - spawn(60) - bibledelay = 0 - - else - for (var/mob/V in hearers(src)) - V.show_message("[src]'s monitor flashes, \"Bible printer currently unavailable, please wait a moment.\"") - - 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 = sanitizeSafe(input("Enter the book's title:") as text|null) - if(href_list["editmob"]) - buffer_mob = sanitize(input("Enter the recipient's name:") as text|null, MAX_NAME_LEN) - if(href_list["checkout"]) - var/datum/borrowbook/b = new /datum/borrowbook - b.bookname = sanitizeSafe(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["setauthor"]) - var/newauthor = sanitize(input("Enter the author's name: ") as text|null) - if(newauthor) - scanner.cache.author = newauthor - if(href_list["setcategory"]) - var/newcategory = input("Choose a category: ") in list("Fiction", "Non-Fiction", "Reference", "Religion") - if(newcategory) - upload_category = newcategory - if(href_list["upload"]) - if(scanner?.anchored) - 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(scanner.cache.unique) - alert("This book has been rejected from the database. Aborting!") - else - if(!establish_db_connection(GLOB.dbcon)) - 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/sqlckey = sanitizeSQL(ckey(usr.client.ckey)) - var/DBQuery/query = GLOB.dbcon.NewQuery("INSERT INTO ss13_library (author, title, content, category, uploadtime, uploader) VALUES ('[sqlauthor]', '[sqltitle]', '[sqlcontent]', '[sqlcategory]', NOW(), '[sqlckey]')") - if(!query.Execute()) - to_chat(usr, query.ErrorMsg()) - else - log_and_message_admins("has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] signs") - log_game("[usr.name]/[usr.key] has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] signs") - alert("Upload Complete.") - - if(href_list["targetid"]) - var/sqlid = sanitizeSQL(href_list["targetid"]) - if(!establish_db_connection(GLOB.dbcon)) - alert("Connection to Archive has been severed. Aborting.") - 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 - var/DBQuery/query = GLOB.dbcon.NewQuery("SELECT * FROM ss13_library WHERE id=[sqlid]") - query.Execute() - - while(query.NextRow()) - var/author = query.item[2] - var/title = query.item[3] - var/content = query.item[4] - var/obj/item/book/B = new(src.loc) - B.name = "Book: [title]" - B.title = title - B.author = author - B.dat = content - B.icon_state = "book[rand(1,16)]" - B.item_state = B.icon_state - src.visible_message("\The [src]\s printer hums as it produces a book.") - break - 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) - if(href_list["sort"] in list("author", "title", "category")) - sortby = href_list["sort"] - src.add_fingerprint(usr) - src.updateUsrDialog() - return - -// Public Related Code -/obj/machinery/librarycomp/public - name = "public library computer" - is_public = TRUE - -/* - * Library Scanner - */ -/obj/machinery/libraryscanner - name = "book scanner" - desc = "A machine that scans books for upload to the library database." - icon = 'icons/obj/library.dmi' - icon_state = "bigscanner" - var/insert_anim = "bigscanner1" - anchored = TRUE - density = TRUE - var/obj/item/book/cache // Last scanned book - -/obj/machinery/libraryscanner/attackby(obj/item/attacking_item, mob/user) - if(istype(attacking_item, /obj/item/book)) - if(!anchored) - to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!")) - return - user.drop_from_inventory(attacking_item,src) - if(attacking_item.tool_behaviour == TOOL_WRENCH) - attacking_item.play_tool_sound(get_turf(src), 75) - if(anchored) - user.visible_message(SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."), - SPAN_NOTICE("You unsecure \the [src] from the floor."), - SPAN_WARNING("You hear a ratcheting noise.")) - else - user.visible_message(SPAN_NOTICE("\The [user] secures \the [src] to the floor."), - SPAN_NOTICE("You secure \the [src] to the floor."), - SPAN_WARNING("You hear a ratcheting noise.")) - anchored = !anchored - -/obj/machinery/libraryscanner/attack_hand(var/mob/user) - usr.set_machine(src) - var/dat = "" - if(cache) - dat += "Data stored in memory.
" - else - dat += "No data stored in memory.
" - dat += "\[Scan\]" - if(cache) - dat += " \[Clear Memory\]

\[Remove Book\]" - else - dat += "
" - user << browse(HTML_SKELETON_TITLE("Scanner Control Interface", 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"]) - flick(insert_anim, src) - playsound(loc, 'sound/items/bureaucracy/scan.ogg', 75, 1) - 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.forceMove(src.loc) - src.add_fingerprint(usr) - src.updateUsrDialog() - return - -/* - * Book binder - */ -/obj/machinery/bookbinder - name = "book binder" - desc = "A machine that takes paper and binds them into books. Fascinating!" - icon = 'icons/obj/library.dmi' - icon_state = "binder" - anchored = TRUE - density = TRUE - var/binding = FALSE - -/obj/machinery/bookbinder/attackby(obj/item/attacking_item, mob/user) - if(istype(attacking_item, /obj/item/paper)) - var/obj/item/paper/paper = attacking_item - if(!anchored) - to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!")) - return - if(binding) - to_chat(user, SPAN_WARNING("You must wait for \the [src] to finish its current operation!")) - return - var/turf/T = get_turf(src) - user.drop_from_inventory(paper,src) - user.visible_message(SPAN_NOTICE("\The [user] loads some paper into \the [src]."), SPAN_NOTICE("You load some paper into \the [src].")) - visible_message(SPAN_NOTICE("\The [src] begins to hum as it warms up its printing drums.")) - playsound(T, 'sound/items/bureaucracy/binder.ogg', 75, 1) - binding = TRUE - sleep(rand(200,400)) - binding = FALSE - if(!anchored) - visible_message(SPAN_WARNING("\The [src] buzzes and flashes an error light.")) - paper.forceMove(T) - return - visible_message(SPAN_NOTICE("\The [src] whirs as it prints and binds a new book.")) - playsound(T, 'sound/items/bureaucracy/print.ogg', 75, 1) - var/obj/item/book/b = new(T) - b.dat = paper.info - b.name = "blank book" - b.icon_state = "book[rand(1,7)]" - b.item_state = icon_state - qdel(paper) - return - - if(attacking_item.tool_behaviour == TOOL_WRENCH) - attacking_item.play_tool_sound(get_turf(src), 75) - if(anchored) - user.visible_message(SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."), \ - SPAN_NOTICE("You unsecure \the [src] from the floor."), \ - SPAN_WARNING("You hear a ratcheting noise.")) - else - user.visible_message(SPAN_NOTICE("\The [user] secures \the [src] to the floor."), \ - SPAN_NOTICE("You secure \the [src] to the floor."), \ - SPAN_WARNING("You hear a ratcheting noise.")) - anchored = !anchored diff --git a/code/modules/library/lib_public_computer.dm b/code/modules/library/lib_public_computer.dm new file mode 100644 index 00000000000..6da669b464a --- /dev/null +++ b/code/modules/library/lib_public_computer.dm @@ -0,0 +1,97 @@ +/* + * Library Public Computer + */ +/obj/machinery/librarypubliccomp + name = "public library computer" + desc = "A computer." + icon = 'icons/obj/library.dmi' + icon_state = "computer" + anchored = TRUE + density = TRUE + var/list/search_results = list() + var/search_title = "" + var/search_author = "" + var/search_category = "Any" + var/db_loading = FALSE + var/db_error = FALSE + +/obj/machinery/librarypubliccomp/attack_hand(var/mob/user) + . = ..() + if(.) + return TRUE + ui_interact(user) + +/obj/machinery/librarypubliccomp/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "LibraryPublicComputer", "Public Library Terminal", 650, 500) + ui.open() + +/obj/machinery/librarypubliccomp/ui_data(mob/user) + var/list/data = list() + data["search_title"] = search_title + data["search_author"] = search_author + data["search_category"] = search_category + data["db_loading"] = db_loading + data["db_error"] = db_error + data["search_results"] = search_results + return data + +/obj/machinery/librarypubliccomp/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + add_fingerprint(usr) + switch(action) + if("search") + if(!db_loading) + search_title = sanitize(params["title"]) + search_author = sanitize(params["author"]) + var/new_category = params["category"] + if(new_category in list("Any", "Fiction", "Non-Fiction", "Reference", "Religion")) + search_category = new_category + INVOKE_ASYNC(src, PROC_REF(async_search)) + . = TRUE + +/obj/machinery/librarypubliccomp/proc/async_search() + db_loading = TRUE + db_error = FALSE + search_results = list() + SStgui.update_uis(src) + + var/sql + var/list/query_args + if(search_category != "Any") + sql = "SELECT author, title, category, id FROM ss13_library WHERE author LIKE :author AND title LIKE :title AND category = :category" + query_args = list( + "title" = "%[search_title]%", + "author" = "%[search_author]%", + "category" = search_category + ) + else + sql = "SELECT author, title, category, id FROM ss13_library WHERE author LIKE :author AND title LIKE :title" + query_args = list( + "title" = "%[search_title]%", + "author" = "%[search_author]%" + ) + + var/datum/db_query/query = SSdbcore.NewQuery(sql, query_args) + if(!query.Execute()) + db_error = TRUE + qdel(query) + db_loading = FALSE + SStgui.update_uis(src) + return + + var/list/results = list() + while(query.NextRow()) + results += list(list( + "author" = query.item[1], + "title" = query.item[2], + "category" = query.item[3], + "id" = text2num(query.item[4]) + )) + qdel(query) + search_results = results + db_loading = FALSE + SStgui.update_uis(src) diff --git a/code/modules/library/lib_scanner.dm b/code/modules/library/lib_scanner.dm new file mode 100644 index 00000000000..febc8e7f52e --- /dev/null +++ b/code/modules/library/lib_scanner.dm @@ -0,0 +1,74 @@ +/* + * Library Scanner + */ +/obj/machinery/libraryscanner + name = "book scanner" + desc = "A machine that scans books for upload to the library database." + icon = 'icons/obj/library.dmi' + icon_state = "bigscanner" + var/insert_animation = "bigscanner1" + anchored = TRUE + density = TRUE + var/obj/item/book/cache // Last scanned book + +/obj/machinery/libraryscanner/attackby(obj/item/attacking_item, mob/user) + if(istype(attacking_item, /obj/item/book)) + if(!anchored) + to_chat(user, SPAN_WARNING("\The [src] must be secured to the floor first!")) + return + user.drop_from_inventory(attacking_item, src) + if(attacking_item.tool_behaviour == TOOL_WRENCH) + attacking_item.play_tool_sound(get_turf(src), 75) + if(anchored) + user.visible_message( + SPAN_NOTICE("\The [user] unsecures \the [src] from the floor."), + SPAN_NOTICE("You unsecure \the [src] from the floor."), + SPAN_WARNING("You hear a ratcheting noise.")) + else + user.visible_message( + SPAN_NOTICE("\The [user] secures \the [src] to the floor."), + SPAN_NOTICE("You secure \the [src] to the floor."), + SPAN_WARNING("You hear a ratcheting noise.")) + anchored = !anchored + +/obj/machinery/libraryscanner/attack_hand(var/mob/user) + . = ..() + if(.) + return TRUE + ui_interact(user) + +/obj/machinery/libraryscanner/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "LibraryScanner", "Book Scanner", 400, 250) + ui.open() + +/obj/machinery/libraryscanner/ui_data(mob/user) + var/list/data = list() + data["has_book"] = !!cache + data["book_title"] = cache ? cache.name : null + data["book_author"] = cache ? cache.author : null + data["is_anchored"] = anchored + return data + +/obj/machinery/libraryscanner/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + add_fingerprint(usr) + switch(action) + if("scan") + flick(insert_animation, src) + playsound(loc, 'sound/items/bureaucracy/scan.ogg', 75, 1) + for(var/obj/item/book/book in contents) + cache = book + break + . = TRUE + if("clear") + cache = null + . = TRUE + if("eject") + for(var/obj/item/book/book in contents) + book.forceMove(src.loc) + cache = null + . = TRUE diff --git a/html/changelogs/arrow768-library.yml b/html/changelogs/arrow768-library.yml new file mode 100644 index 00000000000..0709c4bf898 --- /dev/null +++ b/html/changelogs/arrow768-library.yml @@ -0,0 +1,58 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# - (fixes bugs) +# wip +# - (work in progress) +# qol +# - (quality of life) +# soundadd +# - (adds a sound) +# sounddel +# - (removes a sound) +# rscadd +# - (adds a feature) +# rscdel +# - (removes a feature) +# imageadd +# - (adds an image or sprite) +# imagedel +# - (removes an image or sprite) +# spellcheck +# - (fixes spelling or grammar) +# experiment +# - (experimental change) +# balance +# - (balance changes) +# code_imp +# - (misc internal code change) +# refactor +# - (refactors code) +# config +# - (makes a change to the config files) +# admin +# - (makes changes to administrator tools) +# server +# - (miscellaneous changes to server) +################################# + +# Your name. +author: arrow768 + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, this gets changed to [] after reading. Just remove the brackets when you add new shit. +# Please surround your changes in double quotes ("). It works without them, but if you use certain characters it screws up compiling. The quotes will not show up in the changelog. +changes: + - refactor: "Updates the Library Machines to TGUI" diff --git a/tgui/packages/tgui/interfaces/LibraryComputer.tsx b/tgui/packages/tgui/interfaces/LibraryComputer.tsx new file mode 100644 index 00000000000..d0b878346e4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LibraryComputer.tsx @@ -0,0 +1,755 @@ +import { BooleanLike } from '../../common/react'; +import { useBackend, useLocalState } from '../backend'; +import { + Box, + Button, + Icon, + Input, + LabeledList, + NoticeBox, + Section, + Stack, + Table, + Tabs, +} from '../components'; +import { Dropdown } from '../components/Dropdown'; +import { Window } from '../layouts'; + +export type LibraryComputerData = { + is_public: BooleanLike; + is_emagged: BooleanLike; + bible_on_cooldown: BooleanLike; + checkout_period_minutes: number; + archive_loading: BooleanLike; + archive_error: BooleanLike; + archive_results: ArchiveEntry[]; + archive_page: number; + archive_total: number; + archive_search: string; + archive_sort_field: string; + archive_sort_dir: string; + archive_page_size: number; + upload_category: string; + buffer_book: string | null; + last_uploaded_title: string | null; + inventory: InventoryBook[]; + checkouts: CheckoutEntry[]; + scanner: ScannerState; + crew_names: string[]; +}; + +type InventoryBook = { + ref: string; + name: string; +}; + +type CheckoutEntry = { + ref: string; + book_name: string; + mob_name: string; + taken_minutes: number; + due_minutes: number; + overdue: BooleanLike; +}; + +type ArchiveEntry = { + id: number; + author: string; + title: string; + category: string; +}; + +type ScannerState = { + found: BooleanLike; + title: string | null; + author: string | null; +}; + +const UPLOAD_CATEGORIES = ['Fiction', 'Non-Fiction', 'Reference', 'Religion']; + +export const LibraryComputer = (props, context) => { + const { act, data } = useBackend(context); + const [tab, setTab] = useLocalState(context, 'tab', 'inventory'); + + const handleTabChange = (newTab: string) => { + setTab(newTab); + if ( + newTab === 'archive' && + !data.archive_loading && + data.archive_results.length === 0 + ) { + act('fetch_archive'); + } + }; + + return ( + + + + handleTabChange('inventory')} + > + Inventory + + handleTabChange('checkouts')} + > + Checked Out + + handleTabChange('checkout')} + > + Check Out + + handleTabChange('archive')} + > + Order From Archive + + {!data.is_public && ( + handleTabChange('upload')} + > + Upload Title + + )} + handleTabChange('bible')} + > + Bible Printer + + {!!data.is_emagged && ( + handleTabChange('vault')} + > + Forbidden Lore + + )} + + + {tab === 'inventory' && } + {tab === 'checkouts' && } + {tab === 'checkout' && } + {tab === 'archive' && } + {tab === 'upload' && !data.is_public && } + {tab === 'bible' && } + {tab === 'vault' && !!data.is_emagged && } + + + + ); +}; + +const InventoryTab = (props, context) => { + const { act, data } = useBackend(context); + + return ( +

0 && ( +
+ ); +}; + +const CheckoutsTab = (props, context) => { + const { act, data } = useBackend(context); + + return ( +
0 && ( +
+ ); +}; + +const CheckOutTab = (props, context) => { + const { act, data } = useBackend(context); + const [bookTitle, setBookTitle] = useLocalState(context, 'co_book', ''); + const [recipient, setRecipient] = useLocalState(context, 'co_recip', ''); + const [showBookPicker, setShowBookPicker] = useLocalState( + context, + 'co_book_picker', + false, + ); + const [showRecipientPicker, setShowRecipientPicker] = useLocalState( + context, + 'co_recip_picker', + false, + ); + + return ( +
+ {!!data.buffer_book && bookTitle !== data.buffer_book && ( + +
+ ); +}; + +type SortField = 'author' | 'title' | 'category'; + +const ArchiveTab = (props, context) => { + const { act, data } = useBackend(context); + // Local state only for the search input — committed to server on Enter or button click + const [searchInput, setSearchInput] = useLocalState( + context, + 'archive_search_input', + data.archive_search, + ); + const [isbnInput, setIsbnInput] = useLocalState(context, 'isbn_input', ''); + + const totalPages = Math.max( + 1, + Math.ceil(data.archive_total / data.archive_page_size), + ); + + const commitSearch = (value: string) => { + if (!data.archive_loading) { + act('archive_set_search', { query: value }); + } + }; + + const handleSort = (field: SortField) => { + const newDir = + data.archive_sort_field === field && data.archive_sort_dir === 'asc' + ? 'desc' + : 'asc'; + act('archive_set_sort', { field, dir: newDir }); + }; + + const SortHeader = ({ + field, + label, + }: { + field: SortField; + label: string; + }) => ( + handleSort(field)} + > + {label} + {data.archive_sort_field === field && ( + + )} + + ); + + return ( +
act('fetch_archive')} + /> + } + > + + + setSearchInput(v)} + onEnter={(e, v) => commitSearch(v)} + /> + + +
+ ); +}; + +const UploadTab = (props, context) => { + const { act, data } = useBackend(context); + const { scanner } = data; + + return ( +
+ {!!data.last_uploaded_title && ( + + + + + Uploaded:{' '} + + {data.last_uploaded_title} + + + +
+ ); +}; + +const BibleTab = (props, context) => { + const { act, data } = useBackend(context); + + return ( +
+
+ ); +}; + +const VaultTab = (props, context) => { + const { act, data } = useBackend(context); + + return ( +
+ + Are you absolutely sure you want to proceed? EldritchTomes Inc. takes no + responsibility for loss of sanity resulting from this action. + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/LibraryPublicComputer.tsx b/tgui/packages/tgui/interfaces/LibraryPublicComputer.tsx new file mode 100644 index 00000000000..15edbcb62c2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LibraryPublicComputer.tsx @@ -0,0 +1,136 @@ +import { BooleanLike } from '../../common/react'; +import { useBackend, useLocalState } from '../backend'; +import { + Box, + Button, + Icon, + Input, + LabeledList, + NoticeBox, + Section, + Stack, + Table, +} from '../components'; +import { Dropdown } from '../components/Dropdown'; +import { Window } from '../layouts'; + +export type LibraryPublicComputerData = { + search_title: string; + search_author: string; + search_category: string; + db_loading: BooleanLike; + db_error: BooleanLike; + search_results: SearchResult[]; +}; + +type SearchResult = { + author: string; + title: string; + category: string; + id: number; +}; + +const CATEGORIES = ['Any', 'Fiction', 'Non-Fiction', 'Reference', 'Religion']; + +export const LibraryPublicComputer = (props, context) => { + const { act, data } = useBackend(context); + const [title, setTitle] = useLocalState(context, 'title', ''); + const [author, setAuthor] = useLocalState(context, 'author', ''); + const [category, setCategory] = useLocalState( + context, + 'category', + data.search_category, + ); + const [hasSearched, setHasSearched] = useLocalState( + context, + 'hasSearched', + false, + ); + + return ( + + +
+ + + setTitle(val)} + /> + + + setAuthor(val)} + /> + + + setCategory(val)} + /> + + + +
+ {!!data.db_error && ( + + Unable to contact External Archive. Please contact your system + administrator. + + )} + {!!data.db_loading && ( +
+ + + + + Searching archive... + +
+ )} + {!data.db_loading && data.search_results.length > 0 && ( +
+ + + Author + Title + Category + SS13BN + + {data.search_results.map((r) => ( + + {r.author} + {r.title} + {r.category} + {r.id} + + ))} +
+
+ )} + {!data.db_loading && + !data.db_error && + hasSearched && + data.search_results.length === 0 && ( + No results found. Try a different search. + )} +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/LibraryScanner.tsx b/tgui/packages/tgui/interfaces/LibraryScanner.tsx new file mode 100644 index 00000000000..5caf1bdc7f5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LibraryScanner.tsx @@ -0,0 +1,72 @@ +import { BooleanLike } from '../../common/react'; +import { useBackend } from '../backend'; +import { Box, Button, NoticeBox, Section } from '../components'; +import { Window } from '../layouts'; + +export type LibraryScannerData = { + has_book: BooleanLike; + book_title: string | null; + book_author: string | null; + is_anchored: BooleanLike; +}; + +export const LibraryScanner = (props, context) => { + const { act, data } = useBackend(context); + + if (!data.is_anchored) { + return ( + + + + The scanner must be secured to the floor first. + + + + ); + } + + return ( + + +
+ {data.has_book ? ( + <> + Data stored in memory. + + + Title: + {' '} + {data.book_title} + + + + Author: + {' '} + {data.book_author || 'Anonymous'} + + + ) : ( + No data stored in memory. + )} +
+
+
+
+
+ ); +};