diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index 9696082994db..9991a037ee06 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -802,6 +802,23 @@ CREATE TABLE `subsystem_metrics` ( PRIMARY KEY (`id`) USING BTREE ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- +-- Table structure for table `cassettes` +-- +DROP TABLE IF EXISTS `cassettes`; +CREATE TABLE `cassettes` ( + `id` VARCHAR(255) NOT NULL PRIMARY KEY, + `name` VARCHAR(42) NOT NULL, + `desc` VARCHAR(144) NOT NULL, + `status` TINYINT UNSIGNED NOT NULL, + `author_name` VARCHAR(42) NOT NULL, + `author_ckey` VARCHAR(30) NOT NULL, + `front` TEXT NOT NULL DEFAULT '{}', + `back` TEXT NOT NULL DEFAULT '{}', + CONSTRAINT `front` CHECK (json_valid(`front`)), + CONSTRAINT `back` CHECK (json_valid(`back`)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/_maps/map_files/BoxStation/BoxStation.dmm b/_maps/map_files/BoxStation/BoxStation.dmm index 82cec3c519f6..bf66eb14fa5d 100644 --- a/_maps/map_files/BoxStation/BoxStation.dmm +++ b/_maps/map_files/BoxStation/BoxStation.dmm @@ -21346,7 +21346,7 @@ pixel_y = -32 }, /obj/structure/table/wood, -/obj/item/device/cassette_tape/friday{ +/obj/item/cassette_tape/friday{ pixel_y = 2; pixel_x = 9 }, diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm index 580dbe6c6205..32647bd0032a 100644 --- a/_maps/map_files/MetaStation/MetaStation.dmm +++ b/_maps/map_files/MetaStation/MetaStation.dmm @@ -51163,15 +51163,15 @@ pixel_y = 4; pixel_x = -6 }, -/obj/item/device/cassette_tape/blank{ +/obj/item/cassette_tape/blank{ pixel_y = 3; pixel_x = 5 }, -/obj/item/device/cassette_tape/blank{ +/obj/item/cassette_tape/blank{ pixel_y = 3; pixel_x = 5 }, -/obj/item/device/cassette_tape/blank{ +/obj/item/cassette_tape/blank{ pixel_y = 3; pixel_x = 5 }, diff --git a/_maps/map_files/Voidraptor/VoidRaptor.dmm b/_maps/map_files/Voidraptor/VoidRaptor.dmm index dbae07341eae..c1d858bd99d0 100644 --- a/_maps/map_files/Voidraptor/VoidRaptor.dmm +++ b/_maps/map_files/Voidraptor/VoidRaptor.dmm @@ -10015,7 +10015,7 @@ /obj/item/radio/radio_mic{ pixel_y = 7 }, -/obj/item/device/cassette_tape/friday{ +/obj/item/cassette_tape/friday{ pixel_y = -6; pixel_x = -8 }, diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 2409801b5e34..14a15eb92d27 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -164,6 +164,7 @@ #define INIT_ORDER_OUTPUTS 35 #define INIT_ORDER_RESTAURANT 34 #define INIT_ORDER_POLLUTION 32 +#define INIT_ORDER_CASSETTES 31 // monkestation addition: cassettes initialize before atoms, so that cassette stuff can be used in Initialize() #define INIT_ORDER_ATOMS 30 #define INIT_ORDER_ARMAMENTS 27 #define INIT_ORDER_LANGUAGE 25 diff --git a/code/__DEFINES/~monkestation/cassettes.dm b/code/__DEFINES/~monkestation/cassettes.dm new file mode 100644 index 000000000000..e4ec6a1cdcb2 --- /dev/null +++ b/code/__DEFINES/~monkestation/cassettes.dm @@ -0,0 +1,13 @@ +/// Path to the base directory for cassette stuff +#define CASSETTE_BASE_DIR "data/cassette_storage/" +/// Path to the file containing a list of cassette IDs. +#define CASSETTE_ID_FILE (CASSETTE_BASE_DIR + "ids.json") +/// Path to the data for the cassette of the given ID. +#define CASSETTE_FILE(id) (CASSETTE_BASE_DIR + "[id].json") + +/// This cassette is unapproved, and has not been submitted for review. +#define CASSETTE_STATUS_UNAPPROVED 0 +/// This cassette is under review. +#define CASSETTE_STATUS_REVIEWING 1 +/// This cassette has been approved. +#define CASSETTE_STATUS_APPROVED 2 diff --git a/code/__HELPERS/~monkestation-helpers/text.dm b/code/__HELPERS/~monkestation-helpers/text.dm new file mode 100644 index 000000000000..cb6383f2e031 --- /dev/null +++ b/code/__HELPERS/~monkestation-helpers/text.dm @@ -0,0 +1,6 @@ +/// Checks to see if a string starts with http:// or https:// +/proc/is_http_protocol(text) + var/static/regex/http_regex + if(isnull(http_regex)) + http_regex = new("^https?://") + return findtext(text, http_regex) diff --git a/code/_globalvars/_regexes.dm b/code/_globalvars/_regexes.dm index 7297d509a918..e850889a42b7 100644 --- a/code/_globalvars/_regexes.dm +++ b/code/_globalvars/_regexes.dm @@ -1,7 +1,4 @@ //These are a bunch of regex datums for use /((any|every|no|some|head|foot)where(wolf)?\sand\s)+(\.[\.\s]+\s?where\?)?/i -GLOBAL_DATUM_INIT(is_http_protocol, /regex, regex("^https?://")) -GLOBAL_DATUM_INIT(is_http_protocol_non_secure, /regex, regex("^http?://")) - GLOBAL_DATUM_INIT(is_website, /regex, regex("http|www.|\[a-z0-9_-]+.(com|org|net|mil|edu)+", "i")) GLOBAL_DATUM_INIT(is_email, /regex, regex("\[a-z0-9_-]+@\[a-z0-9_-]+.\[a-z0-9_-]+", "i")) diff --git a/code/controllers/configuration/entries/monkestation.dm b/code/controllers/configuration/entries/monkestation.dm index c49237131fec..342063dd62a9 100644 --- a/code/controllers/configuration/entries/monkestation.dm +++ b/code/controllers/configuration/entries/monkestation.dm @@ -62,3 +62,5 @@ . = ..() if(.) config_entry_value *= 600 // documented as minutes + +/datum/config_entry/flag/cassettes_in_db diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index 784963f00d13..15e12625c8a9 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -150,7 +150,7 @@ message_admins("[key_name(user)] stopped web sounds.") web_sound_url = null stop_web_sounds = TRUE - if(web_sound_url && !findtext(web_sound_url, GLOB.is_http_protocol)) + if(web_sound_url && !is_http_protocol(web_sound_url)) tgui_alert(user, "The media provider returned a content URL that isn't using the HTTP or HTTPS protocol. This is a security risk and the sound will not be played.", "Security Risk", list("OK")) to_chat(user, span_boldwarning("BLOCKED: Content URL not using HTTP(S) Protocol!"), confidential = TRUE) @@ -183,7 +183,7 @@ if(length(web_sound_input)) web_sound_input = trim(web_sound_input) - if(findtext(web_sound_input, ":") && !findtext(web_sound_input, GLOB.is_http_protocol)) + if(findtext(web_sound_input, ":") && !is_http_protocol(web_sound_input)) to_chat(src, span_boldwarning("Non-http(s) URIs are not allowed."), confidential = TRUE) to_chat(src, span_warning("For youtube-dl shortcuts like ytsearch: please use the appropriate full URL from the website."), confidential = TRUE) return diff --git a/code/modules/requests/request_manager.dm b/code/modules/requests/request_manager.dm index eb16a38fc3bc..408889b86eef 100644 --- a/code/modules/requests/request_manager.dm +++ b/code/modules/requests/request_manager.dm @@ -245,7 +245,7 @@ GLOBAL_DATUM_INIT(requests, /datum/request_manager, new) if(request.req_type != REQUEST_INTERNET_SOUND) to_chat(usr, "Request doesn't have a sound to play.", confidential = TRUE) return TRUE - if(findtext(request.message, ":") && !findtext(request.message, GLOB.is_http_protocol)) + if(findtext(request.message, ":") && !is_http_protocol(request.message)) to_chat(usr, "Request is not a valid URL.", confidential = TRUE) return TRUE diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index e3446e24618a..1cc258a6efa0 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -326,8 +326,8 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) ///we generate mobs in these and create destroy does this in null space ignore += typesof(/obj/item/loot_table_maker) ///we need to use json_decode to run randoms properly - ignore += typesof(/obj/item/device/cassette_tape) - ignore += typesof(/datum/cassette/cassette_tape) + ignore += typesof(/obj/item/cassette_tape) + ignore += typesof(/datum/cassette) ///we also dont want weathers or weather events as they will hold refs to alot of stuff as they shouldn't be deleted ignore += typesof(/datum/weather_event) ignore += typesof(/datum/particle_weather) diff --git a/config/config.txt b/config/config.txt index 464b95aa5078..46f90548fe35 100644 --- a/config/config.txt +++ b/config/config.txt @@ -566,3 +566,5 @@ CONFIG_ERRORS_RUNTIME ## The age in days if minimum account age is on #MINIMUM_AGE +## If enabled, cassette tapes will be stored in the database, rather than JSON files on-disk. +#CASSETTES_IN_DB diff --git a/monkestation/code/modules/admin/verbs/spawn_mixtape.dm b/monkestation/code/modules/admin/verbs/spawn_mixtape.dm index bffca51317a0..026026c7e422 100644 --- a/monkestation/code/modules/admin/verbs/spawn_mixtape.dm +++ b/monkestation/code/modules/admin/verbs/spawn_mixtape.dm @@ -52,6 +52,6 @@ switch(action) if("spawn") if (params["id"]) - new/obj/item/device/cassette_tape(usr.loc, params["id"]) + new/obj/item/cassette_tape(usr.loc, params["id"]) SSblackbox.record_feedback("tally", "admin_verb", 1, "Spawn Mixtape") log_admin("[key_name(usr)] created mixtape [params["id"]] at [usr.loc].") diff --git a/monkestation/code/modules/cargo/crates/goodies.dm b/monkestation/code/modules/cargo/crates/goodies.dm index 79ed63cfb20d..d199df9b2f9f 100644 --- a/monkestation/code/modules/cargo/crates/goodies.dm +++ b/monkestation/code/modules/cargo/crates/goodies.dm @@ -8,10 +8,10 @@ name = "Cassette Mini-Pack" desc = "Alright, we'll admit it, 10 cassettes are too much for the majority of our users. Contains 3 Approved Cassettes." cost = PAYCHECK_CREW * 5 - contains = list(/obj/item/device/cassette_tape/random = 3) + contains = list(/obj/item/cassette_tape/random = 3) /datum/supply_pack/goody/blankcassette name = "Blank Cassette Mini-Pack" desc = "NO! We wont admit defeat! You will march yourself down to the Service section and purchase the 10 Blank Cassette pack instead of this Weak 3 Blank Cassette Pack!" cost = PAYCHECK_CREW * 3 - contains = list(/obj/item/device/cassette_tape/blank = 3) + contains = list(/obj/item/cassette_tape/blank = 3) diff --git a/monkestation/code/modules/cargo/crates/service.dm b/monkestation/code/modules/cargo/crates/service.dm index 3b0c69c0efe9..fe7fbcd48ba3 100644 --- a/monkestation/code/modules/cargo/crates/service.dm +++ b/monkestation/code/modules/cargo/crates/service.dm @@ -45,13 +45,13 @@ /datum/supply_pack/service/cassettes/fill(obj/structure/closet/crate/our_crate) for(var/id in unique_random_tapes(10)) - new /obj/item/device/cassette_tape(our_crate, id) + new /obj/item/cassette_tape(our_crate, id) /datum/supply_pack/service/blankcassettes name = "Blank Cassettes Crate" desc = "in the VERY unlikely event you have run out of blank cassettes, you can get 10 blank ones here. Contains 10 blank cassettes for use in Walkmans." cost = CARGO_CRATE_VALUE * 2 - contains = list(/obj/item/device/cassette_tape/blank = 10) + contains = list(/obj/item/cassette_tape/blank = 10) crate_name = "cassette crate" /datum/supply_pack/service/walkmen diff --git a/monkestation/code/modules/cassettes/cassette.dm b/monkestation/code/modules/cassettes/cassette.dm index 9fdfe816cb7e..3fc36c21f793 100644 --- a/monkestation/code/modules/cassettes/cassette.dm +++ b/monkestation/code/modules/cassettes/cassette.dm @@ -1,5 +1,5 @@ -/obj/item/device/cassette_tape +/obj/item/cassette_tape name = "Debug Cassette Tape" desc = "You shouldn't be seeing this!" icon = 'monkestation/code/modules/cassettes/icons/walkman.dmi' @@ -28,7 +28,7 @@ var/random = FALSE var/cassette_desc_string = "Generic Desc" -/obj/item/device/cassette_tape/Initialize(mapload, spawned_id) +/obj/item/cassette_tape/Initialize(mapload, spawned_id) . = ..() if(!length(GLOB.approved_ids)) GLOB.approved_ids = initialize_approved_ids() @@ -57,13 +57,13 @@ update_appearance() -/obj/item/device/cassette_tape/attack_self(mob/user) +/obj/item/cassette_tape/attack_self(mob/user) ..() icon_state = flipped ? side1_icon : side2_icon flipped = !flipped to_chat(user, span_notice("You flip [src].")) -/obj/item/device/cassette_tape/update_desc(updates) +/obj/item/cassette_tape/update_desc(updates) . = ..() desc = cassette_desc_string desc += "\n" @@ -72,7 +72,7 @@ if(author_name) desc += span_notice("Mixed by [author_name]\n") -/obj/item/device/cassette_tape/attackby(obj/item/item, mob/living/user) +/obj/item/cassette_tape/attackby(obj/item/item, mob/living/user) if(!istype(item, /obj/item/pen)) return ..() var/choice = tgui_input_list(usr, "What would you like to change?", items = list("Cassette Name", "Cassette Description", "Cancel")) @@ -106,24 +106,8 @@ else return -/datum/cassette/cassette_tape - var/name = "Broken Cassette" - var/desc = "You shouldn't be seeing this! Make an issue about it" - var/icon_state = "cassette_flip" - var/side1_icon = "cassette_flip" - var/side2_icon = "cassette_flip" - var/id = "blank" - var/creator_ckey = "Dwasint" - var/creator_name = "Collects-The-Candy" - var/approved = TRUE - var/list/song_names = list("side1" = list(), - "side2" = list()) - - var/list/songs = list("side1" = list(), - "side2" = list()) - -/obj/item/device/cassette_tape/blank +/obj/item/cassette_tape/blank id = "blank" -/obj/item/device/cassette_tape/friday +/obj/item/cassette_tape/friday id = "friday" diff --git a/monkestation/code/modules/cassettes/cassette_approval.dm b/monkestation/code/modules/cassettes/cassette_approval.dm index dce781859162..dfa2d3f4857c 100644 --- a/monkestation/code/modules/cassettes/cassette_approval.dm +++ b/monkestation/code/modules/cassettes/cassette_approval.dm @@ -1,7 +1,7 @@ GLOBAL_LIST_INIT(cassette_reviews, list()) #define ADMIN_OPEN_REVIEW(id) "(Open Review)" -/proc/submit_cassette_for_review(obj/item/device/cassette_tape/submitted, mob/user) +/proc/submit_cassette_for_review(obj/item/cassette_tape/submitted, mob/user) if(!user.client) return var/datum/cassette_review/new_review = new @@ -25,7 +25,7 @@ GLOBAL_LIST_INIT(cassette_reviews, list()) has requested a review on their cassette."))]") to_chat(user, span_notice("Your Cassette has been sent to the Space Board of Music for review, you will be notified when an outcome has been made.")) -/obj/item/device/cassette_tape/proc/generate_cassette_json() +/obj/item/cassette_tape/proc/generate_cassette_json() if(approved_tape) return if(!length(GLOB.approved_ids)) @@ -70,7 +70,7 @@ GLOBAL_LIST_INIT(cassette_reviews, list()) "song_url" = list() ) ) - var/obj/item/device/cassette_tape/submitted_tape + var/obj/item/cassette_tape/submitted_tape var/action_taken = FALSE diff --git a/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm b/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm index 07efe51ea95e..3671e96c50c1 100644 --- a/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm +++ b/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm @@ -1,61 +1,182 @@ -/datum/cassette_data - var/cassette_name - var/cassette_author - var/cassette_desc - var/cassette_author_ckey - - var/cassette_design_front - var/cassette_design_back - - var/list/songs - - var/list/song_names - - var/cassette_id - var/approved - var/file_name - - -/datum/cassette_data/proc/populate_data(file_id) - var/file = file("data/cassette_storage/[file_id].json") - if(!fexists(file)) +/datum/cassette + /// The unique ID of the cassette. + var/id + /// The name of the cassette. + var/name + /// The description of the cassette. + var/desc + /// The status of this cassette. + var/status = CASSETTE_STATUS_UNAPPROVED + /// Information about the author of this cassette. + var/datum/cassette_author/author + + /// The front side of the cassette. + var/datum/cassette_side/front + /// The back side of the cassette. + var/datum/cassette_side/back + +/datum/cassette/New() + . = ..() + author = new + front = new + back = new + +/datum/cassette/Destroy(force) + QDEL_NULL(author) + QDEL_NULL(front) + QDEL_NULL(back) + return ..() + +/// Imports cassette date from the old format. +/datum/cassette/proc/import_old_format(list/data) + name = data["name"] + desc = data["desc"] + if("status" in data) + status = data["status"] + else + status = data["approved"] ? CASSETTE_STATUS_APPROVED : CASSETTE_STATUS_UNAPPROVED + + author.name = data["author_name"] + author.ckey = ckey(data["author_ckey"]) + + for(var/i in 1 to 2) + var/datum/cassette_side/side = get_side(i % 2) // side2 = 0, side1 = 1 + var/side_name = "side[i]" + var/list/song_urls = data["songs"][side_name] + var/list/song_names = data["song_names"][side_name] + if(length(song_urls) != length(song_names)) + stack_trace("amount of song urls for [side_name] ([length(song_urls)]) did not match amount of song names for [side_name] ([length(song_names)])") + continue + side.design = data["[side_name]_icon"] + for(var/idx in 1 to length(song_urls)) + side.songs += new /datum/cassette_song(song_names[idx], song_urls[idx]) + +/// Exports cassette date in the old format. +/datum/cassette/proc/export_old_format() as /list + RETURN_TYPE(/list) + . = list( + "name" = name, + "desc" = desc, + "side1_icon" = /datum/cassette_side::design, + "side2_icon" = /datum/cassette_side::design, + "author_name" = author.name, + "author_ckey" = ckey(author.ckey), + "approved" = status == CASSETTE_STATUS_APPROVED, + "status" = status, + "songs" = list( + "side1" = list(), + "side2" = list(), + ), + "song_names" = list( + "side1" = list(), + "side2" = list(), + ), + ) + for(var/i in 1 to 2) + var/datum/cassette_side/side = get_side(i % 2) // side2 = 0, side1 = 1 + var/side_name = "side[i]" + var/list/names = list() + var/list/urls = list() + .["[side_name]_icon"] = side.design + for(var/datum/cassette_song/song as anything in side.songs) + names += song.name + urls += song.url + .["song_names"][side_name] = names + .["songs"][side_name] = urls + +/// Saves the cassette to the data folder, in JSON format. +/datum/cassette/proc/save_to_file() + if(!id) + CRASH("Attempted to save cassette without an ID to disk") + rustg_file_write(json_encode(export_old_format(), JSON_PRETTY_PRINT), CASSETTE_FILE(id)) + +/// Saves the cassette to the database. +/// Returns TRUE if successful, FALSE otherwise. +/datum/cassette/proc/save_to_db() + if(!id) + CRASH("Attempted to save cassette without an ID to database") + if(!SSdbcore.Connect()) + CRASH("Could not save cassette [id], database not connected") + var/datum/db_query/query_save_cassette = SSdbcore.NewQuery({" + INSERT INTO [format_table_name("cassettes")] + (id, name, desc, status, author_name, author_ckey, front, back) + VALUES + (:id, :name, :desc, :status, :author_name, :author_ckey, :front, :back) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + desc = VALUES(desc), + status = VALUES(status), + author_name = VALUES(author_name), + author_ckey = VALUES(author_ckey), + front = VALUES(front), + back = VALUES(back) + "}, list( + "id" = id, + "name" = name, + "desc" = desc, + "status" = status, + "author_name" = author.name, + "author_name" = ckey(author.ckey), + "front" = json_encode(front.export_for_db()), + "back" = json_encode(back.export_for_db()), + )) + if(!query_save_cassette.warn_execute()) + qdel(query_save_cassette) + stack_trace("Failed to save cassette [id] to database") return FALSE - var/list/data = json_decode(file2text(file)) - - cassette_name = data["name"] - cassette_desc = data["desc"] - - cassette_design_front = data["side1_icon"] - cassette_design_back = data["side2_icon"] - - songs = data["songs"] - - song_names = data["song_names"] - - cassette_author = data["author_name"] - cassette_author_ckey = data["author_ckey"] - - cassette_id = file_id - - approved = data["approved"] - - file_name = "data/cassette_storage/[file_id].json" - + qdel(query_save_cassette) return TRUE -/datum/cassette_data/proc/generate_cassette(turf/location) - if(!location) - return - var/obj/item/device/cassette_tape/new_tape = new(location) - new_tape.name = cassette_name - new_tape.cassette_desc_string = cassette_desc - new_tape.icon_state = cassette_design_front - new_tape.side1_icon = cassette_design_front - new_tape.side2_icon = cassette_design_back - new_tape.songs = songs - new_tape.song_names = song_names - new_tape.author_name = cassette_author - new_tape.ckey_author = cassette_author_ckey - new_tape.approved_tape = approved - - new_tape.update_appearance() + +/// Simple helper to get a side of the cassette. +/// TRUE is front side, FALSE is back side. +/datum/cassette/proc/get_side(front_side = TRUE) as /datum/cassette_side + RETURN_TYPE(/datum/cassette_side) + return front_side ? front : back + +/// Returns a list of all the song names in this cassette. +/// Really only useful for searching for cassettes via contained song names. +/datum/cassette/proc/list_song_names() as /list + RETURN_TYPE(/list) + . = list() + for(var/datum/cassette_song/song as anything in front.songs + back.songs) + . |= song.name + +/datum/cassette_author + /// The character name of the cassette author. + var/name + /// The ckey of the cassette author. + var/ckey + +/datum/cassette_side + /// The design of this side of the cassette. + var/design = "cassette_flip" + /// The songs on this side of the cassette. + var/list/datum/cassette_song/songs = list() + +/// Imports data for this cassette side to the JSON format used by the database. +/datum/cassette_side/proc/import_from_db(list/data) + design = data["design"] + for(var/list/song as anything in data["songs"]) + songs += new /datum/cassette_song(song["name"], song["url"]) + +/// Exports data from this cassette side in the JSON format used by the database. +/datum/cassette_side/proc/export_for_db() + . = list("design" = design, "songs" = list()) + for(var/datum/cassette_song/song as anything in songs) + .["songs"] += list(list("name" = song.name, "url" = song.url)) + +/datum/cassette_side/Destroy(force) + QDEL_LIST(songs) + return ..() + +/datum/cassette_song + /// The name of the song. + var/name + /// The URL of the song. + var/url + +/datum/cassette_song/New(name, url) + . = ..() + src.name = name + src.url = url diff --git a/monkestation/code/modules/cassettes/cassette_db/cassette_manager.dm b/monkestation/code/modules/cassettes/cassette_db/cassette_manager.dm new file mode 100644 index 000000000000..0f8c26c6f58c --- /dev/null +++ b/monkestation/code/modules/cassettes/cassette_db/cassette_manager.dm @@ -0,0 +1,190 @@ +SUBSYSTEM_DEF(cassettes) + name = "Cassetes" + init_order = INIT_ORDER_CASSETTES + flags = SS_NO_FIRE + /// An associative list of IDs to cassette data. + var/list/datum/cassette/cassettes = list() + +/datum/controller/subsystem/cassettes/Initialize() + . = SS_INIT_FAILURE + if(CONFIG_GET(flag/cassettes_in_db) && !CONFIG_GET(flag/sql_enabled)) + stack_trace("CASSETTES_IN_DB was enabled, despite the SQL database not being enabled! Disabling CASSETTES_IN_DB.") + CONFIG_SET(flag/cassettes_in_db, FALSE) + if(CONFIG_GET(flag/cassettes_in_db)) + if(!SSdbcore.Connect()) + CRASH("Database-based cassettes are enabled, but a connection to the database could not be established!") + if(!load_all_cassettes_from_db()) + CRASH("Failed to load all cassettes from database!") + else + if(!load_all_cassettes_from_json()) + CRASH("Failed to load all cassettes from data folder!") + return SS_INIT_SUCCESS + +/datum/controller/subsystem/cassettes/Recover() + flags |= SS_NO_INIT + cassettes = SScassettes.cassettes + +/// Loads the cassette with the given ID. +/// If `db` is TRUE, it will load the cassette from the database. +/// If `db` is FALSE, the cassette will be loaded from a JSON in the `data/cassette_storage` folder. +/// If `db` is null (the default), it will load from the database if the `CASSETTES_IN_DB` config option is set, otherwise it will load from the JSON files. +/datum/controller/subsystem/cassettes/proc/load_cassette(id, db) as /datum/cassette + RETURN_TYPE(/datum/cassette) + if(!id) + return null + if(id in cassettes) + return cassettes[id] + if(isnull(db)) + db = CONFIG_GET(flag/cassettes_in_db) + var/datum/cassette/cassette_data = db ? load_cassette_from_db_raw(id) : load_cassette_from_json_raw(id) + if(cassette_data) + cassettes[id] = cassette_data + return cassette_data + +/// Loads the cassette with the given ID from a JSON in the `data/cassette_storage` folder. +/// This does not check the SScassettes.cassettes cache, and you should not use this - this is only used to initialize SScassettes.cassettes +/datum/controller/subsystem/cassettes/proc/load_cassette_from_json_raw(id) as /datum/cassette + RETURN_TYPE(/datum/cassette) + var/cassette_file = CASSETTE_FILE(id) + if(!rustg_file_exists(cassette_file)) + return null + var/cassette_file_data = rustg_file_read(cassette_file) + if(!rustg_json_is_valid(cassette_file_data)) + CRASH("Cassette file [cassette_file] had invalid JSON!") + var/list/cassette_json = json_decode(cassette_file_data) + var/datum/cassette/cassette_data = new + cassette_data.import_old_format(cassette_json) + cassette_data.id = id + return cassette_data + +/// Loads the cassette with the given ID from the database. +/datum/controller/subsystem/cassettes/proc/load_cassette_from_db_raw(id) as /datum/cassette + RETURN_TYPE(/datum/cassette) + if(!SSdbcore.Connect() || !id) + return + var/datum/db_query/query_cassette = SSdbcore.NewQuery("SELECT name, desc, status, author_name, author_ckey, front, back FROM [format_table_name("cassettes")] WHERE id = :id", list("id" = id)) + if(!query_cassette.Execute() || !query_cassette.NextRow()) + qdel(query_cassette) + return + var/name = query_cassette.item[1] + var/desc = query_cassette.item[2] + var/status = query_cassette.item[3] + var/author_name = query_cassette.item[4] + var/author_ckey = query_cassette.item[5] + var/list/front = json_decode(query_cassette.item[6]) + var/list/back = json_decode(query_cassette.item[7]) + qdel(query_cassette) + + var/datum/cassette/cassette = new + cassette.id = id + cassette.name = name + cassette.desc = desc + cassette.status = status + cassette.author.name = author_name + cassette.author.ckey = author_ckey + cassette.front.import_from_db(front) + cassette.back.import_from_db(back) + return cassette + +/// Returns an associative list of id to cassette datums, of all existing saved cassettes. +/// This uses the database. +/datum/controller/subsystem/cassettes/proc/load_all_cassettes_from_db() + . = FALSE + if(!SSdbcore.Connect()) + CRASH("Failed to connect to database") + var/datum/db_query/query_cassettes = SSdbcore.NewQuery("SELECT id, name, desc, status, author_name, author_ckey, front, back FROM [format_table_name("cassettes")]") + if(!query_cassettes.Execute()) + qdel(query_cassettes) + CRASH("Failed to load cassettes from database") + while(query_cassettes.NextRow()) + var/id = query_cassettes.item[1] + var/name = query_cassettes.item[2] + var/desc = query_cassettes.item[3] + var/status = query_cassettes.item[4] + var/author_name = query_cassettes.item[5] + var/author_ckey = query_cassettes.item[6] + var/list/front = json_decode(query_cassettes.item[7]) + var/list/back = json_decode(query_cassettes.item[8]) + + var/datum/cassette/cassette = new + cassette.id = id + cassette.name = name + cassette.desc = desc + cassette.status = status + cassette.author.name = author_name + cassette.author.ckey = author_ckey + cassette.front.import_from_db(front) + cassette.back.import_from_db(back) + + cassettes[id] = cassette + qdel(query_cassettes) + return TRUE + +/// Returns an associative list of id to cassette datums, of all existing saved cassettes. +/// This uses JSON files. +/datum/controller/subsystem/cassettes/proc/load_all_cassettes_from_json() + . = FALSE + if(!rustg_file_exists(CASSETTE_ID_FILE)) // this just means there's no cassettes at all i guess? which is valid. + return TRUE + var/list/ids = json_decode(rustg_file_read(CASSETTE_ID_FILE)) + for(var/id in ids) + if(!ids) + continue + var/datum/cassette/cassette_data = load_cassette_from_json_raw(id) + if(isnull(cassette_data)) + stack_trace("Failed to load cassette [id]") + continue + cassettes[id] = cassette_data + return TRUE + +/// Updates the ids.json file on-disk. +/datum/controller/subsystem/cassettes/proc/save_ids_json() + var/list/ids = list() + if(rustg_file_exists(CASSETTE_ID_FILE)) + // Verify that each cassette ID still exists and is still considered "approved" before adding them to the list. + for(var/id in json_decode(rustg_file_read(CASSETTE_ID_FILE))) + if(!rustg_file_exists(CASSETTE_FILE(id))) + continue + ids += id + for(var/id in cassettes) + var/datum/cassette/cassette = cassettes[id] + if(cassette.status == CASSETTE_STATUS_UNAPPROVED) + ids -= id + else + ids |= id + rustg_file_write(ids, CASSETTE_ID_FILE) + +/// Gets all the cassettes authored by the given ckey. +/datum/controller/subsystem/cassettes/proc/get_cassettes_by_ckey(user_ckey) as /list + RETURN_TYPE(/list/datum/cassette) + . = list() + user_ckey = ckey(user_ckey) + for(var/id in cassettes) + var/datum/cassette/cassette = cassettes[id] + if(cassette.author.ckey == user_ckey) + . += cassette + +/datum/controller/subsystem/cassettes/proc/migrate_json_cassettes_to_db() + if(!SSdbcore.Connect()) + CRASH("Cannot migrate JSON cassettes to the database if we can't even connect to the database!") + var/list/old_cassettes = cassettes.Copy() + cassettes.Cut() + if(!load_all_cassettes_from_json()) + cassettes = old_cassettes + CRASH("Failed to load cassettes from JSON") + var/list/sql_cassettes = list() + for(var/id in cassettes) + var/datum/cassette/cassette = cassettes[id] + sql_cassettes += list(list( + "id" = id, + "name" = cassette.name, + "desc" = cassette.desc, + "status" = cassette.status, + "author_name" = cassette.author.name, + "author_ckey" = ckey(cassette.author.ckey), + "front" = cassette.front.export_for_db(), + "back" = cassette.back.export_for_db(), + )) + if(!length(sql_cassettes)) + return + SSdbcore.MassInsert(format_table_name("cassettes"), sql_cassettes, duplicate_key = TRUE, warn = TRUE) diff --git a/monkestation/code/modules/cassettes/cassette_db/subsystem.dm b/monkestation/code/modules/cassettes/cassette_db/subsystem.dm deleted file mode 100644 index bbca8076a20c..000000000000 --- a/monkestation/code/modules/cassettes/cassette_db/subsystem.dm +++ /dev/null @@ -1,30 +0,0 @@ -SUBSYSTEM_DEF(cassette_storage) - name = "Cassette Storage" - flags = SS_NO_FIRE - runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT - var/list/cassette_datums = list() - - -/datum/controller/subsystem/cassette_storage/Initialize() - if(!length(GLOB.approved_ids)) - GLOB.approved_ids = initialize_approved_ids() - generate_cassette_datums() - return SS_INIT_SUCCESS - -/datum/controller/subsystem/cassette_storage/proc/generate_cassette_datums() - for(var/id in GLOB.approved_ids) - var/datum/cassette_data/new_data = new - if(!new_data.populate_data(id)) - qdel(new_data) - continue - cassette_datums += new_data - -/datum/controller/subsystem/cassette_storage/proc/get_cassettes_by_ckey(user_ckey) as /list - RETURN_TYPE(/list) - . = list() - if(!user_ckey) - return - user_ckey = ckey(user_ckey) - for(var/datum/cassette_data/tape as anything in SScassette_storage.cassette_datums) - if(ckey(tape.cassette_author_ckey) == user_ckey) - . += tape diff --git a/monkestation/code/modules/cassettes/machines/cassette_rack.dm b/monkestation/code/modules/cassettes/machines/cassette_rack.dm index 9abbeabb0371..3925273a2a06 100644 --- a/monkestation/code/modules/cassettes/machines/cassette_rack.dm +++ b/monkestation/code/modules/cassettes/machines/cassette_rack.dm @@ -29,7 +29,7 @@ /datum/storage/cassette_rack/New() . = ..() - set_holdable(/obj/item/device/cassette_tape) + set_holdable(/obj/item/cassette_tape) // Allow opening on a normal left click /datum/storage/cassette_rack/on_attack(datum/source, mob/user) @@ -50,9 +50,9 @@ REGISTER_REQUIRED_MAP_ITEM(1, INFINITY) RegisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED, PROC_REF(spawn_curator_tapes)) for(var/i in 1 to spawn_blanks) - new /obj/item/device/cassette_tape/blank(src) + new /obj/item/cassette_tape/blank(src) for(var/id in unique_random_tapes(spawn_random)) - new /obj/item/device/cassette_tape(src, id) + new /obj/item/cassette_tape(src, id) update_appearance() /obj/structure/cassette_rack/prefilled/Destroy() @@ -68,20 +68,18 @@ add_user_tapes(new_crewmember.ckey) /obj/structure/cassette_rack/prefilled/proc/add_user_tapes(user_ckey, max_amt = 3, expand_max_size = TRUE) - var/list/user_tapes = SScassette_storage.get_cassettes_by_ckey(user_ckey) - if(!length(user_tapes)) + var/list/user_cassettes = SScassettes.get_cassettes_by_ckey(user_ckey) + if(!length(user_cassettes)) return FALSE - var/list/existing_tapes = list() - for(var/obj/item/device/cassette_tape/tape in src) + var/list/existing_cassettes = list() + for(var/obj/item/cassette_tape/tape in src) if(tape.id) - existing_tapes[tape.id] = TRUE - for(var/iter in 1 to max_amt) - if(!length(user_tapes)) - break - var/datum/cassette_data/tape = pick_n_take(user_tapes) - if(existing_tapes[tape.cassette_id]) + existing_cassettes[tape.id] = TRUE + for(var/iter in 1 to min(max_amt, length(user_cassettes))) + var/datum/cassette/cassette = pick_n_take(user_cassettes) + if(existing_cassettes[cassette.id]) continue - new /obj/item/device/cassette_tape(src, tape.cassette_id) + new /obj/item/cassette_tape(src, cassette.id) if(expand_max_size && !QDELETED(atom_storage)) atom_storage.max_slots += max_amt atom_storage.max_total_storage += max_amt * WEIGHT_CLASS_SMALL diff --git a/monkestation/code/modules/cassettes/machines/dj_station.dm b/monkestation/code/modules/cassettes/machines/dj_station.dm index 386de43c5f57..a7742247f810 100644 --- a/monkestation/code/modules/cassettes/machines/dj_station.dm +++ b/monkestation/code/modules/cassettes/machines/dj_station.dm @@ -1,6 +1,5 @@ GLOBAL_VAR(dj_broadcast) -GLOBAL_VAR(dj_booth) - +GLOBAL_DATUM(dj_booth, /obj/machinery/cassette/dj_station) /obj/item/clothing/ears //can we be used to listen to radio? @@ -8,42 +7,35 @@ GLOBAL_VAR(dj_booth) /obj/machinery/cassette/dj_station name = "Cassette Player" - desc = "Plays Space Music Board approved cassettes for anyone in the station to listen to " + desc = "Plays Space Music Board approved cassettes for anyone in the station to listen to." icon = 'monkestation/code/modules/cassettes/icons/radio_station.dmi' icon_state = "cassette_player" - active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION + use_power = NO_POWER_USE + + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + move_resist = MOVE_FORCE_OVERPOWERING - resistance_flags = INDESTRUCTIBLE anchored = TRUE density = TRUE - var/broadcasting = FALSE - var/obj/item/device/cassette_tape/inserted_tape - var/time_left = 0 - var/current_song_duration = 0 - var/list/people_with_signals = list() - var/list/active_listeners = list() - var/waiting_for_yield = FALSE - - //tape stuff goes here - var/pl_index = 0 - var/list/current_playlist = list() - var/list/current_namelist = list() + var/broadcasting = FALSE COOLDOWN_DECLARE(next_song_timer) /obj/machinery/cassette/dj_station/Initialize(mapload) . = ..() REGISTER_REQUIRED_MAP_ITEM(1, INFINITY) - GLOB.dj_booth = src register_context() + if(QDELETED(GLOB.dj_booth)) + GLOB.dj_booth = src /obj/machinery/cassette/dj_station/Destroy() - . = ..() - GLOB.dj_booth = null - STOP_PROCESSING(SSprocessing, src) + if(GLOB.dj_booth == src) + GLOB.dj_booth = null + return ..() +/* /obj/machinery/cassette/dj_station/add_context(atom/source, list/context, obj/item/held_item, mob/user) . = ..() if(inserted_tape) @@ -51,327 +43,4 @@ GLOBAL_VAR(dj_booth) if(!broadcasting) context[SCREENTIP_CONTEXT_LMB] = "Play Tape" return CONTEXTUAL_SCREENTIP_SET - -/obj/machinery/cassette/dj_station/examine(mob/user) - . = ..() - if(time_left > 0 || next_song_timer) - . += span_notice("It seems to be cooling down, you estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))].") - -/obj/machinery/cassette/dj_station/process(seconds_per_tick) - if(waiting_for_yield) - return - time_left -= round(seconds_per_tick) - if(time_left <= 0) - time_left = 0 - if(COOLDOWN_FINISHED(src, next_song_timer) && broadcasting) - COOLDOWN_START(src, next_song_timer, 10 MINUTES) - broadcasting = FALSE - -/obj/machinery/cassette/dj_station/attack_hand(mob/user) - . = ..() - if(!inserted_tape) - return - if((!COOLDOWN_FINISHED(src, next_song_timer)) && !broadcasting) - to_chat(user, span_notice("The [src] feels hot to the touch and needs time to cooldown.")) - to_chat(user, span_info("You estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))] to cool down.")) - return - message_admins("[src] started broadcasting [inserted_tape] interacted with by [user]") - logger.Log(LOG_CATEGORY_MUSIC, "[src] started broadcasting [inserted_tape]") - start_broadcast() - -/obj/machinery/cassette/dj_station/AltClick(mob/user) - . = ..() - if(!isliving(user) || !user.Adjacent(src)) - return - if(!inserted_tape) - return - if(broadcasting) - next_song() - -/obj/machinery/cassette/dj_station/CtrlClick(mob/user) - . = ..() - if(!inserted_tape || broadcasting) - return - if(Adjacent(user) && !issiliconoradminghost(user)) - if(!user.put_in_hands(inserted_tape)) - inserted_tape.forceMove(drop_location()) - else - inserted_tape.forceMove(drop_location()) - inserted_tape = null - time_left = 0 - current_song_duration = 0 - pl_index = 0 - current_playlist = list() - current_namelist = list() - stop_broadcast(TRUE) - -/obj/machinery/cassette/dj_station/attackby(obj/item/weapon, mob/user, params) - if(!istype(weapon, /obj/item/device/cassette_tape)) - return - var/obj/item/device/cassette_tape/attacked = weapon - if(!attacked.approved_tape) - to_chat(user, span_warning("The [src] smartly rejects the bootleg cassette tape")) - return - if(!inserted_tape) - insert_tape(attacked) - else - if(!broadcasting) - if(Adjacent(user) && !issiliconoradminghost(user)) - if(!user.put_in_hands(inserted_tape)) - inserted_tape.forceMove(drop_location()) - else - inserted_tape.forceMove(drop_location()) - inserted_tape = null - time_left = 0 - current_song_duration = 0 - pl_index = 0 - current_playlist = list() - current_namelist = list() - insert_tape(attacked) - if(broadcasting) - stop_broadcast(TRUE) - -/obj/machinery/cassette/dj_station/proc/insert_tape(obj/item/device/cassette_tape/CTape) - if(inserted_tape || !istype(CTape)) - return - - inserted_tape = CTape - CTape.forceMove(src) - - update_appearance() - pl_index = 1 - if(inserted_tape.songs["side1"] && inserted_tape.songs["side2"]) - var/list/list = inserted_tape.songs["[inserted_tape.flipped ? "side2" : "side1"]"] - for(var/song in list) - current_playlist += song - - var/list/name_list = inserted_tape.song_names["[inserted_tape.flipped ? "side2" : "side1"]"] - for(var/song in name_list) - current_namelist += song - -/obj/machinery/cassette/dj_station/proc/stop_broadcast(soft = FALSE) - STOP_PROCESSING(SSprocessing, src) - GLOB.dj_broadcast = FALSE - broadcasting = FALSE - message_admins("[src] has stopped broadcasting [inserted_tape].") - logger.Log(LOG_CATEGORY_MUSIC, "[src] has stopped broadcasting [inserted_tape]") - for(var/client/anything as anything in active_listeners) - if(!istype(anything)) - continue - anything.tgui_panel?.stop_music() - GLOB.youtube_exempt["dj-station"] -= anything - active_listeners = list() - - if(!soft) - for(var/mob/living/carbon/anything as anything in people_with_signals) - if(!istype(anything)) - continue - UnregisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS) - UnregisterSignal(anything, COMSIG_CARBON_EQUIP_EARS) - UnregisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED) - people_with_signals = list() - -/obj/machinery/cassette/dj_station/proc/start_broadcast() - var/choice = tgui_input_list(usr, "Choose which song to play.", "[src]", current_namelist) - if(!choice) - return - var/list_index = current_namelist.Find(choice) - if(!list_index) - return - GLOB.dj_broadcast = TRUE - pl_index = list_index - - var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM, ZTRAIT_RESERVED)) - for(var/mob/person as anything in GLOB.player_list) - if(issilicon(person) || isobserver(person) || isaicamera(person) || isbot(person)) - active_listeners |= person.client - continue - if(iscarbon(person)) - var/mob/living/carbon/anything = person - if(!(anything in people_with_signals)) - if(!istype(anything)) - continue - - RegisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast)) - RegisterSignal(anything, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast)) - RegisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast)) - people_with_signals |= anything - - if(!(anything.client in active_listeners)) - if(!(anything.z in viable_z)) - continue - - if(!anything.client) - continue - - if(anything.client in GLOB.youtube_exempt["walkman"]) - continue - - var/obj/item/ear_slot = anything.get_item_by_slot(ITEM_SLOT_EARS) - if(istype(ear_slot, /obj/item/clothing/ears)) - var/obj/item/clothing/ears/worn - if(!worn || !worn?.radio_compat) - continue - else if(!istype(ear_slot, /obj/item/radio/headset)) - continue - - if(!anything.client.prefs?.read_preference(/datum/preference/toggle/hear_music)) - continue - - active_listeners |= anything.client - - if(!length(active_listeners)) - return - - start_playing(active_listeners) - START_PROCESSING(SSprocessing, src) - - -/obj/machinery/cassette/dj_station/proc/check_solo_broadcast(mob/living/carbon/source, obj/item/clothing/ears/ear_item) - SIGNAL_HANDLER - - if(!istype(source)) - return - - if(istype(ear_item, /obj/item/clothing/ears)) - var/obj/item/clothing/ears/worn - if(!worn || !worn?.radio_compat) - return - else if(!istype(ear_item, /obj/item/radio/headset)) - return - - var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM)) - if(!(source.z in viable_z) || !source.client) - return - - if(!source.client.prefs?.read_preference(/datum/preference/toggle/hear_music)) - return - - active_listeners |= source.client - GLOB.youtube_exempt["dj-station"] |= source.client - INVOKE_ASYNC(src, PROC_REF(start_playing),list(source.client)) - -/obj/machinery/cassette/dj_station/proc/stop_solo_broadcast(mob/living/carbon/source) - SIGNAL_HANDLER - - if(!source.client || !(source.client in active_listeners)) - return - - active_listeners -= source.client - GLOB.youtube_exempt["dj-station"] -= source.client - source.client.tgui_panel?.stop_music() - -/obj/machinery/cassette/dj_station/proc/start_playing(list/clients) - if(!inserted_tape) - if(broadcasting) - stop_broadcast(TRUE) - return - - waiting_for_yield = TRUE - if(findtext(current_playlist[pl_index], GLOB.is_http_protocol)) - ///invoking youtube-dl - var/ytdl = CONFIG_GET(string/invoke_youtubedl) - ///the input for ytdl handled by the song list - var/web_sound_input - ///the url for youtube-dl - var/web_sound_url = "" - ///all extra data from the youtube-dl really want the name - var/list/music_extra_data = list() - web_sound_input = trim(current_playlist[pl_index]) - if(!(web_sound_input in GLOB.parsed_audio)) - ///scrubbing the input before putting it in the shell - var/shell_scrubbed_input = shell_url_scrub(web_sound_input) - ///putting it in the shell - var/list/output = world.shelleo("[ytdl] --geo-bypass --format \"bestaudio\[ext=mp3]/best\[ext=mp4]\[height <= 360]/bestaudio\[ext=m4a]/bestaudio\[ext=aac]\" --dump-single-json --no-playlist --extractor-args \"youtube:lang=en\" -- \"[shell_scrubbed_input]\"") - ///any errors - var/errorlevel = output[SHELLEO_ERRORLEVEL] - ///the standard output - var/stdout = output[SHELLEO_STDOUT] - if(!errorlevel) - ///list for all the output data to go to - var/list/data - try - data = json_decode(stdout) - catch(var/exception/error) ///catch errors here - to_chat(src, "Youtube-dl JSON parsing FAILED:", confidential = TRUE) - to_chat(src, "[error]: [stdout]", confidential = TRUE) - return - - if (data["url"]) - web_sound_url = data["url"] - music_extra_data["start"] = data["start_time"] - music_extra_data["end"] = data["end_time"] - music_extra_data["link"] = data["webpage_url"] - music_extra_data["title"] = data["title"] - if(music_extra_data["start"]) - time_left = data["duration"] - music_extra_data["start"] - else - time_left = data["duration"] - - current_song_duration = data["duration"] - - GLOB.parsed_audio["[web_sound_input]"] = data - else - var/list/data = GLOB.parsed_audio["[web_sound_input]"] - web_sound_url = data["url"] - music_extra_data["start"] = data["start_time"] - music_extra_data["end"] = data["end_time"] - music_extra_data["link"] = data["webpage_url"] - music_extra_data["title"] = data["title"] - if(time_left <= 0) - if(music_extra_data["start"]) - time_left = data["duration"] - music_extra_data["start"] - else - time_left = data["duration"] - - current_song_duration = data["duration"] - music_extra_data["duration"] = data["duration"] - - if(time_left > 0) - music_extra_data["start"] = music_extra_data["duration"] - time_left - - for(var/client/anything as anything in clients) - if(!istype(anything)) - continue - anything.tgui_panel?.play_music(web_sound_url, music_extra_data) - GLOB.youtube_exempt["dj-station"] |= anything - broadcasting = TRUE - waiting_for_yield = FALSE - -/obj/machinery/cassette/dj_station/proc/add_new_player(mob/living/carbon/new_player) - if(!(new_player in people_with_signals)) - RegisterSignal(new_player, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast)) - RegisterSignal(new_player, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast)) - RegisterSignal(new_player, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast)) - people_with_signals |= new_player - - if(!broadcasting) - return - - var/obj/item/ear_slot = new_player.get_item_by_slot(ITEM_SLOT_EARS) - if(istype(ear_slot, /obj/item/clothing/ears)) - var/obj/item/clothing/ears/worn - if(!worn || !worn?.radio_compat) - return - else if(!istype(ear_slot, /obj/item/radio/headset)) - return - var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM)) - if(!(new_player.z in viable_z)) - return - - if(!(new_player.client in active_listeners)) - active_listeners |= new_player.client - start_playing(list(new_player.client)) - -/obj/machinery/cassette/dj_station/proc/next_song() - waiting_for_yield = TRUE - var/choice = tgui_input_number(usr, "Choose which song number to play.", "[src]", 1, length(current_playlist), 1) - if(!choice) - waiting_for_yield = FALSE - stop_broadcast() - return - GLOB.dj_broadcast = TRUE - pl_index = choice - - pl_index++ - start_playing(active_listeners) +*/ diff --git a/monkestation/code/modules/cassettes/machines/dj_station_old.dm b/monkestation/code/modules/cassettes/machines/dj_station_old.dm new file mode 100644 index 000000000000..f9b24f679fc8 --- /dev/null +++ b/monkestation/code/modules/cassettes/machines/dj_station_old.dm @@ -0,0 +1,377 @@ +GLOBAL_VAR(dj_broadcast) +GLOBAL_DATUM(dj_booth, /obj/machinery/cassette/dj_station) + +/obj/item/clothing/ears + //can we be used to listen to radio? + var/radio_compat = FALSE + +/obj/machinery/cassette/dj_station + name = "Cassette Player" + desc = "Plays Space Music Board approved cassettes for anyone in the station to listen to." + + icon = 'monkestation/code/modules/cassettes/icons/radio_station.dmi' + icon_state = "cassette_player" + + active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION + + resistance_flags = INDESTRUCTIBLE + anchored = TRUE + density = TRUE + var/broadcasting = FALSE + var/obj/item/cassette_tape/inserted_tape + var/time_left = 0 + var/current_song_duration = 0 + var/list/people_with_signals = list() + var/list/active_listeners = list() + var/waiting_for_yield = FALSE + + //tape stuff goes here + var/pl_index = 0 + var/list/current_playlist = list() + var/list/current_namelist = list() + + COOLDOWN_DECLARE(next_song_timer) + +/obj/machinery/cassette/dj_station/Initialize(mapload) + . = ..() + REGISTER_REQUIRED_MAP_ITEM(1, INFINITY) + register_context() + if(QDELETED(GLOB.dj_booth)) + GLOB.dj_booth = src + +/obj/machinery/cassette/dj_station/Destroy() + if(GLOB.dj_booth == src) + GLOB.dj_booth = null + return ..() + +/obj/machinery/cassette/dj_station/add_context(atom/source, list/context, obj/item/held_item, mob/user) + . = ..() + if(inserted_tape) + context[SCREENTIP_CONTEXT_CTRL_LMB] = "Eject Tape" + if(!broadcasting) + context[SCREENTIP_CONTEXT_LMB] = "Play Tape" + return CONTEXTUAL_SCREENTIP_SET + +/obj/machinery/cassette/dj_station/examine(mob/user) + . = ..() + if(time_left > 0 || next_song_timer) + . += span_notice("It seems to be cooling down, you estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))].") + +/obj/machinery/cassette/dj_station/process(seconds_per_tick) + if(waiting_for_yield) + return + time_left -= round(seconds_per_tick) + if(time_left <= 0) + time_left = 0 + if(COOLDOWN_FINISHED(src, next_song_timer) && broadcasting) + COOLDOWN_START(src, next_song_timer, 10 MINUTES) + broadcasting = FALSE + +/obj/machinery/cassette/dj_station/attack_hand(mob/user) + . = ..() + if(!inserted_tape) + return + if((!COOLDOWN_FINISHED(src, next_song_timer)) && !broadcasting) + to_chat(user, span_notice("The [src] feels hot to the touch and needs time to cooldown.")) + to_chat(user, span_info("You estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))] to cool down.")) + return + message_admins("[src] started broadcasting [inserted_tape] interacted with by [user]") + logger.Log(LOG_CATEGORY_MUSIC, "[src] started broadcasting [inserted_tape]") + start_broadcast() + +/obj/machinery/cassette/dj_station/AltClick(mob/user) + . = ..() + if(!isliving(user) || !user.Adjacent(src)) + return + if(!inserted_tape) + return + if(broadcasting) + next_song() + +/obj/machinery/cassette/dj_station/CtrlClick(mob/user) + . = ..() + if(!inserted_tape || broadcasting) + return + if(Adjacent(user) && !issiliconoradminghost(user)) + if(!user.put_in_hands(inserted_tape)) + inserted_tape.forceMove(drop_location()) + else + inserted_tape.forceMove(drop_location()) + inserted_tape = null + time_left = 0 + current_song_duration = 0 + pl_index = 0 + current_playlist = list() + current_namelist = list() + stop_broadcast(TRUE) + +/obj/machinery/cassette/dj_station/attackby(obj/item/weapon, mob/user, params) + if(!istype(weapon, /obj/item/cassette_tape)) + return + var/obj/item/cassette_tape/attacked = weapon + if(!attacked.approved_tape) + to_chat(user, span_warning("The [src] smartly rejects the bootleg cassette tape")) + return + if(!inserted_tape) + insert_tape(attacked) + else + if(!broadcasting) + if(Adjacent(user) && !issiliconoradminghost(user)) + if(!user.put_in_hands(inserted_tape)) + inserted_tape.forceMove(drop_location()) + else + inserted_tape.forceMove(drop_location()) + inserted_tape = null + time_left = 0 + current_song_duration = 0 + pl_index = 0 + current_playlist = list() + current_namelist = list() + insert_tape(attacked) + if(broadcasting) + stop_broadcast(TRUE) + +/obj/machinery/cassette/dj_station/proc/insert_tape(obj/item/cassette_tape/CTape) + if(inserted_tape || !istype(CTape)) + return + + inserted_tape = CTape + CTape.forceMove(src) + + update_appearance() + pl_index = 1 + if(inserted_tape.songs["side1"] && inserted_tape.songs["side2"]) + var/list/list = inserted_tape.songs["[inserted_tape.flipped ? "side2" : "side1"]"] + for(var/song in list) + current_playlist += song + + var/list/name_list = inserted_tape.song_names["[inserted_tape.flipped ? "side2" : "side1"]"] + for(var/song in name_list) + current_namelist += song + +/obj/machinery/cassette/dj_station/proc/stop_broadcast(soft = FALSE) + STOP_PROCESSING(SSprocessing, src) + GLOB.dj_broadcast = FALSE + broadcasting = FALSE + message_admins("[src] has stopped broadcasting [inserted_tape].") + logger.Log(LOG_CATEGORY_MUSIC, "[src] has stopped broadcasting [inserted_tape]") + for(var/client/anything as anything in active_listeners) + if(!istype(anything)) + continue + anything.tgui_panel?.stop_music() + GLOB.youtube_exempt["dj-station"] -= anything + active_listeners = list() + + if(!soft) + for(var/mob/living/carbon/anything as anything in people_with_signals) + if(!istype(anything)) + continue + UnregisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS) + UnregisterSignal(anything, COMSIG_CARBON_EQUIP_EARS) + UnregisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED) + people_with_signals = list() + +/obj/machinery/cassette/dj_station/proc/start_broadcast() + var/choice = tgui_input_list(usr, "Choose which song to play.", "[src]", current_namelist) + if(!choice) + return + var/list_index = current_namelist.Find(choice) + if(!list_index) + return + GLOB.dj_broadcast = TRUE + pl_index = list_index + + var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM, ZTRAIT_RESERVED)) + for(var/mob/person as anything in GLOB.player_list) + if(issilicon(person) || isobserver(person) || isaicamera(person) || isbot(person)) + active_listeners |= person.client + continue + if(iscarbon(person)) + var/mob/living/carbon/anything = person + if(!(anything in people_with_signals)) + if(!istype(anything)) + continue + + RegisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast)) + RegisterSignal(anything, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast)) + RegisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast)) + people_with_signals |= anything + + if(!(anything.client in active_listeners)) + if(!(anything.z in viable_z)) + continue + + if(!anything.client) + continue + + if(anything.client in GLOB.youtube_exempt["walkman"]) + continue + + var/obj/item/ear_slot = anything.get_item_by_slot(ITEM_SLOT_EARS) + if(istype(ear_slot, /obj/item/clothing/ears)) + var/obj/item/clothing/ears/worn + if(!worn || !worn?.radio_compat) + continue + else if(!istype(ear_slot, /obj/item/radio/headset)) + continue + + if(!anything.client.prefs?.read_preference(/datum/preference/toggle/hear_music)) + continue + + active_listeners |= anything.client + + if(!length(active_listeners)) + return + + start_playing(active_listeners) + START_PROCESSING(SSprocessing, src) + + +/obj/machinery/cassette/dj_station/proc/check_solo_broadcast(mob/living/carbon/source, obj/item/clothing/ears/ear_item) + SIGNAL_HANDLER + + if(!istype(source)) + return + + if(istype(ear_item, /obj/item/clothing/ears)) + var/obj/item/clothing/ears/worn + if(!worn || !worn?.radio_compat) + return + else if(!istype(ear_item, /obj/item/radio/headset)) + return + + var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM)) + if(!(source.z in viable_z) || !source.client) + return + + if(!source.client.prefs?.read_preference(/datum/preference/toggle/hear_music)) + return + + active_listeners |= source.client + GLOB.youtube_exempt["dj-station"] |= source.client + INVOKE_ASYNC(src, PROC_REF(start_playing),list(source.client)) + +/obj/machinery/cassette/dj_station/proc/stop_solo_broadcast(mob/living/carbon/source) + SIGNAL_HANDLER + + if(!source.client || !(source.client in active_listeners)) + return + + active_listeners -= source.client + GLOB.youtube_exempt["dj-station"] -= source.client + source.client.tgui_panel?.stop_music() + +/obj/machinery/cassette/dj_station/proc/start_playing(list/clients) + if(!inserted_tape) + if(broadcasting) + stop_broadcast(TRUE) + return + + waiting_for_yield = TRUE + if(is_http_protocol(current_playlist[pl_index])) + ///invoking youtube-dl + var/ytdl = CONFIG_GET(string/invoke_youtubedl) + ///the input for ytdl handled by the song list + var/web_sound_input + ///the url for youtube-dl + var/web_sound_url = "" + ///all extra data from the youtube-dl really want the name + var/list/music_extra_data = list() + web_sound_input = trim(current_playlist[pl_index]) + if(!(web_sound_input in GLOB.parsed_audio)) + ///scrubbing the input before putting it in the shell + var/shell_scrubbed_input = shell_url_scrub(web_sound_input) + ///putting it in the shell + var/list/output = world.shelleo("[ytdl] --geo-bypass --format \"bestaudio\[ext=mp3]/best\[ext=mp4]\[height <= 360]/bestaudio\[ext=m4a]/bestaudio\[ext=aac]\" --dump-single-json --no-playlist --extractor-args \"youtube:lang=en\" -- \"[shell_scrubbed_input]\"") + ///any errors + var/errorlevel = output[SHELLEO_ERRORLEVEL] + ///the standard output + var/stdout = output[SHELLEO_STDOUT] + if(!errorlevel) + ///list for all the output data to go to + var/list/data + try + data = json_decode(stdout) + catch(var/exception/error) ///catch errors here + to_chat(src, "Youtube-dl JSON parsing FAILED:", confidential = TRUE) + to_chat(src, "[error]: [stdout]", confidential = TRUE) + return + + if (data["url"]) + web_sound_url = data["url"] + music_extra_data["start"] = data["start_time"] + music_extra_data["end"] = data["end_time"] + music_extra_data["link"] = data["webpage_url"] + music_extra_data["title"] = data["title"] + if(music_extra_data["start"]) + time_left = data["duration"] - music_extra_data["start"] + else + time_left = data["duration"] + + current_song_duration = data["duration"] + + GLOB.parsed_audio["[web_sound_input]"] = data + else + var/list/data = GLOB.parsed_audio["[web_sound_input]"] + web_sound_url = data["url"] + music_extra_data["start"] = data["start_time"] + music_extra_data["end"] = data["end_time"] + music_extra_data["link"] = data["webpage_url"] + music_extra_data["title"] = data["title"] + if(time_left <= 0) + if(music_extra_data["start"]) + time_left = data["duration"] - music_extra_data["start"] + else + time_left = data["duration"] + + current_song_duration = data["duration"] + music_extra_data["duration"] = data["duration"] + + if(time_left > 0) + music_extra_data["start"] = music_extra_data["duration"] - time_left + + for(var/client/anything as anything in clients) + if(!istype(anything)) + continue + anything.tgui_panel?.play_music(web_sound_url, music_extra_data) + GLOB.youtube_exempt["dj-station"] |= anything + broadcasting = TRUE + waiting_for_yield = FALSE + +/obj/machinery/cassette/dj_station/proc/add_new_player(mob/living/carbon/new_player) + if(!(new_player in people_with_signals)) + RegisterSignal(new_player, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast)) + RegisterSignal(new_player, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast)) + RegisterSignal(new_player, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast)) + people_with_signals |= new_player + + if(!broadcasting) + return + + var/obj/item/ear_slot = new_player.get_item_by_slot(ITEM_SLOT_EARS) + if(istype(ear_slot, /obj/item/clothing/ears)) + var/obj/item/clothing/ears/worn + if(!worn || !worn?.radio_compat) + return + else if(!istype(ear_slot, /obj/item/radio/headset)) + return + var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM)) + if(!(new_player.z in viable_z)) + return + + if(!(new_player.client in active_listeners)) + active_listeners |= new_player.client + start_playing(list(new_player.client)) + +/obj/machinery/cassette/dj_station/proc/next_song() + waiting_for_yield = TRUE + var/choice = tgui_input_number(usr, "Choose which song number to play.", "[src]", 1, length(current_playlist), 1) + if(!choice) + waiting_for_yield = FALSE + stop_broadcast() + return + GLOB.dj_broadcast = TRUE + pl_index = choice + + pl_index++ + start_playing(active_listeners) diff --git a/monkestation/code/modules/cassettes/machines/portable_mixer.dm b/monkestation/code/modules/cassettes/machines/portable_mixer.dm index 4f95b833c7da..332d22e5366a 100644 --- a/monkestation/code/modules/cassettes/machines/portable_mixer.dm +++ b/monkestation/code/modules/cassettes/machines/portable_mixer.dm @@ -5,13 +5,13 @@ icon_state = "walkman" w_class = WEIGHT_CLASS_SMALL ///The cassette that is being copied from - var/obj/item/device/cassette_tape/send + var/obj/item/cassette_tape/send ///List of songs the sender has var/list/sender_list ///List of names the Sender has var/list/sender_names ///The cassette you are copying to - var/obj/item/device/cassette_tape/recieve + var/obj/item/cassette_tape/recieve ///List of songs the Reciever has var/list/reciever_list ///List of song names the Reciever has @@ -32,7 +32,7 @@ removal = !removal /obj/item/device/cassette_deck/attackby(obj/item/cassette, mob/user) - if(!istype(cassette, /obj/item/device/cassette_tape)) + if(!istype(cassette, /obj/item/cassette_tape)) return if(!send || !recieve) insert_tape(cassette) @@ -76,7 +76,7 @@ reciever_list.Remove(reciever_list[num]) reciever_names.Remove(reciever_names[num]) -/obj/item/device/cassette_deck/proc/insert_tape(obj/item/device/cassette_tape/CTape) +/obj/item/device/cassette_deck/proc/insert_tape(obj/item/cassette_tape/CTape) if(send && recieve || !istype(CTape)) return diff --git a/monkestation/code/modules/cassettes/machines/postbox.dm b/monkestation/code/modules/cassettes/machines/postbox.dm index 3b60a4326667..01cbc8161b29 100644 --- a/monkestation/code/modules/cassettes/machines/postbox.dm +++ b/monkestation/code/modules/cassettes/machines/postbox.dm @@ -16,10 +16,10 @@ /obj/machinery/cassette/mailbox/attackby(obj/item/weapon, mob/user, params) - if(!istype(weapon, /obj/item/device/cassette_tape) || !user.client) + if(!istype(weapon, /obj/item/cassette_tape) || !user.client) return - var/obj/item/device/cassette_tape/attacked_tape = weapon + var/obj/item/cassette_tape/attacked_tape = weapon var/list/admin_count = get_admin_counts(R_FUN) if(!length(admin_count["present"])) diff --git a/monkestation/code/modules/cassettes/machines/stationary_mixer.dm b/monkestation/code/modules/cassettes/machines/stationary_mixer.dm index 632e9426e8ae..f0f5958f2411 100644 --- a/monkestation/code/modules/cassettes/machines/stationary_mixer.dm +++ b/monkestation/code/modules/cassettes/machines/stationary_mixer.dm @@ -6,7 +6,7 @@ density = TRUE pass_flags = PASSTABLE ///cassette tape used in adding songs or customizing - var/obj/item/device/cassette_tape/tape + var/obj/item/cassette_tape/tape ///Selection used to remove songs var/selection @@ -20,7 +20,7 @@ return TRUE /obj/machinery/cassette/adv_cassette_deck/attackby(obj/item/cassette, mob/user) - if(!istype(cassette, /obj/item/device/cassette_tape)) + if(!istype(cassette, /obj/item/cassette_tape)) return ..() if(!tape) insert_tape(cassette) @@ -29,7 +29,7 @@ else to_chat(user,"Remove a tape first!") -/obj/machinery/cassette/adv_cassette_deck/proc/insert_tape(obj/item/device/cassette_tape/CTape) +/obj/machinery/cassette/adv_cassette_deck/proc/insert_tape(obj/item/cassette_tape/CTape) if(tape || !istype(CTape)) return tape = CTape diff --git a/monkestation/code/modules/cassettes/random_cassette_selection.dm b/monkestation/code/modules/cassettes/random_cassette_selection.dm index 19b27f0fdd81..0cc292301f09 100644 --- a/monkestation/code/modules/cassettes/random_cassette_selection.dm +++ b/monkestation/code/modules/cassettes/random_cassette_selection.dm @@ -17,7 +17,7 @@ GLOBAL_LIST_INIT(approved_ids, initialize_approved_ids()) return list() return json_decode(file2text(ids_exist)) -/obj/item/device/cassette_tape/random +/obj/item/cassette_tape/random name = "Not Correctly Created Random Cassette" desc = "How did this happen?" random = TRUE diff --git a/monkestation/code/modules/cassettes/walkman/_walkmen.dm b/monkestation/code/modules/cassettes/walkman/_walkmen.dm index 6eee606c9ff9..a275fd3c8fed 100644 --- a/monkestation/code/modules/cassettes/walkman/_walkmen.dm +++ b/monkestation/code/modules/cassettes/walkman/_walkmen.dm @@ -15,7 +15,7 @@ GLOBAL_LIST_INIT(youtube_exempt, list( w_class = WEIGHT_CLASS_SMALL actions_types = list(/datum/action/item_action/walkman/play_pause,/datum/action/item_action/walkman/next_song,/datum/action/item_action/walkman/restart_song) ///the cassette tape object - var/obj/item/device/cassette_tape/tape + var/obj/item/cassette_tape/tape ///if the walkman is paused or not var/paused = TRUE ///songs inside the current playlist @@ -58,7 +58,7 @@ GLOBAL_LIST_INIT(youtube_exempt, list( . = ..() /obj/item/device/walkman/attackby(obj/item/cassette, mob/user) - if(!istype(cassette, /obj/item/device/cassette_tape)) + if(!istype(cassette, /obj/item/cassette_tape)) return if(!tape) insert_tape(cassette) @@ -142,7 +142,7 @@ GLOBAL_LIST_INIT(youtube_exempt, list( /obj/item/device/walkman/proc/play() if(!current_song) if(current_playlist.len > 0) - if(findtext(current_playlist[pl_index], GLOB.is_http_protocol)) + if(is_http_protocol(current_playlist[pl_index])) ///invoking youtube-dl var/ytdl = CONFIG_GET(string/invoke_youtubedl) ///the input for ytdl handled by the song list @@ -232,9 +232,9 @@ GLOBAL_LIST_INIT(youtube_exempt, list( /*Called when - *Arguments: obj/item/device/cassette_tape/CT -> the cassette in question that you are inserting into the walkman + *Arguments: obj/item/cassette_tape/CT -> the cassette in question that you are inserting into the walkman */ -/obj/item/device/walkman/proc/insert_tape(obj/item/device/cassette_tape/CTape) +/obj/item/device/walkman/proc/insert_tape(obj/item/cassette_tape/CTape) if(tape || !istype(CTape)) return @@ -285,7 +285,7 @@ GLOBAL_LIST_INIT(youtube_exempt, list( break_sound() pl_index = pl_index + 1 <= current_playlist.len ? (pl_index += 1) : 1 - link_play = findtext(current_playlist[pl_index], GLOB.is_http_protocol) ? TRUE : FALSE + link_play = is_http_protocol(current_playlist[pl_index]) if(!link_play) diff --git a/tgstation.dme b/tgstation.dme index 7518b42b14c8..f461f0a49bbe 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -412,6 +412,7 @@ #include "code\__DEFINES\~monkestation\blueshift.dm" #include "code\__DEFINES\~monkestation\botany.dm" #include "code\__DEFINES\~monkestation\cargo.dm" +#include "code\__DEFINES\~monkestation\cassettes.dm" #include "code\__DEFINES\~monkestation\chat.dm" #include "code\__DEFINES\~monkestation\chewin.dm" #include "code\__DEFINES\~monkestation\clock_cult.dm" @@ -608,6 +609,7 @@ #include "code\__HELPERS\~monkestation-helpers\mobs.dm" #include "code\__HELPERS\~monkestation-helpers\records.dm" #include "code\__HELPERS\~monkestation-helpers\roundend.dm" +#include "code\__HELPERS\~monkestation-helpers\text.dm" #include "code\__HELPERS\~monkestation-helpers\time.dm" #include "code\__HELPERS\~monkestation-helpers\virology.dm" #include "code\__HELPERS\~monkestation-helpers\logging\attack.dm" @@ -7030,7 +7032,7 @@ #include "monkestation\code\modules\cassettes\cassette_approval.dm" #include "monkestation\code\modules\cassettes\random_cassette_selection.dm" #include "monkestation\code\modules\cassettes\cassette_db\cassette_datum.dm" -#include "monkestation\code\modules\cassettes\cassette_db\subsystem.dm" +#include "monkestation\code\modules\cassettes\cassette_db\cassette_manager.dm" #include "monkestation\code\modules\cassettes\machines\cassette_rack.dm" #include "monkestation\code\modules\cassettes\machines\dj_station.dm" #include "monkestation\code\modules\cassettes\machines\portable_mixer.dm"