diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b5147f210dd7..8046f3196652 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,7 @@ "eamodio.gitlens", "usernamehw.errorlens", "anturk.dmi-editor", - "esbenp.prettier-vscode" + "esbenp.prettier-vscode", + "arcanis.vscode-zipfs" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 9c4c5f94ab1f..961a080b06bf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,8 +4,16 @@ ], // ESLint settings: "eslint.workingDirectories": [ - "tgui/" + "./tgui" ], + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + }, + "eslint.nodePath": "tgui/.yarn/sdks", + "prettier.prettierPath": "tgui/.yarn/sdks/prettier/index.cjs", + "typescript.tsdk": "tgui/.yarn/sdks/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, "eslint.rules.customizations": [ // We really want to fail the CI builds on styling errors, // but it's better to show them as yellow squigglies in IDE diff --git a/.vscode/tasks.json b/.vscode/tasks.json index dca695c03ccc..47c848d24001 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -14,7 +14,7 @@ "type": "shell", "command": "tgui/bin/tgui-build", "windows": { - "command": ".\\tgui\\bin\\tgui-build.bat" + "command": ".\\tgui\\bin\\tgui.bat" }, "problemMatcher": [ "$tsc", @@ -37,18 +37,5 @@ "label": "tgui: run dev server" } , - { - "type": "shell", - "command": "tgui/bin/tgui-formatting", - "windows": { - "command": ".\\tgui\\bin\\tgui-formatting.bat" - }, - "problemMatcher": [ - "$tsc", - "$eslint-stylish" - ], - "group": "build", - "label": "tgui: run prettier formatting" - } ] } diff --git a/_build_dependencies.sh b/_build_dependencies.sh index d146ad1c518d..7e4a8ce153ec 100644 --- a/_build_dependencies.sh +++ b/_build_dependencies.sh @@ -2,7 +2,7 @@ # For dreamchecker export SPACEMANDMM_TAG=suite-1.7.1 # For TGUI -export NODE_VERSION=18 +export NODE_VERSION=20 # Stable Byond Major export STABLE_BYOND_MAJOR=515 # Stable Byond Minor diff --git a/code/__DEFINES/chat.dm b/code/__DEFINES/chat.dm new file mode 100644 index 000000000000..149f5d856988 --- /dev/null +++ b/code/__DEFINES/chat.dm @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/// How many chat payloads to keep in history +#define CHAT_RELIABILITY_HISTORY_SIZE 5 +/// How many resends to allow before giving up +#define CHAT_RELIABILITY_MAX_RESENDS 3 + +#define MESSAGE_TYPE_SYSTEM "system" +#define MESSAGE_TYPE_LOCALCHAT "localchat" +#define MESSAGE_TYPE_RADIO "radio" +#define MESSAGE_TYPE_INFO "info" +#define MESSAGE_TYPE_WARNING "warning" +#define MESSAGE_TYPE_DEADCHAT "deadchat" +#define MESSAGE_TYPE_OOC "ooc" +#define MESSAGE_TYPE_ADMINPM "adminpm" +#define MESSAGE_TYPE_COMBAT "combat" +#define MESSAGE_TYPE_ADMINCHAT "adminchat" +#define MESSAGE_TYPE_MENTORCHAT "mentorchat" +#define MESSAGE_TYPE_EVENTCHAT "eventchat" +#define MESSAGE_TYPE_ADMINLOG "adminlog" +#define MESSAGE_TYPE_ATTACKLOG "attacklog" +#define MESSAGE_TYPE_DEBUG "debug" diff --git a/code/__DEFINES/chat_box_defines.dm b/code/__DEFINES/chat_box_defines.dm index f2ffff8aa07c..40a1c491cf37 100644 --- a/code/__DEFINES/chat_box_defines.dm +++ b/code/__DEFINES/chat_box_defines.dm @@ -2,6 +2,7 @@ #define chat_box_examine(str) ("
") #define chat_box_red(str) (" ") #define chat_box_green(str) (" ") +#define chat_box_purple(str) (" ") #define chat_box_notice(str) (" ") #define chat_box_healthscan(str) (" ") #define chat_box_notice_thick(str) (" ") diff --git a/code/__DEFINES/construction_defines.dm b/code/__DEFINES/construction_defines.dm index 9612ed7f2c1f..86be91003162 100644 --- a/code/__DEFINES/construction_defines.dm +++ b/code/__DEFINES/construction_defines.dm @@ -59,19 +59,19 @@ #define FULLTILE_WINDOW_DIR NORTHEAST //Material defines, for determining how much of a given material an item contains -#define MAT_METAL "$metal" -#define MAT_GLASS "$glass" -#define MAT_SILVER "$silver" -#define MAT_GOLD "$gold" -#define MAT_DIAMOND "$diamond" -#define MAT_URANIUM "$uranium" -#define MAT_PLASMA "$plasma" -#define MAT_BLUESPACE "$bluespace" -#define MAT_BANANIUM "$bananium" -#define MAT_TRANQUILLITE "$tranquillite" -#define MAT_TITANIUM "$titanium" -#define MAT_BIOMASS "$biomass" -#define MAT_PLASTIC "$plastic" +#define MAT_METAL "metal" +#define MAT_GLASS "glass" +#define MAT_SILVER "silver" +#define MAT_GOLD "gold" +#define MAT_DIAMOND "diamond" +#define MAT_URANIUM "uranium" +#define MAT_PLASMA "plasma" +#define MAT_BLUESPACE "bluespace" +#define MAT_BANANIUM "bananium" +#define MAT_TRANQUILLITE "tranquillite" +#define MAT_TITANIUM "titanium" +#define MAT_BIOMASS "biomass" +#define MAT_PLASTIC "plastic" //The amount of materials you get from a sheet of mineral like iron/diamond/glass etc #define MINERAL_MATERIAL_AMOUNT 2000 //The maximum size of a stack object. diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm index 482bc95f622c..5d0e4d461454 100644 --- a/code/__DEFINES/misc_defines.dm +++ b/code/__DEFINES/misc_defines.dm @@ -313,6 +313,8 @@ #define TRIGGER_GUARD_NONE 0 #define TRIGGER_GUARD_NORMAL 1 +#define CLIENT_FROM_VAR(I) (ismob(I) ? I:client : (istype(I, /client) ? I : (istype(I, /datum/mind) ? I:current?:client : null))) + // Macro to get the current elapsed round time, rather than total world runtime #define ROUND_TIME (SSticker.time_game_started ? (world.time - SSticker.time_game_started) : 0) diff --git a/code/__DEFINES/stat.dm b/code/__DEFINES/stat.dm index addf59e2f02e..ac824022f6cf 100644 --- a/code/__DEFINES/stat.dm +++ b/code/__DEFINES/stat.dm @@ -3,12 +3,6 @@ #define UNCONSCIOUS 1 #define DEAD 2 -// TGUI flags -#define STATUS_INTERACTIVE 2 // GREEN Visability -#define STATUS_UPDATE 1 // ORANGE Visability -#define STATUS_DISABLED 0 // RED Visability -#define STATUS_CLOSE -1 // Close the interface - /* Shuttles */ diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 19a59ed2faf6..53962a8ee9a1 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -84,10 +84,12 @@ #define INIT_ORDER_LATE_MAPPING -40 #define INIT_ORDER_PATH -50 #define INIT_ORDER_PERSISTENCE -95 +#define INIT_ORDER_CHAT -100 // Should be last to ensure chat remains smooth during init. // Subsystem fire priority, from lowest to highest priority // If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child) +#define FIRE_PRIORITY_PING 10 #define FIRE_PRIORITY_NANOMOB 10 #define FIRE_PRIORITY_NIGHTSHIFT 10 #define FIRE_PRIORITY_IDLE_NPC 10 @@ -112,6 +114,7 @@ #define FIRE_PRIORITY_MOBS 100 #define FIRE_PRIORITY_TGUI 110 #define FIRE_PRIORITY_TICKER 200 +#define FIRE_PRIORITY_CHAT 400 #define FIRE_PRIORITY_RUNECHAT 410 // I hate how high the fire priority on this is -aa #define FIRE_PRIORITY_OVERLAYS 500 #define FIRE_PRIORITY_DELAYED_VERBS 950 diff --git a/code/__DEFINES/tgui_defines.dm b/code/__DEFINES/tgui_defines.dm index 6257e24d9f8c..ccd8d25f6f70 100644 --- a/code/__DEFINES/tgui_defines.dm +++ b/code/__DEFINES/tgui_defines.dm @@ -1,4 +1,4 @@ -// TGUI defines +// TGUI Modal defines #define UI_MODAL_INPUT_MAX_LENGTH 1024 #define UI_MODAL_INPUT_MAX_LENGTH_NAME 64 // Names for generally anything don't go past 32, let alone 64. @@ -6,3 +6,41 @@ #define UI_MODAL_DELEGATE 2 #define UI_MODAL_ANSWER 3 #define UI_MODAL_CLOSE 4 + +/// Green eye; fully interactive +#define UI_INTERACTIVE 2 +/// Orange eye; updates but is not interactive +#define UI_UPDATE 1 +/// Red eye; disabled, does not update +#define UI_DISABLED 0 +/// UI Should close +#define UI_CLOSE -1 + +/// Maximum number of windows that can be suspended/reused +#define TGUI_WINDOW_SOFT_LIMIT 5 +/// Maximum number of open windows +#define TGUI_WINDOW_HARD_LIMIT 9 + +/// Maximum ping timeout allowed to detect zombie windows +#define TGUI_PING_TIMEOUT (4 SECONDS) +/// Used for rate-limiting to prevent DoS by excessively refreshing a TGUI window +#define TGUI_REFRESH_FULL_UPDATE_COOLDOWN (1 SECONDS) + +/// Window does not exist +#define TGUI_WINDOW_CLOSED 0 +/// Window was just opened, but is still not ready to be sent data +#define TGUI_WINDOW_LOADING 1 +/// Window is free and ready to receive data +#define TGUI_WINDOW_READY 2 + +/// Get a window id based on the provided pool index +#define TGUI_WINDOW_ID(index) "tgui-window-[index]" +/// Get a pool index of the provided window id +#define TGUI_WINDOW_INDEX(window_id) text2num(copytext(window_id, 13)) + +/// Creates a message packet for sending via output() +#define TGUI_CREATE_MESSAGE(type, payload) ( \ + url_encode(json_encode(list( \ + "type" = type, \ + "payload" = payload, \ + )))) diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index de2800a7ccc8..c1d4dde450ca 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -40,7 +40,10 @@ GLOBAL_PROTECT(log_end) for(var/client/C in GLOB.admins) if(check_rights(R_DEBUG | R_VIEWRUNTIMES, FALSE, C.mob) && (C.prefs.toggles & PREFTOGGLE_CHAT_DEBUGLOGS)) - to_chat(C, "DEBUG: [text]") + to_chat(C, + type = MESSAGE_TYPE_DEBUG, + html = "DEBUG: [text]", + confidential = TRUE) /proc/log_game(text) if(GLOB.configuration.logging.game_logging) @@ -149,8 +152,19 @@ GLOBAL_PROTECT(log_end) /proc/log_runtime_summary(text) rustg_log_write(GLOB.runtime_summary_log, "[text][GLOB.log_end]") -/proc/log_tgui(text) - rustg_log_write(GLOB.tgui_log, "[text][GLOB.log_end]") +/proc/log_tgui(user_or_client, text) + var/list/messages = list() + if(!user_or_client) + messages.Add("no user") + else if(ismob(user_or_client)) + var/mob/user = user_or_client + messages.Add("[user.ckey] (as [user])") + else if(isclient(user_or_client)) + var/client/client = user_or_client + messages.Add("[client.ckey]") + messages.Add(": [text]") + messages.Add("[GLOB.log_end]") + rustg_log_write(GLOB.tgui_log, messages.Join()) #ifdef REFERENCE_TRACKING /proc/log_gc(text) diff --git a/code/__HELPERS/assets.dm b/code/__HELPERS/assets.dm new file mode 100644 index 000000000000..a288f7727288 --- /dev/null +++ b/code/__HELPERS/assets.dm @@ -0,0 +1,5 @@ +/// Generate a filename for this asset +/// The same asset will always lead to the same asset name +/// (Generated names do not include file extention.) +/proc/generate_asset_name(file) + return "asset.[md5(fcopy_rsc(file))]" diff --git a/code/__HELPERS/files.dm b/code/__HELPERS/files.dm index 397c05c466c8..114bd52c7b4a 100644 --- a/code/__HELPERS/files.dm +++ b/code/__HELPERS/files.dm @@ -33,11 +33,6 @@ return text -//Sends resource files to client cache -/client/proc/getFiles() - for(var/file in args) - src << browse_rsc(file) - /client/proc/browse_files(root="data/logs/", max_iterations=10, list/valid_extensions=list("txt", "log", "htm", "json")) // wow why was this ever a parameter root = "data/logs/" diff --git a/code/__HELPERS/iconprocs.dm b/code/__HELPERS/iconprocs.dm index eb86df49e86d..85921eee53d2 100644 --- a/code/__HELPERS/iconprocs.dm +++ b/code/__HELPERS/iconprocs.dm @@ -299,3 +299,44 @@ world M.Blend("#ffffff", ICON_SUBTRACT) // apply mask Blend(M, ICON_ADD) + +//Converts an icon to base64. Operates by putting the icon in the iconCache savefile, +// exporting it as text, and then parsing the base64 from that. +// (This relies on byond automatically storing icons in savefiles as base64) +GLOBAL_DATUM_INIT(iconCache, /savefile, new /savefile("data/iconCache.sav")) + +GLOBAL_LIST_EMPTY(bicon_cache) + +/proc/icon2base64(icon/icon, iconKey = "misc") + if(!isicon(icon)) return 0 + + GLOB.iconCache[iconKey] << icon + var/iconData = GLOB.iconCache.ExportText(iconKey) + var/list/partial = splittext(iconData, "{") + return replacetext(copytext(partial[2], 3, -5), "\n", "") + +/proc/bicon(obj, use_class = 1) + var/class = use_class ? "class='icon misc'" : null + if(!obj) + return + + if(isicon(obj)) + if(!GLOB.bicon_cache["\ref[obj]"]) // Doesn't exist yet, make it. + GLOB.bicon_cache["\ref[obj]"] = icon2base64(obj) + + return "" + + // Either an atom or somebody fucked up and is gonna get a runtime, which I'm fine with. + var/atom/A = obj + var/key = "[istype(A.icon, /icon) ? "\ref[A.icon]" : A.icon]:[A.icon_state]" + if(!GLOB.bicon_cache[key]) // Doesn't exist, make it. + var/icon/I = icon(A.icon, A.icon_state, SOUTH, 1) + if(ishuman(obj)) // Shitty workaround for a BYOND issue. + var/icon/temp = I + I = icon() + I.Insert(temp, dir = SOUTH) + GLOB.bicon_cache[key] = icon2base64(I, key) + if(use_class) + class = "class='icon [A.icon_state]'" + + return "" diff --git a/code/__HELPERS/lists.dm b/code/__HELPERS/lists.dm index c4f932574a6e..cf6642875ffa 100644 --- a/code/__HELPERS/lists.dm +++ b/code/__HELPERS/lists.dm @@ -410,13 +410,6 @@ var/middle = L.len / 2 + 1 // Copy is first,second-1 return mergeLists(sortList(L.Copy(0,middle)), sortList(L.Copy(middle))) //second parameter null = to end of list -//Mergsorge: uses sortAssoc() but uses the var's name specifically. This should probably be using mergeAtom() instead -/proc/sortNames(list/L) - var/list/Q = new() - for(var/atom/x in L) - Q[x.name] = x - return sortAssoc(Q) - /proc/mergeLists(list/L, list/R) var/Li=1 var/Ri=1 diff --git a/code/__HELPERS/name_helpers.dm b/code/__HELPERS/name_helpers.dm index 5f97da0fd55a..389d8a9759db 100644 --- a/code/__HELPERS/name_helpers.dm +++ b/code/__HELPERS/name_helpers.dm @@ -108,6 +108,10 @@ GLOBAL_VAR(syndicate_name) GLOBAL_VAR(syndicate_code_phrase) //Code phrase for traitors. GLOBAL_VAR(syndicate_code_response) //Code response for traitors. +//Cached regex search - for checking if codewords are used. +GLOBAL_DATUM(syndicate_code_phrase_regex, /regex) +GLOBAL_DATUM(syndicate_code_response_regex, /regex) + /* Should be expanded. How this works: @@ -122,9 +126,12 @@ GLOBAL_VAR(syndicate_code_response) //Code response for traitors. /N */ -/proc/generate_code_phrase()//Proc is used for phrase and response in master_controller.dm +/proc/generate_code_phrase(return_list = FALSE) // Proc is used for phrase and response in master_controller.dm - var/code_phrase = ""//What is returned when the proc finishes. + if(!return_list) + . = "" + else + . = list() var/words = pick(//How many words there will be. Minimum of two. 2, 4 and 5 have a lesser chance of being selected. 3 is the most likely. 50; 2, 200; 3, @@ -152,34 +159,34 @@ GLOBAL_VAR(syndicate_code_response) //Code response for traitors. switch(pick(safety))//Chance based on the safety list. if(1)//1 and 2 can only be selected once each to prevent more than two specific names/places/etc. - switch(rand(1,2))//Mainly to add more options later. + switch(rand(1, 2)) // Mainly to add more options later. if(1) - if(names.len) - code_phrase += pick(names) + if(length(names)) + . += pick(names) if(2) - code_phrase += pick(GLOB.jobs)//Returns a job. // SS220 EDIT - ORIGINAL: (GLOB.joblist) + . += pick(GLOB.jobs)//Returns a job. // SS220 EDIT - ORIGINAL: (GLOB.joblist) safety -= 1 if(2) switch(rand(1,2))//Places or things. if(1) - code_phrase += pick(GLOB.cocktails) // SS220 EDIT - ORIGINAL: (drinks) + . += pick(GLOB.cocktails) // SS220 EDIT - ORIGINAL: (drinks) if(2) - code_phrase += pick(GLOB.locations) // SS220 EDIT - ORIGINAL: (locations) + . += pick(GLOB.locations) // SS220 EDIT - ORIGINAL: (locations) safety -= 2 if(3) - switch(rand(1,3))//Nouns, adjectives, verbs. Can be selected more than once. + switch(rand(1, 3)) // Nouns, adjectives, verbs. Can be selected more than once. if(1) - code_phrase += pick(GLOB.nouns) // SS220 EDIT - ORIGINAL: (nouns) + . += pick(GLOB.nouns) // SS220 EDIT - ORIGINAL: (nouns) if(2) - code_phrase += pick(GLOB.adjectives) + . += pick(GLOB.adjectives) if(3) - code_phrase += pick(GLOB.verbs) - if(words==1) - code_phrase += "." - else - code_phrase += ", " + . += pick(GLOB.verbs) - return code_phrase + if(!return_list) + if(words == 1) + . += "." + else + . += ", " /proc/GenerateKey() var/newKey @@ -187,89 +194,3 @@ GLOBAL_VAR(syndicate_code_response) //Code response for traitors. newKey += pick("diamond", "beer", "mushroom", "assistant", "clown", "captain", "twinkie", "security", "nuke", "small", "big", "escape", "yellow", "gloves", "monkey", "engine", "nuclear", "ai") newKey += pick("1", "2", "3", "4", "5", "6", "7", "8", "9", "0") return newKey - -/* -//This proc tests the gen above. -/client/verb/test_code_phrase() - set name = "Generate Code Phrase" - set category = "Debug" - - to_chat(world, "Code Phrase is: [generate_code_phrase()]") - return - - - This was an earlier attempt at code phrase system, aside from an even earlier attempt (and failure). - This system more or less works as intended--aside from being unfinished--but it's still very predictable. - Particularly, the phrase opening statements are pretty easy to recognize and identify when metagaming. - I think the above-used method solves this issue by using words in a sequence, providing for much greater flexibility. - /N - - switch(choice) - if(1) - syndicate_code_phrase += pick("I'm looking for","Have you seen","Maybe you've seen","I'm trying to find","I'm tracking") - syndicate_code_phrase += " " - syndicate_code_phrase += pick(pick(GLOB.first_names_male,GLOB.first_names_female)) - syndicate_code_phrase += " " - syndicate_code_phrase += pick(GLOB.last_names) - syndicate_code_phrase += "." - if(2) - syndicate_code_phrase += pick("How do I get to","How do I find","Where is","Where do I find") - syndicate_code_phrase += " " - syndicate_code_phrase += pick("Escape","Engineering","Atmos","the bridge","the brig","CentComm","the library","the chapel","a bathroom","Med Bay","Tool Storage","the escape shuttle","Robotics","a locker room","the living quarters","the gym","the autolathe","QM","the bar","the theater","the derelict") - syndicate_code_phrase += "?" - if(3) - if(prob(70)) - syndicate_code_phrase += pick("Get me","I want","I'd like","Make me") - syndicate_code_phrase += " a " - else - syndicate_code_phrase += pick("One") - syndicate_code_phrase += " " - syndicate_code_phrase += pick("vodka and tonic","gin fizz","bahama mama","manhattan","black Russian","whiskey soda","long island tea","margarita","Irish coffee"," manly dwarf","Irish cream","doctor's delight","Beepksy Smash","tequila sunrise","brave bull","gargle blaster","bloody mary","whiskey cola","white Russian","vodka martini","martini","Cuba libre","kahlua","vodka","wine","moonshine") - syndicate_code_phrase += "." - if(4) - syndicate_code_phrase += pick("I wish I was","My dad was","His mom was","Where do I find","The hero this station needs is","I'd fuck","I wouldn't trust","Someone caught","HoS caught","Someone found","I'd wrestle","I wanna kill") - syndicate_code_phrase += " [pick("a","the")] " - syndicate_code_phrase += pick("wizard","ninja","xeno","lizard","slime","monkey","syndicate","cyborg","clown","space carp","singularity","singulo","mime") - syndicate_code_phrase += "." - if(5) - syndicate_code_phrase += pick("Do we have","Is there","Where is","Where's","Who's") - syndicate_code_phrase += " " - syndicate_code_phrase += "[pick(GLOB.joblist)]" - syndicate_code_phrase += "?" - - switch(choice) - if(1) - if(prob(80)) - syndicate_code_response += pick("Try looking for them near","I they ran off to","Yes. I saw them near","Nope. I'm heading to","Try searching") - syndicate_code_response += " " - syndicate_code_response += pick("Escape","Engineering","Atmos","the bridge","the brig","CentComm","the library","the chapel","a bathroom","Med Bay","Tool Storage","the escape shuttle","Robotics","a locker room","the living quarters","the gym","the autolathe","QM","the bar","the theater","the derelict") - syndicate_code_response += "." - else if(prob(60)) - syndicate_code_response += pick("No. I'm busy, sorry.","I don't have the time.","Not sure, maybe?","There is no time.") - else - syndicate_code_response += pick("*shrug*","*smile*","*blink*","*sigh*","*laugh*","*nod*","*giggle*") - if(2) - if(prob(80)) - syndicate_code_response += pick("Go to","Navigate to","Try","Sure, run to","Try searching","It's near","It's around") - syndicate_code_response += " the " - syndicate_code_response += pick("[pick("south","north","east","west")] maitenance door","nearby maitenance","teleporter","[pick("cold","dead")] space","morgue","vacuum","[pick("south","north","east","west")] hall ","[pick("south","north","east","west")] hallway","[pick("white","black","red","green","blue","pink","purple")] [pick("rabbit","frog","lion","tiger","panther","snake","facehugger")]") - syndicate_code_response += "." - else if(prob(60)) - syndicate_code_response += pick("Try asking","Ask","Talk to","Go see","Follow","Hunt down") - syndicate_code_response += " " - if(prob(50)) - syndicate_code_response += pick(pick(GLOB.first_names_male,GLOB.first_names_female)) - syndicate_code_response += " " - syndicate_code_response += pick(GLOB.last_names) - else - syndicate_code_response += " the " - syndicate_code_response += "[pic(GLOB.joblist)]" - syndicate_code_response += "." - else - syndicate_code_response += pick("*shrug*","*smile*","*blink*","*sigh*","*laugh*","*nod*","*giggle*") - if(3) - if(4) - if(5) - - return -*/ diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm index 4aa40db1f936..994ebd4f733e 100644 --- a/code/__HELPERS/type2type.dm +++ b/code/__HELPERS/type2type.dm @@ -389,3 +389,8 @@ if(input == "true") return TRUE return FALSE // + +/// Return html to load a url. +/// for use inside of browse() calls to html assets that might be loaded on a cdn. +/proc/url2htmlloader(url) + return {""} diff --git a/code/controllers/configuration/configuration_core.dm b/code/controllers/configuration/configuration_core.dm index 26d3647ae7ee..0ace31c5bd89 100644 --- a/code/controllers/configuration/configuration_core.dm +++ b/code/controllers/configuration/configuration_core.dm @@ -46,6 +46,8 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new()) var/datum/configuration_section/url_configuration/url /// Holder for the voting configuration datum var/datum/configuration_section/vote_configuration/vote + /// Holder for the asset cache configuration datum + var/datum/configuration_section/asset_cache_configuration/asset_cache /// Raw data. Stored here to avoid passing data between procs constantly var/list/raw_data = list() @@ -92,6 +94,7 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new()) system = new() url = new() vote = new() + asset_cache = new() // Load our stuff up var/config_file = "config/config.toml" @@ -129,6 +132,7 @@ GLOBAL_DATUM_INIT(configuration, /datum/server_configuration, new()) safe_load(system, "system_configuration") safe_load(url, "url_configuration") safe_load(vote, "voting_configuration") + safe_load(asset_cache, "asset_cache_configuration") // Proc to load up instance-specific overrides /datum/server_configuration/proc/load_overrides() diff --git a/code/controllers/configuration/sections/asset_cache_configuration.dm b/code/controllers/configuration/sections/asset_cache_configuration.dm new file mode 100644 index 000000000000..40516344c6e3 --- /dev/null +++ b/code/controllers/configuration/sections/asset_cache_configuration.dm @@ -0,0 +1,22 @@ +/datum/configuration_section/asset_cache_configuration + /// Type of asset transport that will be used for asset delivery + var/asset_transport = "simple" + /// Whether to make server passively send all browser assets to each client in the background. + /// (instead of waiting for them to be needed) + var/asset_simple_preload = TRUE + /// Local folder to save assets to. + /// Assets will be saved in the format of asset.MD5HASH.EXT or in namespaces/hash/ + /// as ASSET_FILE_NAME or asset.MD5HASH.EXT + var/asset_cdn_webroot = "data/asset-store/" + /// URL the `asset_cdn_webroot` can be accessed from. + /// For best results the webserver powering this should return a long cache validity time, + /// as all assets sent via this transport use hash based urls + /// if you want to test this locally, you simpily run the `localhost-asset-webroot-server.py` python3 script + /// to host assets stored in `data/asset-store/` via http://localhost:58715/ + var/asset_cdn_url = "http://localhost:58715/" + +/datum/configuration_section/asset_cache_configuration/load_data(list/data) + CONFIG_LOAD_STR(asset_transport, data["asset_transport"]) + CONFIG_LOAD_BOOL(asset_simple_preload, data["asset_simple_preload"]) + CONFIG_LOAD_STR(asset_cdn_webroot, data["asset_cdn_webroot"]) + CONFIG_LOAD_STR(asset_cdn_url, data["asset_cdn_url"]) diff --git a/code/controllers/controller.dm b/code/controllers/controller.dm index 3645cea82e83..93c78e915bf9 100644 --- a/code/controllers/controller.dm +++ b/code/controllers/controller.dm @@ -23,5 +23,8 @@ */ /datum/controller/proc/log_startup_progress(message) Master.last_init_info = "([name]): [message]" - to_chat(world, "\[[name]] [message]") + to_chat(world, + type = MESSAGE_TYPE_DEBUG, + html = "\[[name]] [message]", + confidential = TRUE) log_world("\[[name]] [message]") diff --git a/code/controllers/subsystem/SSchat.dm b/code/controllers/subsystem/SSchat.dm new file mode 100644 index 000000000000..88448a736b83 --- /dev/null +++ b/code/controllers/subsystem/SSchat.dm @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +SUBSYSTEM_DEF(chat) + name = "Chat" + flags = SS_TICKER|SS_NO_INIT + wait = 1 + priority = FIRE_PRIORITY_CHAT + init_order = INIT_ORDER_CHAT + + /// Assosciates a ckey with a list of messages to send to them. + var/list/list/datum/chat_payload/client_to_payloads = list() + + /// Associates a ckey with an assosciative list of their last CHAT_RELIABILITY_HISTORY_SIZE messages. + var/list/list/datum/chat_payload/client_to_reliability_history = list() + + /// Assosciates a ckey with their next sequence number. + var/list/client_to_sequence_number = list() + +/datum/controller/subsystem/chat/proc/generate_payload(client/target, message_data) + var/sequence = client_to_sequence_number[target.ckey] + client_to_sequence_number[target.ckey] += 1 + + var/datum/chat_payload/payload = new + payload.sequence = sequence + payload.content = message_data + + if(!(target.ckey in client_to_reliability_history)) + client_to_reliability_history[target.ckey] = list() + var/list/client_history = client_to_reliability_history[target.ckey] + client_history["[sequence]"] = payload + + if(length(client_history) > CHAT_RELIABILITY_HISTORY_SIZE) + var/oldest = text2num(client_history[1]) + for(var/index in 2 to length(client_history)) + var/test = text2num(client_history[index]) + if(test < oldest) + oldest = test + client_history -= "[oldest]" + return payload + +/datum/controller/subsystem/chat/proc/send_payload_to_client(client/target, datum/chat_payload/payload) + target.tgui_panel.window.send_message("chat/message", payload.into_message()) + SEND_TEXT(target, payload.get_content_as_html()) + +/datum/controller/subsystem/chat/fire() + for(var/ckey in client_to_payloads) + var/client/target = GLOB.directory[ckey] + if(isnull(target)) // verify client still exists + LAZYREMOVE(client_to_payloads, ckey) + continue + + for(var/datum/chat_payload/payload as anything in client_to_payloads[ckey]) + send_payload_to_client(target, payload) + LAZYREMOVE(client_to_payloads, ckey) + + if(MC_TICK_CHECK) + return + +/datum/controller/subsystem/chat/proc/queue(queue_target, list/message_data) + var/list/targets = islist(queue_target) ? queue_target : list(queue_target) + for(var/target in targets) + var/client/client = CLIENT_FROM_VAR(target) + if(isnull(client)) + continue + LAZYADDASSOC(client_to_payloads, client.ckey, generate_payload(client, message_data)) + +/datum/controller/subsystem/chat/proc/send_immediate(send_target, list/message_data) + var/list/targets = islist(send_target) ? send_target : list(send_target) + for(var/target in targets) + var/client/client = CLIENT_FROM_VAR(target) + if(isnull(client)) + continue + send_payload_to_client(client, generate_payload(client, message_data)) + +/datum/controller/subsystem/chat/proc/handle_resend(client/client, sequence) + var/list/client_history = client_to_reliability_history[client.ckey] + sequence = "[sequence]" + if(isnull(client_history) || !(sequence in client_history)) + return + + var/datum/chat_payload/payload = client_history[sequence] + if(payload.resends > CHAT_RELIABILITY_MAX_RESENDS) + return // we tried but byond said no + + payload.resends += 1 + send_payload_to_client(client, client_history[sequence]) + SSblackbox.record_feedback( + "nested tally", + "chat_resend_byond_version", + 1, + list( + "[client.byond_version]", + "[client.byond_build]", + ), + ) diff --git a/code/controllers/subsystem/SSchat_pings.dm b/code/controllers/subsystem/SSchat_pings.dm deleted file mode 100644 index 24fb85d0bd87..000000000000 --- a/code/controllers/subsystem/SSchat_pings.dm +++ /dev/null @@ -1,17 +0,0 @@ -SUBSYSTEM_DEF(chat_pings) - name = "Chat Pings" - flags = SS_NO_INIT - runlevels = RUNLEVEL_INIT | RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME // ALL OF THEM - wait = 30 SECONDS // Chat pings every 30 seconds - cpu_display = SS_CPUDISPLAY_LOW - /// List of all held chat datums - var/list/datum/chatOutput/chat_datums = list() // Do NOT put this in Initialize(). You will cause issues. - -/datum/controller/subsystem/chat_pings/fire(resumed) - for(var/datum/chatOutput/CO as anything in chat_datums) - CO.updatePing() - if(MC_TICK_CHECK) - return - -/datum/controller/subsystem/chat_pings/get_stat_details() - return "P: [length(chat_datums)]" diff --git a/code/controllers/subsystem/SSdbcore.dm b/code/controllers/subsystem/SSdbcore.dm index e0dfe5c1067a..de652c3faf4d 100644 --- a/code/controllers/subsystem/SSdbcore.dm +++ b/code/controllers/subsystem/SSdbcore.dm @@ -386,7 +386,10 @@ SUBSYSTEM_DEF(dbcore) if(!.) SSdbcore.total_errors++ if(usr) - to_chat(usr, "A SQL error occurred during this operation, please inform an admin or a coder.") + to_chat(usr, + type = MESSAGE_TYPE_ADMINLOG, + html = "A SQL error occurred during this operation, please inform an admin or a coder.", + confidential = TRUE) message_admins("An SQL error has occurred. Please check the server logs, with the following timestamp ID: \[[time_stamp()]]") /** diff --git a/code/controllers/subsystem/SSdebugview.dm b/code/controllers/subsystem/SSdebugview.dm index 6b145b416be3..0f973a5deb21 100644 --- a/code/controllers/subsystem/SSdebugview.dm +++ b/code/controllers/subsystem/SSdebugview.dm @@ -31,7 +31,7 @@ SUBSYSTEM_DEF(debugview) entries += "\[Processing] Cost: [round(SSprocessing.cost, 1)]ms | P: [length(SSprocessing.processing)]" entries += "\[Projectiles] Cost: [round(SSprojectiles.cost, 1)]ms | P: [length(SSprojectiles.processing)]" entries += "\[Runechat] Cost: [round(SSrunechat.cost, 1)]ms | AM: [SSrunechat.bucket_count] | SQ: [length(SSrunechat.second_queue)]" - entries += "\[TGUI] Cost: [round(SStgui.cost, 1)]ms | P: [length(SStgui.processing_uis)]" + entries += "\[TGUI] Cost: [round(SStgui.cost, 1)]ms | P: [length(SStgui.open_uis)]]" entries += "\[Timer] Cost: [round(SStimer.cost, 1)]ms | B: [SStimer.bucket_count] | P: [length(SStimer.second_queue)] | RST: [SStimer.bucket_reset_count]" // Do some parsing to format it properly diff --git a/code/controllers/subsystem/SSping.dm b/code/controllers/subsystem/SSping.dm new file mode 100644 index 000000000000..2882c2bfc262 --- /dev/null +++ b/code/controllers/subsystem/SSping.dm @@ -0,0 +1,39 @@ +/*! + * Copyright (c) 2022 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +SUBSYSTEM_DEF(ping) + name = "Ping" + priority = FIRE_PRIORITY_PING + wait = 4 SECONDS + flags = SS_NO_INIT + runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME + + var/list/currentrun = list() + +/datum/controller/subsystem/ping/stat_entry() + ..("P:[length(GLOB.clients)]") + +/datum/controller/subsystem/ping/fire(resumed = FALSE) + // Prepare the new batch of clients + if(!resumed) + src.currentrun = GLOB.clients.Copy() + + // De-reference the list for sanic speeds + var/list/currentrun = src.currentrun + + while(length(currentrun)) + var/client/client = currentrun[currentrun.len] + currentrun.len-- + + if(client?.tgui_panel?.is_ready()) + // Send a soft ping + client.tgui_panel.window.send_message("ping/soft", list( + // Slightly less than the subsystem timer (somewhat arbitrary) + // to prevent incoming pings from resetting the afk state + "afk" = client.is_afk(3.5 SECONDS), + )) + + if(MC_TICK_CHECK) + return diff --git a/code/controllers/subsystem/SStgui.dm b/code/controllers/subsystem/SStgui.dm index 8ef9384f09c3..704ef2490b6d 100644 --- a/code/controllers/subsystem/SStgui.dm +++ b/code/controllers/subsystem/SStgui.dm @@ -1,311 +1,361 @@ - /** - * tgui subsystem - * - * Contains all tgui state and subsystem code. - **/ - +/** + * tgui subsystem + * + * Contains all tgui state and subsystem code. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ SUBSYSTEM_DEF(tgui) - name = "TGUI" + name = "tgui" wait = 9 flags = SS_NO_INIT priority = FIRE_PRIORITY_TGUI runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT - offline_implications = "All TGUIs will no longer process. Shuttle call recommended." - var/list/currentrun = list() - var/list/open_uis = list() // A list of open UIs, grouped by src_object and ui_key. - var/list/processing_uis = list() // A list of processing UIs, ungrouped. - var/basehtml // The HTML base used for all UIs. + /// A list of UIs scheduled to process + var/list/current_run = list() + /// A list of open UIs + var/list/open_uis = list() + /// A list of open UIs, grouped by src_object. + var/list/open_uis_by_src = list() + /// The HTML base used for all UIs. + var/basehtml /datum/controller/subsystem/tgui/PreInit() - basehtml = file2text('tgui/packages/tgui/public/tgui.html') + basehtml = file2text('tgui/public/tgui.html') + // Inject inline polyfills + var/polyfill = file2text('tgui/public/tgui-polyfill.min.js') + polyfill = "" + basehtml = replacetextEx(basehtml, "", polyfill) /datum/controller/subsystem/tgui/Shutdown() close_all_uis() -/datum/controller/subsystem/tgui/get_stat_details() - return "P:[length(processing_uis)]" - -/datum/controller/subsystem/tgui/get_metrics() - . = ..() - var/list/cust = list() - cust["processing"] = length(processing_uis) - .["custom"] = cust +/datum/controller/subsystem/tgui/stat_entry(msg) + msg = "P:[length(open_uis)]" + return ..() - -/datum/controller/subsystem/tgui/fire(resumed = 0) +/datum/controller/subsystem/tgui/fire(resumed = FALSE) if(!resumed) - src.currentrun = processing_uis.Copy() - //cache for sanic speed (lists are references anyways) - var/list/currentrun = src.currentrun - - while(currentrun.len) - var/datum/tgui/ui = currentrun[currentrun.len] - currentrun.len-- + src.current_run = open_uis.Copy() + // Cache for sanic speed (lists are references anyways) + var/list/current_run = src.current_run + while(current_run.len) + var/datum/tgui/ui = current_run[current_run.len] + current_run.len-- + // TODO: Move user/src_object check to process() if(ui && ui.user && ui.src_object) ui.process() else - processing_uis.Remove(ui) + open_uis.Remove(ui) if(MC_TICK_CHECK) return /** * public * - * Get an open UI given a user, src_object, and ui_key and try to update it with data. - * Returns the found UI. + * Requests a usable tgui window from the pool. + * Returns null if pool was exhausted. * - * * mob/user - The mob who opened/is using the UI. (REQUIRED) - * * datum/src_object - The object/datum which owns the UI. (REQUIRED) - * * ui_key - The ui_key of the UI. (REQUIRED) - * * datum/tgui/ui - The UI to be updated, if it exists. (OPTIONAL) - * * force_open - If the UI should be re-opened instead of updated. (OPTIONAL) + * required user mob + * return datum/tgui */ -/datum/controller/subsystem/tgui/proc/try_update_ui(mob/user, datum/src_object, ui_key, datum/tgui/ui, force_open = FALSE) - if(isnull(ui)) // No UI was passed, so look for one. - ui = get_open_ui(user, src_object, ui_key) - - if(!isnull(ui)) - var/data = src_object.ui_data(user) // Get data from the src_object. - if(!force_open) // UI is already open; update it. - ui.push_data(data) - else // Re-open it anyways. - ui.reinitialize(null, data) - return ui // We found the UI, return it. - else - return null // We couldn't find a UI. +/datum/controller/subsystem/tgui/proc/request_pooled_window(mob/user) + if(!user.client) + return null + var/list/windows = user.client.tgui_windows + var/window_id + var/datum/tgui_window/window + var/window_found = FALSE + // Find a usable window + for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT) + window_id = TGUI_WINDOW_ID(i) + window = windows[window_id] + // As we are looping, create missing window datums + if(!window) + window = new(user.client, window_id, pooled = TRUE) + // Skip windows with acquired locks + if(window.locked) + continue + if(window.status == TGUI_WINDOW_READY) + return window + if(window.status == TGUI_WINDOW_CLOSED) + window.status = TGUI_WINDOW_LOADING + window_found = TRUE + break + if(!window_found) + log_tgui(user, "Error: Pool exhausted") + return null + return window /** - * private + * public * - * Get an open UI given a user, src_object, and ui_key. - * Returns the found UI. + * Force closes all tgui windows. * - * * mob/user - The mob who opened/is using the UI. (REQUIRED) - * * datum/src_object - The object/datum which owns the UI. (REQUIRED) - * * ui_key - The ui_key of the UI. (REQUIRED) + * required user mob */ -/datum/controller/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object, ui_key) - var/src_object_key = "[src_object.UID()]" - if(isnull(open_uis[src_object_key]) || !islist(open_uis[src_object_key])) - return null // No UIs open. - else if(isnull(open_uis[src_object_key][ui_key]) || !islist(open_uis[src_object_key][ui_key])) - return null // No UIs open for this object. - - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) // Find UIs for this object. - if(ui.user == user) // Make sure we have the right user - return ui - - return null // Couldn't find a UI! +/datum/controller/subsystem/tgui/proc/force_close_all_windows(mob/user) + log_tgui(user, "force_close_all_windows") + if(user.client) + user.client.tgui_windows = list() + for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT) + var/window_id = TGUI_WINDOW_ID(i) + user << browse(null, "window=[window_id]") /** - * private + * public * - * Update all UIs attached to src_object. - * Returns the number of UIs updated. + * Force closes the tgui window by window_id. * - * * datum/src_object - The object/datum which owns the UIs. - * * update_static_data - If the static data of the `src_object` should be updated for every viewing user. + * required user mob + * required window_id string */ -/datum/controller/subsystem/tgui/proc/update_uis(datum/src_object, update_static_data = FALSE) - var/src_object_key = "[src_object.UID()]" - if(isnull(open_uis[src_object_key]) || !islist(open_uis[src_object_key])) - return 0 // Couldn't find any UIs for this object. - - var/update_count = 0 - for(var/ui_key in open_uis[src_object_key]) - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid. - if(update_static_data) - src_object.update_static_data(ui.user, ui, ui_key) - ui.process(force = 1) // Update the UI. - update_count++ // Count each UI we update. - return update_count +/datum/controller/subsystem/tgui/proc/force_close_window(mob/user, window_id) + log_tgui(user, "force_close_window") + // Close all tgui datums based on window_id. + for(var/datum/tgui/ui in user.tgui_open_uis) + if(ui.window && ui.window.id == window_id) + ui.close(can_be_suspended = FALSE) + // Unset machine just to be sure. + user.unset_machine() + // Close window directly just to be sure. + user << browse(null, "window=[window_id]") /** - * private + * public * - * Close all UIs attached to src_object. - * Returns the number of UIs closed. + * Try to find an instance of a UI, and push an update to it. + * + * required user mob The mob who opened/is using the UI. + * required src_object datum The object/datum which owns the UI. + * optional ui datum/tgui The UI to be updated, if it exists. + * optional force_open bool If the UI should be re-opened instead of updated. * - * * datum/src_object - The object/datum which owns the UIs. + * return datum/tgui The found UI. */ -/datum/controller/subsystem/tgui/proc/close_uis(datum/src_object) - if(!src_object.unique_datum_id) // First check if the datum has an UID set - return 0 - var/src_object_key = "[src_object.UID()]" - if(isnull(open_uis[src_object_key]) || !islist(open_uis[src_object_key])) - return 0 // Couldn't find any UIs for this object. - - var/close_count = 0 - for(var/ui_key in open_uis[src_object_key]) - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid. - ui.close() // Close the UI. - close_count++ // Count each UI we close. - return close_count +/datum/controller/subsystem/tgui/proc/try_update_ui( + mob/user, + datum/src_object, + datum/tgui/ui, + force_open = FALSE) + // Look up a UI if it wasn't passed + if(isnull(ui)) + ui = get_open_ui(user, src_object) + // Couldn't find a UI. + if(isnull(ui)) + return null + var/data = src_object.ui_data(user) // Get data from the src_object. + if(force_open) // UI is already open; update it. + ui.send_full_update(data, TRUE) + return ui // We found the UI, return it + ui.process_status() + // UI ended up with the closed status + // or is actively trying to close itself. + // FIXME: Doesn't actually fix the paper bug. + if(ui.status <= UI_CLOSE) + ui.close() + return null + ui.send_update() + return ui /** + * public + * + * Get a open UI given a user and src_object. * - * Gets the amount of open UIs on an object - * Returns the number of UIs open. + * required user mob The mob who opened/is using the UI. + * required src_object datum The object/datum which owns the UI. * - * * datum/src_object - The object/datum which owns the UIs. + * return datum/tgui The found UI. */ -/datum/controller/subsystem/tgui/proc/get_open_ui_count(datum/src_object) - if(!src_object.unique_datum_id) // First check if the datum has an UID set - return 0 - var/src_object_key = "[src_object.UID()]" - if(isnull(open_uis[src_object_key]) || !islist(open_uis[src_object_key])) - return 0 // Couldn't find any UIs for this object. - - var/open_count = 0 - for(var/ui_key in open_uis[src_object_key]) - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid. - open_count++ // Count each UI thats open +/datum/controller/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object) + var/key = "[src_object.UID()]" + // No UIs opened for this src_object + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return null + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Make sure we have the right user + if(ui.user == user) + return ui + return null - return open_count +/** + * public + * + * Update all UIs attached to src_object. + * + * required src_object datum The object/datum which owns the UIs. + * + * return int The number of UIs updated. + */ +/datum/controller/subsystem/tgui/proc/update_uis(datum/src_object) + var/count = 0 + var/key = "[src_object.UID()]" + // No UIs opened for this src_object + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return count + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Check if UI is valid. + if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.process(force = TRUE) + count++ + return count +/** + * public + * + * Close all UIs attached to src_object. + * + * required src_object datum The object/datum which owns the UIs. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/close_uis(datum/src_object) + var/count = 0 + var/key = "[src_object.UID()]" + // No UIs opened for this src_object + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return count + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Check if UI is valid. + if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.close() + count++ + return count /** - * private + * public + * + * Close all UIs regardless of their attachment to src_object. * - * Close *ALL* UIs - * Returns the number of UIs closed. + * return int The number of UIs closed. */ /datum/controller/subsystem/tgui/proc/close_all_uis() - var/close_count = 0 - for(var/src_object_key in open_uis) - for(var/ui_key in open_uis[src_object_key]) - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid. - ui.close() // Close the UI. - close_count++ // Count each UI we close. - return close_count + var/count = 0 + for(var/key in open_uis_by_src) + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Check if UI is valid. + if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.close() + count++ + return count /** - * private + * public * * Update all UIs belonging to a user. - * Returns the number of UIs updated. * - * * mob/user - The mob who opened/is using the UI. (REQUIRED) - * * datum/src_object - If provided, only update UIs belonging this src_object. (OPTIONAL) - * * ui_key - If provided, only update UIs with this UI key. (OPTIONAL) + * required user mob The mob who opened/is using the UI. + * optional src_object datum If provided, only update UIs belonging this src_object. + * + * return int The number of UIs updated. */ -/datum/controller/subsystem/tgui/proc/update_user_uis(mob/user, datum/src_object = null, ui_key = null) - if(isnull(user.open_uis) || !islist(user.open_uis) || open_uis.len == 0) - return 0 // Couldn't find any UIs for this user. - - var/update_count = 0 - for(var/datum/tgui/ui in user.open_uis) - if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key)) - ui.process(force = 1) // Update the UI. - update_count++ // Count each UI we upadte. - return update_count +/datum/controller/subsystem/tgui/proc/update_user_uis(mob/user, datum/src_object) + var/count = 0 + if(length(user?.tgui_open_uis) == 0) + return count + for(var/datum/tgui/ui in user.tgui_open_uis) + if(isnull(src_object) || ui.src_object == src_object) + ui.process(force = TRUE) + count++ + return count /** - * private + * public * * Close all UIs belonging to a user. - * Returns the number of UIs closed. * - * * mob/user - The mob who opened/is using the UI. (REQUIRED) - * * datum/src_object - If provided, only close UIs belonging this src_object. (OPTIONAL) - * * ui_key - If provided, only close UIs with this UI key. (OPTIONAL) + * required user mob The mob who opened/is using the UI. + * optional src_object datum If provided, only close UIs belonging this src_object. + * + * return int The number of UIs closed. */ -/datum/controller/subsystem/tgui/proc/close_user_uis(mob/user, datum/src_object = null, ui_key = null) - if(isnull(user.open_uis) || !islist(user.open_uis) || open_uis.len == 0) - return 0 // Couldn't find any UIs for this user. - - var/close_count = 0 - for(var/datum/tgui/ui in user.open_uis) - if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key)) - ui.close() // Close the UI. - close_count++ // Count each UI we close. - return close_count +/datum/controller/subsystem/tgui/proc/close_user_uis(mob/user, datum/src_object) + var/count = 0 + if(length(user?.tgui_open_uis) == 0) + return count + for(var/datum/tgui/ui in user.tgui_open_uis) + if(isnull(src_object) || ui.src_object == src_object) + ui.close() + count++ + return count /** * private * * Add a UI to the list of open UIs. * - * * datum/tgui/ui - The UI to be added. + * required ui datum/tgui The UI to be added. */ /datum/controller/subsystem/tgui/proc/on_open(datum/tgui/ui) - var/src_object_key = "[ui.src_object.UID()]" - if(isnull(open_uis[src_object_key]) || !islist(open_uis[src_object_key])) - open_uis[src_object_key] = list(ui.ui_key = list()) // Make a list for the ui_key and src_object. - else if(isnull(open_uis[src_object_key][ui.ui_key]) || !islist(open_uis[src_object_key][ui.ui_key])) - open_uis[src_object_key][ui.ui_key] = list() // Make a list for the ui_key. - - // Append the UI to all the lists. - ui.user.open_uis |= ui - var/list/uis = open_uis[src_object_key][ui.ui_key] + var/key = "[ui.src_object.UID()]" + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + open_uis_by_src[key] = list() + ui.user.tgui_open_uis |= ui + var/list/uis = open_uis_by_src[key] uis |= ui - processing_uis |= ui + open_uis |= ui /** * private * * Remove a UI from the list of open UIs. - * Returns TRUE if removed, and FALSE if not. * - * * datum/tgui/ui - The UI to be removed. + * required ui datum/tgui The UI to be removed. + * + * return bool If the UI was removed or not. */ /datum/controller/subsystem/tgui/proc/on_close(datum/tgui/ui) - var/src_object_key = "[ui.src_object.UID()]" - if(isnull(open_uis[src_object_key]) || !islist(open_uis[src_object_key])) - return FALSE // It wasn't open. - else if(isnull(open_uis[src_object_key][ui.ui_key]) || !islist(open_uis[src_object_key][ui.ui_key])) - return FALSE // It wasn't open. - - processing_uis.Remove(ui) // Remove it from the list of processing UIs. - if(ui.user) // If the user exists, remove it from them too. - ui.user.open_uis.Remove(ui) - var/Ukey = ui.ui_key - var/list/uis = open_uis[src_object_key][Ukey] // Remove it from the list of open UIs. + var/key = "[ui.src_object.UID()]" + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return FALSE + // Remove it from the list of processing UIs. + open_uis.Remove(ui) + // If the user exists, remove it from them too. + if(ui.user) + ui.user.tgui_open_uis.Remove(ui) + var/list/uis = open_uis_by_src[key] uis.Remove(ui) - if(!uis.len) - var/list/uiobj = open_uis[src_object_key] - uiobj.Remove(Ukey) - if(!uiobj.len) - open_uis.Remove(src_object_key) - - return TRUE // Let the caller know we did it. + if(length(uis) == 0) + open_uis_by_src.Remove(key) + return TRUE /** * private * * Handle client logout, by closing all their UIs. - * Returns the number of UIs closed. * - * * mob/user - The mob which logged out. + * required user mob The mob which logged out. + * + * return int The number of UIs closed. */ /datum/controller/subsystem/tgui/proc/on_logout(mob/user) - return close_user_uis(user) + close_user_uis(user) /** * private * * Handle clients switching mobs, by transferring their UIs. - * Returns TRUE if the UIs were transferred, and FALSE if not. * - * * mob/source - The client's original mob. - * * mob/target - The client's new mob. + * required user source The client's original mob. + * required user target The client's new mob. + * + * return bool If the UIs were transferred. */ /datum/controller/subsystem/tgui/proc/on_transfer(mob/source, mob/target) - if(!source || isnull(source.open_uis) || !islist(source.open_uis) || open_uis.len == 0) - return FALSE // The old mob had no open UIs. - - if(isnull(target.open_uis) || !islist(target.open_uis)) - target.open_uis = list() // Create a list for the new mob if needed. - - for(var/datum/tgui/ui in source.open_uis) - ui.user = target // Inform the UIs of their new owner. - target.open_uis.Add(ui) // Transfer all the UIs. - - source.open_uis.Cut() // Clear the old list. - return TRUE // Let the caller know we did it. + // The old mob had no open UIs. + if(length(source?.tgui_open_uis) == 0) + return FALSE + if(isnull(target.tgui_open_uis) || !istype(target.tgui_open_uis, /list)) + target.tgui_open_uis = list() + // Transfer all the UIs. + for(var/datum/tgui/ui in source.tgui_open_uis) + // Inform the UIs of their new owner. + ui.user = target + target.tgui_open_uis.Add(ui) + // Clear the old list. + source.tgui_open_uis.Cut() + return TRUE diff --git a/code/controllers/subsystem/SSticker.dm b/code/controllers/subsystem/SSticker.dm index 0e99b37bcd5d..02d604363304 100644 --- a/code/controllers/subsystem/SSticker.dm +++ b/code/controllers/subsystem/SSticker.dm @@ -296,9 +296,25 @@ SUBSYSTEM_DEF(ticker) // Generate code phrases and responses if(!GLOB.syndicate_code_phrase) - GLOB.syndicate_code_phrase = generate_code_phrase() + var/temp_syndicate_code_phrase = generate_code_phrase(return_list = TRUE) + + var/codewords = jointext(temp_syndicate_code_phrase, "|") + var/regex/codeword_match = new("([codewords])", "ig") + + GLOB.syndicate_code_phrase_regex = codeword_match + temp_syndicate_code_phrase = jointext(temp_syndicate_code_phrase, ", ") + GLOB.syndicate_code_phrase = temp_syndicate_code_phrase + + if(!GLOB.syndicate_code_response) - GLOB.syndicate_code_response = generate_code_phrase() + var/temp_syndicate_code_response = generate_code_phrase(return_list=TRUE) + + var/codewords = jointext(temp_syndicate_code_response, "|") + var/regex/codeword_match = new("([codewords])", "ig") + + GLOB.syndicate_code_response_regex = codeword_match + temp_syndicate_code_response = jointext(temp_syndicate_code_response, ", ") + GLOB.syndicate_code_response = temp_syndicate_code_response // Run post setup stuff mode.post_setup() diff --git a/code/controllers/subsystem/non_firing/SSassets.dm b/code/controllers/subsystem/non_firing/SSassets.dm index d2c9a90217df..4f6d5a9a5e10 100644 --- a/code/controllers/subsystem/non_firing/SSassets.dm +++ b/code/controllers/subsystem/non_firing/SSassets.dm @@ -2,15 +2,37 @@ SUBSYSTEM_DEF(assets) name = "Assets" init_order = INIT_ORDER_ASSETS flags = SS_NO_FIRE - var/list/cache = list() + var/list/datum/asset_cache_item/cache = list() var/list/preload = list() + var/datum/asset_transport/transport = new() -/datum/controller/subsystem/assets/Initialize() - for(var/type in typesof(/datum/asset) - list(/datum/asset, /datum/asset/simple)) - var/datum/asset/A = new type() - A.register() +/datum/controller/subsystem/assets/Initialize(timeofday) + load_assets() + apply_configuration() - preload = cache.Copy() //don't preload assets generated during the round +/datum/controller/subsystem/assets/Recover() + cache = SSassets.cache + preload = SSassets.preload - for(var/client/C in GLOB.clients) - addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(getFilesSlow), C, preload, FALSE), 10) +/datum/controller/subsystem/assets/proc/apply_configuration(initialize_transport = TRUE) + var/newtransporttype = /datum/asset_transport + switch(GLOB.configuration.asset_cache.asset_transport) + if("webroot") + newtransporttype = /datum/asset_transport/webroot + + if(newtransporttype == transport.type) + return + + var/datum/asset_transport/newtransport = new newtransporttype + if(newtransport.validate_config()) + transport = newtransport + + if(initialize_transport) + transport.Initialize(cache) + +/datum/controller/subsystem/assets/proc/load_assets() + for(var/datum/asset/asset_to_load as anything in typesof(/datum/asset)) + if(initial(asset_to_load._abstract)) + continue + + get_asset_datum(type) diff --git a/code/controllers/subsystem/non_firing/SSchangelog.dm b/code/controllers/subsystem/non_firing/SSchangelog.dm index cfff6afee409..9c61048239f5 100644 --- a/code/controllers/subsystem/non_firing/SSchangelog.dm +++ b/code/controllers/subsystem/non_firing/SSchangelog.dm @@ -76,22 +76,12 @@ SUBSYSTEM_DEF(changelog) /datum/controller/subsystem/changelog/proc/UpdatePlayerChangelogButton(client/C) // If SQL aint even enabled, or we aint ready just set the button to default style if(!SSdbcore.IsConnected() || !ss_ready) - if(C.prefs.toggles & PREFTOGGLE_UI_DARKMODE) - winset(C, "rpane.changelog", "background-color=#40628a;text-color=#FFFFFF") - else - winset(C, "rpane.changelog", "background-color=none;text-color=#000000") return // If we are ready, process the button style if(C.prefs.lastchangelog != current_cl_timestamp) winset(C, "rpane.changelog", "background-color=#bb7700;text-color=#FFFFFF;font-style=bold") to_chat(C, "Changelog has changed since your last visit.") - else - if(C.prefs.toggles & PREFTOGGLE_UI_DARKMODE) - winset(C, "rpane.changelog", "background-color=#40628a;text-color=#FFFFFF") - else - winset(C, "rpane.changelog", "background-color=none;text-color=#000000") - /datum/controller/subsystem/changelog/proc/OpenChangelog(client/C) // If SQL isnt enabled, dont even queue them, just tell them it wont work @@ -204,10 +194,13 @@ SUBSYSTEM_DEF(changelog) return data -/datum/controller/subsystem/changelog/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.always_state) - ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) +/datum/controller/subsystem/changelog/ui_state(mob/user) + return GLOB.always_state + +/datum/controller/subsystem/changelog/ui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, ui_key, "ChangelogView", name, 750, 800, master_ui, state) + ui = new(user, src, "ChangelogView", name) ui.set_autoupdate(FALSE) ui.open() diff --git a/code/datums/browser.dm b/code/datums/browser.dm index 24d324d6e77f..8a5db47bcec0 100644 --- a/code/datums/browser.dm +++ b/code/datums/browser.dm @@ -4,21 +4,18 @@ var/window_id // window_id is used as the window name for browse and onclose var/width = 0 var/height = 0 - var/atom/ref = null - var/window_options = "focus=0;can_close=1;can_minimize=1;can_maximize=0;can_resize=1;titlebar=1;" // window option is set using window_id + var/atom_uid = null + var/list/window_options = list("focus=0;can_close=1;can_minimize=1;can_maximize=0;can_resize=1;titlebar=1;") // window option is set using window_id var/stylesheets[0] var/scripts[0] - var/title_image - var/head_elements - var/body_elements - var/head_content = "" - var/content = "" - var/title_buttons = "" + var/include_default_stylesheet = TRUE + var/list/head_content = list() + var/list/content = list() -/datum/browser/New(nuser, nwindow_id, ntitle = 0, nwidth = 0, nheight = 0, atom/nref = null) - +/datum/browser/New(nuser, nwindow_id, ntitle = 0, nwidth = 0, nheight = 0, atom/atom = null) user = nuser + RegisterSignal(user, COMSIG_PARENT_QDELETING, PROC_REF(user_deleted)) window_id = nwindow_id if(ntitle) title = format_text(ntitle) @@ -26,64 +23,71 @@ width = nwidth if(nheight) height = nheight - if(nref) - ref = nref - add_stylesheet("common", 'html/browser/common.css') // this CSS sheet is common to all UIs + if(atom) + atom_uid = atom.UID() + +/datum/browser/proc/user_deleted(datum/source) + SIGNAL_HANDLER + user = null /datum/browser/proc/set_title(ntitle) - title = format_text(ntitle) + title = islist(ntitle) ? ntitle : list(ntitle) /datum/browser/proc/add_head_content(nhead_content) - head_content = nhead_content - -/datum/browser/proc/set_title_buttons(ntitle_buttons) - title_buttons = ntitle_buttons + head_content = islist(nhead_content) ? nhead_content : list(nhead_content) /datum/browser/proc/set_window_options(nwindow_options) - window_options = nwindow_options - -/datum/browser/proc/set_title_image(ntitle_image) - //title_image = ntitle_image + window_options = islist(nwindow_options) ? nwindow_options : list(nwindow_options) /datum/browser/proc/add_stylesheet(name, file) - stylesheets[name] = file + if(istype(name, /datum/asset/spritesheet)) + var/datum/asset/spritesheet/sheet = name + stylesheets["spritesheet_[sheet.name].css"] = "data/spritesheets/[sheet.name]" + else + var/asset_name = "[name].css" + + stylesheets[asset_name] = file + + if(!SSassets.cache[asset_name]) + SSassets.transport.register_asset(asset_name, file) + +/datum/browser/proc/add_scss_stylesheet(name, file) + var/asset_name = "[name].scss" + stylesheets[asset_name] = file + + if(!SSassets.cache[asset_name]) + SSassets.transport.register_asset(asset_name, file) /datum/browser/proc/add_script(name, file) - scripts[name] = file + scripts["[ckey(name)].js"] = file + SSassets.transport.register_asset("[ckey(name)].js", file) /datum/browser/proc/set_content(ncontent) - content = ncontent + content = islist(ncontent) ? ncontent : list(ncontent) /datum/browser/proc/add_content(ncontent) content += ncontent /datum/browser/proc/get_header() - var/key - var/filename - for(key in stylesheets) - filename = "[ckey(key)].css" - user << browse_rsc(stylesheets[key], filename) - head_content += "" - - for(key in scripts) - filename = "[ckey(key)].js" - user << browse_rsc(scripts[key], filename) - head_content += "" - - var/title_attributes = "class='uiTitle'" - if(title_image) - title_attributes = "class='uiTitle icon' style='background-image: url([title_image]);'" - - return {" + if(include_default_stylesheet) + head_content += "" + + for(var/file in stylesheets) + head_content += "" + + for(var/file in scripts) + head_content += "" + + return {" - - - [head_content] + + + [head_content.Join("")]" - dat += "" - dat += " | "
- else
- dat += "ERR: Unable to retrieve image data for occupant."
- dat += "Probe " - dat += "Dissect " - dat += "Analyze " - dat += " |