diff --git a/code/__DEFINES/context_flags.dm b/code/__DEFINES/context_flags.dm
new file mode 100644
index 00000000000..94b43de769e
--- /dev/null
+++ b/code/__DEFINES/context_flags.dm
@@ -0,0 +1,7 @@
+#define FLAG_PM "Private Message"
+#define FLAG_SM "Subtle Message"
+#define FLAG_GIB "Gib"
+#define FLAG_JUMP "Jump To"
+#define FLAG_JUMP_GHOST "Jump To As Ghost"
+#define FLAG_PP "Player Panel"
+#define FLAG_VV "View Variables"
diff --git a/code/__HELPERS/files.dm b/code/__HELPERS/files.dm
index 44349ca7318..3b0f75fdf58 100644
--- a/code/__HELPERS/files.dm
+++ b/code/__HELPERS/files.dm
@@ -1,8 +1,3 @@
-//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", "html"))
var/path = root
diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm
index 5b1d03e2793..85c66defafa 100644
--- a/code/__HELPERS/global_lists.dm
+++ b/code/__HELPERS/global_lists.dm
@@ -56,11 +56,10 @@
if(B.allow_random)
GLOB.bark_random_list[B.id] = path
- // Backgrounds
- for (var/path in subtypesof(/datum/background))
- var/datum/background/background = new path()
- GLOB.backgrounds[path] = background
- sortList(GLOB.backgrounds, GLOBAL_PROC_REF(cmp_text_dsc))
+ // Loadout items
+ for (var/path in subtypesof(/datum/loadout_item))
+ var/datum/loadout_item/loadout_item = new path()
+ GLOB.loadout_items[path] = loadout_item
//creates every subtype of prototype (excluding prototype) and adds it to list L.
//if no list/L is provided, one is created.
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index 08d20391770..01f30f772b7 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -1112,6 +1112,11 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
alpha += 25
obj_flags &= ~FROZEN
+/// 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))]"
//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.
@@ -1145,11 +1150,11 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
if (!isicon(I))
if (isfile(thing)) //special snowflake
var/name = sanitize_filename("[generate_asset_name(thing)].png")
- register_asset(name, thing)
+ SSassets.transport.register_asset(name, thing)
for (var/mob/thing2 in targets)
if(!istype(thing2) || !thing2.client)
continue
- send_asset(thing2?.client, key)
+ SSassets.transport.send_assets(thing2?.client, key)
return ""
var/atom/A = thing
if (isnull(dir))
@@ -1171,13 +1176,13 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
I = icon(I, icon_state, dir, frame, moving)
key = "[generate_asset_name(I)].png"
- register_asset(key, I)
+ SSassets.transport.register_asset(key, I)
for (var/mob/thing2 in targets)
if(!istype(thing2) || !thing2.client)
continue
- send_asset(thing2?.client, key)
+ SSassets.transport.send_assets(thing2?.client, key)
- return ""
+ return ""
/proc/icon2base64html(thing)
if (!thing)
diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm
index d13cfc13066..047ad6274a1 100644
--- a/code/__HELPERS/type2type.dm
+++ b/code/__HELPERS/type2type.dm
@@ -561,6 +561,11 @@
else //regex everything else (works for /proc too)
return lowertext(replacetext("[the_type]", "[type2parent(the_type)]/", ""))
+/// 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 {"
"}
+
/proc/strtohex(str)
if(!istext(str)||!str)
return
diff --git a/code/_globalvars/special_traits.dm b/code/_globalvars/special_traits.dm
index ffa2e4b2542..70395b24004 100644
--- a/code/_globalvars/special_traits.dm
+++ b/code/_globalvars/special_traits.dm
@@ -47,6 +47,8 @@ GLOBAL_LIST_INIT(special_traits, build_special_traits())
player = character.client
apply_charflaw_equipment(character, player)
apply_prefs_special(character, player)
+ if(player.prefs.loadout)
+ character.mind.special_items[player.prefs.loadout.name] = player.prefs.loadout.path
/proc/apply_charflaw_equipment(mob/living/carbon/human/character, client/player)
if(character.charflaw)
diff --git a/code/_onclick/hud/mouseover.dm b/code/_onclick/hud/mouseover.dm
index 4c96d34fc59..eee3ca5cc14 100644
--- a/code/_onclick/hud/mouseover.dm
+++ b/code/_onclick/hud/mouseover.dm
@@ -208,5 +208,5 @@
/client/proc/genmouseobj()
mouseovertext = new /atom/movable/screen/movable/mouseover/maptext
mouseoverbox = new /atom/movable/screen/movable/mouseover
- var/datum/asset/stuff = get_asset_datum(/datum/asset/simple/roguefonts)
+ var/datum/asset/stuff = get_asset_datum(/datum/asset/simple/namespaced/roguefonts)
stuff.send(src)
diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm
index 3ee46499080..f1e7a39b0a1 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -53,6 +53,9 @@
LoadPolicy()
LoadChatFilter()
+ if(Master)
+ Master.OnConfigLoad()
+
/datum/controller/configuration/proc/full_wipe()
if(IsAdminAdvancedProcCall())
return
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index 9e6f527b45d..9d5cb28756c 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -623,3 +623,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
processing = CONFIG_GET(number/mc_tick_rate/base_mc_tick_rate)
else if (client_count > CONFIG_GET(number/mc_tick_rate/high_pop_mc_mode_amount))
processing = CONFIG_GET(number/mc_tick_rate/high_pop_mc_tick_rate)
+
+/datum/controller/master/proc/OnConfigLoad()
+ for (var/thing in subsystems)
+ var/datum/controller/subsystem/SS = thing
+ SS.OnConfigLoad()
diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm
index 233dd76709b..834f318f136 100644
--- a/code/controllers/subsystem.dm
+++ b/code/controllers/subsystem.dm
@@ -220,3 +220,6 @@
if ("queued_priority") //editing this breaks things.
return 0
. = ..()
+
+/// Called after the config has been loaded or reloaded.
+/datum/controller/subsystem/proc/OnConfigLoad()
diff --git a/code/controllers/subsystem/assets.dm b/code/controllers/subsystem/assets.dm
index dfe6438d79f..9893502efc6 100644
--- a/code/controllers/subsystem/assets.dm
+++ b/code/controllers/subsystem/assets.dm
@@ -4,6 +4,21 @@ SUBSYSTEM_DEF(assets)
flags = SS_NO_FIRE
var/list/cache = list()
var/list/preload = list()
+ var/datum/asset_transport/transport = new()
+
+/datum/controller/subsystem/assets/OnConfigLoad()
+ var/newtransporttype = /datum/asset_transport
+ switch (CONFIG_GET(string/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
+ transport.Load()
/datum/controller/subsystem/assets/Initialize(timeofday)
for(var/type in typesof(/datum/asset))
@@ -11,8 +26,5 @@ SUBSYSTEM_DEF(assets)
if (type != initial(A._abstract))
get_asset_datum(type)
- preload = cache.Copy() //don't preload assets generated during the round
-
- for(var/client/C in GLOB.clients)
- addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(getFilesSlow), C, preload, FALSE), 10)
- ..()
+ transport.Initialize(cache)
+ return ..()
diff --git a/code/datums/browser.dm b/code/datums/browser.dm
index 0400fbf8698..37c8c4c85ad 100644
--- a/code/datums/browser.dm
+++ b/code/datums/browser.dm
@@ -8,12 +8,12 @@
var/window_options = "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/no_close_movement = FALSE
+ var/static/datum/asset/simple/namespaced/common/common_asset = get_asset_datum(/datum/asset/simple/namespaced/common)
/datum/browser/Destroy(force, ...)
. = ..()
@@ -45,7 +45,6 @@
if (nref)
ref = nref
RegisterSignal(ref, COMSIG_PARENT_QDELETING, PROC_REF(ref_deleted))
- add_stylesheet("common", 'html/browser/common.css') // this CSS sheet is common to all UIs
/datum/browser/proc/user_deleted(datum/source)
SIGNAL_HANDLER
@@ -61,9 +60,6 @@
/datum/browser/proc/set_window_options(nwindow_options)
window_options = nwindow_options
-/datum/browser/proc/set_title_image(ntitle_image)
- title_image = ntitle_image
-
/datum/browser/proc/add_stylesheet(name, file)
if (istype(name, /datum/asset/spritesheet))
var/datum/asset/spritesheet/sheet = name
@@ -74,11 +70,11 @@
stylesheets[asset_name] = file
if (!SSassets.cache[asset_name])
- register_asset(asset_name, file)
+ SSassets.transport.register_asset(asset_name, file)
/datum/browser/proc/add_script(name, file)
scripts["[ckey(name)].js"] = file
- register_asset("[ckey(name)].js", file)
+ SSassets.transport.register_asset("[ckey(name)].js", file)
/datum/browser/proc/set_content(ncontent)
content = ncontent
@@ -87,16 +83,14 @@
content += ncontent
/datum/browser/proc/get_header()
+ var/datum/asset/simple/namespaced/common/common_asset = get_asset_datum(/datum/asset/simple/namespaced/common)
var/file
+ head_content += ""
for (file in stylesheets)
- head_content += ""
+ head_content += ""
for (file in scripts)
- head_content += ""
-
- var/title_attributes = "class='uiTitle'"
- if (title_image)
- title_attributes = "class='uiTitle icon' style='background-image: url([title_image]);'"
+ head_content += ""
return {"
@@ -107,7 +101,7 @@
- [title ? "
[title]
" : ""]
+ [title ? "
[title]
" : ""]
"}
//" This is here because else the rest of the file looks like a string in notepad++.
@@ -133,10 +127,11 @@
var/window_size = ""
if (width && height)
window_size = "size=[width]x[height];"
+ common_asset.send(user)
if (stylesheets.len)
- send_asset_list(user, stylesheets, verify=FALSE)
+ SSassets.transport.send_assets(user, stylesheets)
if (scripts.len)
- send_asset_list(user, scripts, verify=FALSE)
+ SSassets.transport.send_assets(user, scripts)
user << browse(get_content(), "window=[window_id];[window_size][window_options]")
if (use_onclose)
setup_onclose()
@@ -445,12 +440,6 @@
if (A.selectedbutton)
return list("button" = A.selectedbutton, "settings" = A.settings)
-// This will allow you to show an icon in the browse window
-// This is added to mob so that it can be used without a reference to the browser object
-// There is probably a better place for this...
-/mob/proc/browse_rsc_icon(icon, icon_state, dir = -1)
-
-
// Registers the on-close verb for a browse window (client/verb/.windowclose)
// this will be called when the close-button of a window is pressed.
//
diff --git a/code/game/objects/items/toys.dm b/code/game/objects/items/toys.dm
index 8d724f567fb..8af31da4179 100644
--- a/code/game/objects/items/toys.dm
+++ b/code/game/objects/items/toys.dm
@@ -240,7 +240,6 @@
dat += "A [t]. "
dat += "Which card will you remove next?"
var/datum/browser/popup = new(user, "cardhand", "Hand of Cards", 400, 240)
- popup.set_title_image(user.browse_rsc_icon(src.icon, src.icon_state))
popup.set_content(dat)
popup.open()
diff --git a/code/game/world.dm b/code/game/world.dm
index 8a2c123ea8b..cceec035bfd 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -39,8 +39,6 @@ GLOBAL_VAR(restart_counter)
log_world("World loaded at [time_stamp()]!")
- SetupExternalRSC()
-
GLOB.config_error_log = GLOB.world_manifest_log = GLOB.world_pda_log = GLOB.world_job_debug_log = GLOB.sql_error_log = GLOB.world_href_log = GLOB.world_runtime_log = GLOB.world_attack_log = GLOB.world_game_log = "data/logs/config_error.[GUID()].log" //temporary file used to record errors with loading config, moved to log directory once logging is set bl
make_datum_references_lists() //initialises global lists for referencing frequently used datums (so that we only ever do it once)
@@ -118,17 +116,6 @@ GLOBAL_VAR(restart_counter)
#endif
SSticker.OnRoundstart(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(addtimer), cb, 10 SECONDS))
-/world/proc/SetupExternalRSC()
-#if (PRELOAD_RSC == 0)
- GLOB.external_rsc_urls = world.file2list("[global.config.directory]/external_rsc_urls.txt","\n")
- var/i=1
- while(i<=GLOB.external_rsc_urls.len)
- if(GLOB.external_rsc_urls[i])
- i++
- else
- GLOB.external_rsc_urls.Cut(i,i+1)
-#endif
-
/world/proc/SetupLogs()
var/override_dir = params[OVERRIDE_LOG_DIRECTORY_PARAMETER]
if(!override_dir)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 7dbdf5a43ee..a5b00e343c7 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -149,7 +149,8 @@ GLOBAL_PROTECT(admin_verbs_server)
/client/proc/forcerandomrotate,
/client/proc/adminchangemap,
/client/proc/panicbunker,
- /client/proc/toggle_hub
+ /client/proc/toggle_hub,
+ /client/proc/toggle_cdn
)
GLOBAL_LIST_INIT(admin_verbs_debug, world.AVerbsDebug())
GLOBAL_PROTECT(admin_verbs_debug)
diff --git a/code/modules/admin/permissionedit.dm b/code/modules/admin/permissionedit.dm
index f3ac048316d..4a59e0d3c29 100644
--- a/code/modules/admin/permissionedit.dm
+++ b/code/modules/admin/permissionedit.dm
@@ -9,7 +9,9 @@
/datum/admins/proc/edit_admin_permissions(action, target, operation, page)
if(!check_rights(R_PERMISSIONS))
return
- var/list/output = list("\[Permissions\]")
+ var/datum/asset/asset_cache_datum = get_asset_datum(/datum/asset/group/permissions)
+ asset_cache_datum.send(usr)
+ var/list/output = list("\[Permissions\]")
if(action)
output += " | \[Log\] | \[Management\]"
else
@@ -92,7 +94,7 @@
Permissions Panel
-
+
")
if(severity)
- data += " "
+ data += " "
data += "[timestamp] | [server] | [admin_key][secret ? " | - Secret" : ""]"
if(expire_timestamp)
data += " | Expires [expire_timestamp]"
diff --git a/code/modules/admin/verbs/beakerpanel.dm b/code/modules/admin/verbs/beakerpanel.dm
index ec690001561..9dd7f4e90ea 100644
--- a/code/modules/admin/verbs/beakerpanel.dm
+++ b/code/modules/admin/verbs/beakerpanel.dm
@@ -41,13 +41,16 @@
if(!check_rights())
return
+ var/datum/asset/asset_datum = get_asset_datum(/datum/asset/simple/namespaced/common)
+ asset_datum.send()
+ //Could somebody tell me why this isn't using the browser datum, given that it copypastes all of browser datum's html
var/dat = {"
-
+
diff --git a/code/modules/admin/verbs/diagnostics.dm b/code/modules/admin/verbs/diagnostics.dm
index c8b67cf9672..32f53eb440b 100644
--- a/code/modules/admin/verbs/diagnostics.dm
+++ b/code/modules/admin/verbs/diagnostics.dm
@@ -70,3 +70,31 @@
load_admins()
SSblackbox.record_feedback("tally", "admin_verb", 1, "Reload All Admins") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
message_admins("[key_name_admin(usr)] manually reloaded admins")
+
+/client/proc/toggle_cdn()
+ set name = "Toggle CDN"
+ set category = "Server"
+ var/static/admin_disabled_cdn_transport = null
+ if (alert(usr, "Are you sure you want to toggle the CDN asset transport?", "Confirm", "Yes", "No") != "Yes")
+ return
+ var/current_transport = CONFIG_GET(string/asset_transport)
+ if (!current_transport || current_transport == "simple")
+ if (admin_disabled_cdn_transport)
+ CONFIG_SET(string/asset_transport, admin_disabled_cdn_transport)
+ admin_disabled_cdn_transport = null
+ SSassets.OnConfigLoad()
+ message_admins("[key_name_admin(usr)] re-enabled the CDN asset transport")
+ log_admin("[key_name(usr)] re-enabled the CDN asset transport")
+ else
+ to_chat(usr, "The CDN is not enabled!")
+ if (alert(usr, "The CDN asset transport is not enabled! If you having issues with assets you can also try disabling filename mutations.", "The CDN asset transport is not enabled!", "Try disabling filename mutations", "Nevermind") == "Try disabling filename mutations")
+ SSassets.transport.dont_mutate_filenames = !SSassets.transport.dont_mutate_filenames
+ message_admins("[key_name_admin(usr)] [(SSassets.transport.dont_mutate_filenames ? "disabled" : "re-enabled")] asset filename transforms")
+ log_admin("[key_name(usr)] [(SSassets.transport.dont_mutate_filenames ? "disabled" : "re-enabled")] asset filename transforms")
+ else
+ admin_disabled_cdn_transport = current_transport
+ CONFIG_SET(string/asset_transport, "simple")
+ SSassets.OnConfigLoad()
+ SSassets.transport.dont_mutate_filenames = TRUE
+ message_admins("[key_name_admin(usr)] disabled the CDN asset transport")
+ log_admin("[key_name(usr)] disabled the CDN asset transport")
diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm
index a8f792aa905..7a42940dab6 100644
--- a/code/modules/admin/verbs/playsound.dm
+++ b/code/modules/admin/verbs/playsound.dm
@@ -231,6 +231,7 @@ GLOBAL_LIST_INIT(ambience_files, list(
'sound/music/area/bog.ogg',
'sound/music/area/caves.ogg',
'sound/music/area/church.ogg',
+ 'sound/music/area/13angels.ogg',
'sound/music/area/dwarf.ogg',
'sound/music/area/field.ogg',
'sound/music/area/magiciantower.ogg',
diff --git a/code/modules/admin/view_variables/view_variables.dm b/code/modules/admin/view_variables/view_variables.dm
index 00c5fbe89f9..01ae0ec8aba 100644
--- a/code/modules/admin/view_variables/view_variables.dm
+++ b/code/modules/admin/view_variables/view_variables.dm
@@ -4,8 +4,8 @@
//set src in world
var/static/cookieoffset = rand(1, 9999) //to force cookies to reset after the round.
- if(!usr.client || !usr.client.holder) //This is usr because admins can call the proc on other clients, even if they're not admins, to show them VVs.
- to_chat(usr, span_danger("I need to be an administrator to access this."))
+ if(!usr.client || !usr.client.holder) //This is usr because admins can call the proc on other clients, even if they're not admins, to show them VVs.
+ to_chat(usr, span_danger("You need to be an administrator to access this."))
return
if(!D)
@@ -39,6 +39,8 @@
sprite_text = no_icon? "\[NO ICON\]" : "
"
var/list/header = islist(D)? list("/list") : D.vv_get_header()
+ var/ref_line = "@[copytext(refid, 2, -1)]" // get rid of the brackets, add a @ prefix for copy pasting in asay
+
var/marked_line
if(holder && holder.marked_datum && holder.marked_datum == D)
marked_line = VV_MSG_MARKED
@@ -72,7 +74,7 @@
if(!islist)
for(var/V in D.vars)
names += V
- sleep(1)
+ sleep(1 TICKS)
var/list/variable_html = list()
if(islist)
@@ -92,17 +94,9 @@
var/html = {"
+
[title]
-
+
"}, "window=asset_cache_browser&file=asset_cache_send_verify.htm")
+ else
+ src << browse({""}, "window=asset_cache_browser&file=asset_cache_send_verify.htm")
+
+ while(!completed_asset_jobs["[job]"] && t < timeout_time) // Reception is handled in Topic()
+ stoplag(1) // Lock up the caller until this is received.
+ t++
+ if (t < timeout_time)
+ return TRUE
diff --git a/code/modules/asset_cache/asset_configs.dm b/code/modules/asset_cache/asset_configs.dm
new file mode 100644
index 00000000000..c839ccc078d
--- /dev/null
+++ b/code/modules/asset_cache/asset_configs.dm
@@ -0,0 +1,30 @@
+/datum/config_entry/keyed_list/external_rsc_urls
+ key_mode = KEY_MODE_TEXT
+ value_mode = VALUE_MODE_FLAG
+
+/datum/config_entry/flag/asset_simple_preload
+
+/datum/config_entry/string/asset_transport
+/datum/config_entry/string/asset_transport/ValidateAndSet(str_val)
+ return (lowertext(str_val) in list("simple", "webroot")) && ..(lowertext(str_val))
+
+/datum/config_entry/string/asset_cdn_webroot
+ protection = CONFIG_ENTRY_LOCKED
+
+/datum/config_entry/string/asset_cdn_webroot/ValidateAndSet(str_var)
+ if (!str_var || trim(str_var) == "")
+ return FALSE
+ if (str_var && str_var[length(str_var)] != "/")
+ str_var += "/"
+ return ..(str_var)
+
+/datum/config_entry/string/asset_cdn_url
+ protection = CONFIG_ENTRY_LOCKED
+ default = null
+
+/datum/config_entry/string/asset_cdn_url/ValidateAndSet(str_var)
+ if (!str_var || trim(str_var) == "")
+ return FALSE
+ if (str_var && str_var[length(str_var)] != "/")
+ str_var += "/"
+ return ..(str_var)
diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm
new file mode 100644
index 00000000000..82456abb107
--- /dev/null
+++ b/code/modules/asset_cache/asset_list.dm
@@ -0,0 +1,307 @@
+
+//These datums are used to populate the asset cache, the proc "register()" does this.
+//Place any asset datums you create in asset_list_items.dm
+
+//all of our asset datums, used for referring to these later
+GLOBAL_LIST_EMPTY(asset_datums)
+
+//get an assetdatum or make a new one
+/proc/get_asset_datum(type)
+ return GLOB.asset_datums[type] || new type()
+
+/datum/asset
+ var/_abstract = /datum/asset
+
+/datum/asset/New()
+ GLOB.asset_datums[type] = src
+ register()
+
+/datum/asset/proc/get_url_mappings()
+ return list()
+
+/datum/asset/proc/register()
+ return
+
+/datum/asset/proc/send(client)
+ return
+
+
+//If you don't need anything complicated.
+/datum/asset/simple
+ _abstract = /datum/asset/simple
+ var/assets = list() //! list of assets for this datum in the form of asset_filename = asset_file. At runtime the asset_file will be converted into a asset_cache datum.
+ var/legacy = FALSE //! set to true to have this asset also be sent via browse_rsc when cdn asset transports are enabled.
+
+/datum/asset/simple/register()
+ for(var/asset_name in assets)
+ var/datum/asset_cache_item/ACI = SSassets.transport.register_asset(asset_name, assets[asset_name])
+ if (!ACI)
+ log_asset("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
+ continue
+ if (legacy)
+ ACI.legacy = TRUE
+ assets[asset_name] = ACI
+
+/datum/asset/simple/send(client)
+ . = SSassets.transport.send_assets(client, assets)
+
+/datum/asset/simple/get_url_mappings()
+ . = list()
+ for (var/asset_name in assets)
+ .[asset_name] = SSassets.transport.get_asset_url(asset_name, assets[asset_name])
+
+
+// For registering or sending multiple others at once
+/datum/asset/group
+ _abstract = /datum/asset/group
+ var/list/children
+
+/datum/asset/group/register()
+ for(var/type in children)
+ get_asset_datum(type)
+
+/datum/asset/group/send(client/C)
+ for(var/type in children)
+ var/datum/asset/A = get_asset_datum(type)
+ . = A.send(C) || .
+
+/datum/asset/group/get_url_mappings()
+ . = list()
+ for(var/type in children)
+ var/datum/asset/A = get_asset_datum(type)
+ . += A.get_url_mappings()
+
+// spritesheet implementation - coalesces various icons into a single .png file
+// and uses CSS to select icons out of that file - saves on transferring some
+// 1400-odd individual PNG files
+#define SPR_SIZE 1
+#define SPR_IDX 2
+#define SPRSZ_COUNT 1
+#define SPRSZ_ICON 2
+#define SPRSZ_STRIPPED 3
+
+/datum/asset/spritesheet
+ _abstract = /datum/asset/spritesheet
+ var/name
+ var/list/sizes = list() // "32x32" -> list(10, icon/normal, icon/stripped)
+ var/list/sprites = list() // "foo_bar" -> list("32x32", 5)
+
+/datum/asset/spritesheet/register()
+ if (!name)
+ CRASH("spritesheet [type] cannot register without a name")
+ ensure_stripped()
+ for(var/size_id in sizes)
+ var/size = sizes[size_id]
+ SSassets.transport.register_asset("[name]_[size_id].png", size[SPRSZ_STRIPPED])
+ var/res_name = "spritesheet_[name].css"
+ var/fname = "data/spritesheets/[res_name]"
+ fdel(fname)
+ text2file(generate_css(), fname)
+ SSassets.transport.register_asset(res_name, fcopy_rsc(fname))
+ fdel(fname)
+
+/datum/asset/spritesheet/send(client/C)
+ if (!name)
+ return
+ var/all = list("spritesheet_[name].css")
+ for(var/size_id in sizes)
+ all += "[name]_[size_id].png"
+ . = SSassets.transport.send_assets(C, all)
+
+/datum/asset/spritesheet/get_url_mappings()
+ if (!name)
+ return
+ . = list("spritesheet_[name].css" = SSassets.transport.get_asset_url("spritesheet_[name].css"))
+ for(var/size_id in sizes)
+ .["[name]_[size_id].png"] = SSassets.transport.get_asset_url("[name]_[size_id].png")
+
+
+
+/datum/asset/spritesheet/proc/ensure_stripped(sizes_to_strip = sizes)
+ for(var/size_id in sizes_to_strip)
+ var/size = sizes[size_id]
+ if (size[SPRSZ_STRIPPED])
+ continue
+
+ // save flattened version
+ var/fname = "data/spritesheets/[name]_[size_id].png"
+ fcopy(size[SPRSZ_ICON], fname)
+ var/error = rustg_dmi_strip_metadata(fname)
+ if(length(error))
+ stack_trace("Failed to strip [name]_[size_id].png: [error]")
+ size[SPRSZ_STRIPPED] = icon(fname)
+ fdel(fname)
+
+/datum/asset/spritesheet/proc/generate_css()
+ var/list/out = list()
+
+ for (var/size_id in sizes)
+ var/size = sizes[size_id]
+ var/icon/tiny = size[SPRSZ_ICON]
+ out += ".[name][size_id]{display:inline-block;width:[tiny.Width()]px;height:[tiny.Height()]px;background:url('[SSassets.transport.get_asset_url("[name]_[size_id].png")]') no-repeat;}"
+
+ for (var/sprite_id in sprites)
+ var/sprite = sprites[sprite_id]
+ var/size_id = sprite[SPR_SIZE]
+ var/idx = sprite[SPR_IDX]
+ var/size = sizes[size_id]
+
+ var/icon/tiny = size[SPRSZ_ICON]
+ var/icon/big = size[SPRSZ_STRIPPED]
+ var/per_line = big.Width() / tiny.Width()
+ var/x = (idx % per_line) * tiny.Width()
+ var/y = round(idx / per_line) * tiny.Height()
+
+ out += ".[name][size_id].[sprite_id]{background-position:-[x]px -[y]px;}"
+
+ return out.Join("\n")
+
+/datum/asset/spritesheet/proc/Insert(sprite_name, icon/I, icon_state="", dir=SOUTH, frame=1, moving=FALSE)
+ I = icon(I, icon_state=icon_state, dir=dir, frame=frame, moving=moving)
+ if (!I || !length(icon_states(I))) // that direction or state doesn't exist
+ return
+ var/size_id = "[I.Width()]x[I.Height()]"
+ var/size = sizes[size_id]
+
+ if (sprites[sprite_name])
+ CRASH("duplicate sprite \"[sprite_name]\" in sheet [name] ([type])")
+
+ if (size)
+ var/position = size[SPRSZ_COUNT]++
+ var/icon/sheet = size[SPRSZ_ICON]
+ size[SPRSZ_STRIPPED] = null
+ sheet.Insert(I, icon_state=sprite_name)
+ sprites[sprite_name] = list(size_id, position)
+ else
+ sizes[size_id] = size = list(1, I, null)
+ sprites[sprite_name] = list(size_id, 0)
+
+/datum/asset/spritesheet/proc/InsertAll(prefix, icon/I, list/directions)
+ if (length(prefix))
+ prefix = "[prefix]-"
+
+ if (!directions)
+ directions = list(SOUTH)
+
+ for (var/icon_state_name in icon_states(I))
+ for (var/direction in directions)
+ var/prefix2 = (directions.len > 1) ? "[dir2text(direction)]-" : ""
+ Insert("[prefix][prefix2][icon_state_name]", I, icon_state=icon_state_name, dir=direction)
+
+/datum/asset/spritesheet/proc/css_tag()
+ return {""}
+
+/datum/asset/spritesheet/proc/css_filename()
+ return SSassets.transport.get_asset_url("spritesheet_[name].css")
+
+/datum/asset/spritesheet/proc/icon_tag(sprite_name)
+ var/sprite = sprites[sprite_name]
+ if (!sprite)
+ return null
+ var/size_id = sprite[SPR_SIZE]
+ return {""}
+
+/datum/asset/spritesheet/proc/icon_class_name(sprite_name)
+ var/sprite = sprites[sprite_name]
+ if (!sprite)
+ return null
+ var/size_id = sprite[SPR_SIZE]
+ return {"[name][size_id] [sprite_name]"}
+
+#undef SPR_SIZE
+#undef SPR_IDX
+#undef SPRSZ_COUNT
+#undef SPRSZ_ICON
+#undef SPRSZ_STRIPPED
+
+
+/datum/asset/spritesheet/simple
+ _abstract = /datum/asset/spritesheet/simple
+ var/list/assets
+
+/datum/asset/spritesheet/simple/register()
+ for (var/key in assets)
+ Insert(key, assets[key])
+ ..()
+
+//Generates assets based on iconstates of a single icon
+/datum/asset/simple/icon_states
+ _abstract = /datum/asset/simple/icon_states
+ var/icon
+ var/list/directions = list(SOUTH)
+ var/frame = 1
+ var/movement_states = FALSE
+
+ var/prefix = "default" //asset_name = "[prefix].[icon_state_name].png"
+ var/generic_icon_names = FALSE //generate icon filenames using generate_asset_name() instead the above format
+
+/datum/asset/simple/icon_states/register(_icon = icon)
+ for(var/icon_state_name in icon_states(_icon))
+ for(var/direction in directions)
+ var/asset = icon(_icon, icon_state_name, direction, frame, movement_states)
+ if (!asset)
+ continue
+ asset = fcopy_rsc(asset) //dedupe
+ var/prefix2 = (directions.len > 1) ? "[dir2text(direction)]." : ""
+ var/asset_name = sanitize_filename("[prefix].[prefix2][icon_state_name].png")
+ if (generic_icon_names)
+ asset_name = "[generate_asset_name(asset)].png"
+
+ SSassets.transport.register_asset(asset_name, asset)
+
+/datum/asset/simple/icon_states/multiple_icons
+ _abstract = /datum/asset/simple/icon_states/multiple_icons
+ var/list/icons
+
+/datum/asset/simple/icon_states/multiple_icons/register()
+ for(var/i in icons)
+ ..(i)
+
+/// Namespace'ed assets (for static css and html files)
+/// When sent over a cdn transport, all assets in the same asset datum will exist in the same folder, as their plain names.
+/// Used to ensure css files can reference files by url() without having to generate the css at runtime, both the css file and the files it depends on must exist in the same namespace asset datum. (Also works for html)
+/// For example `blah.css` with asset `blah.png` will get loaded as `namespaces/a3d..14f/f12..d3c.css` and `namespaces/a3d..14f/blah.png`. allowing the css file to load `blah.png` by a relative url rather then compute the generated url with get_url_mappings().
+/// The namespace folder's name will change if any of the assets change. (excluding parent assets)
+/datum/asset/simple/namespaced
+ _abstract = /datum/asset/simple/namespaced
+ /// parents - list of the parent asset or assets (in name = file assoicated format) for this namespace.
+ /// parent assets must be referenced by their generated url, but if an update changes a parent asset, it won't change the namespace's identity.
+ var/list/parents = list()
+
+/datum/asset/simple/namespaced/register()
+ if (legacy)
+ assets |= parents
+ var/list/hashlist = list()
+ var/list/sorted_assets = sortList(assets)
+
+ for (var/asset_name in sorted_assets)
+ var/datum/asset_cache_item/ACI = new(asset_name, sorted_assets[asset_name])
+ if (!ACI?.hash)
+ log_asset("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
+ continue
+ hashlist += ACI.hash
+ sorted_assets[asset_name] = ACI
+ var/namespace = md5(hashlist.Join())
+
+ for (var/asset_name in parents)
+ var/datum/asset_cache_item/ACI = new(asset_name, parents[asset_name])
+ if (!ACI?.hash)
+ log_asset("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
+ continue
+ ACI.namespace_parent = TRUE
+ sorted_assets[asset_name] = ACI
+
+ for (var/asset_name in sorted_assets)
+ var/datum/asset_cache_item/ACI = sorted_assets[asset_name]
+ if (!ACI?.hash)
+ log_asset("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
+ continue
+ ACI.namespace = namespace
+
+ assets = sorted_assets
+ ..()
+
+/// Get a html string that will load a html asset.
+/// Needed because byond doesn't allow you to browse() to a url.
+/datum/asset/simple/namespaced/proc/get_htmlloader(filename)
+ return url2htmlloader(SSassets.transport.get_asset_url(filename, assets[filename]))
diff --git a/code/modules/asset_cache/readme.md b/code/modules/asset_cache/readme.md
new file mode 100644
index 00000000000..c8c9d78b719
--- /dev/null
+++ b/code/modules/asset_cache/readme.md
@@ -0,0 +1,37 @@
+# Asset cache system
+
+## Framework for managing browser assets (javascript,css,images,etc)
+
+This manages getting the asset to the client without doing unneeded re-sends, as well as utilizing any configured cdns.
+
+There are two frameworks for using this system:
+
+### Asset datum:
+
+Make a datum in asset_list_items.dm with your browser assets for your thing.
+
+Checkout asset_list.dm for the helper subclasses
+
+The `simple` subclass will most likely be of use for most cases.
+
+Call get_asset_datum() with the type of the datum you created to get your asset cache datum
+
+Call .send(client|usr) on that datum to send the asset to the client. Depending on the asset transport this may or may not block.
+
+Call .get_url_mappings() to get an associated list with the urls your assets can be found at.
+
+### Manual backend:
+
+See the documentation for `/datum/asset_transport` for the backend api the asset datums utilize.
+
+The global variable `SSassets.transport` contains the currently configured transport.
+
+
+
+### Notes:
+
+Because byond browse() calls use non-blocking queues, if your code uses output() (which bypasses all of these queues) to invoke javascript functions you will need to first have the javascript announce to the server it has loaded before trying to invoke js functions.
+
+To make your code work with any CDNs configured by the server, you must make sure assets are referenced from the url returned by `get_url_mappings()` or by asset_transport's `get_asset_url()`. (TGUI also has helpers for this.) If this can not be easily done, you can bypass the cdn using legacy assets, see the simple asset datum for details.
+
+CSS files that use url() can be made to use the CDN without needing to rewrite all url() calls in code by using the namespaced helper datum. See the documentation for `/datum/asset/simple/namespaced` for details.
diff --git a/code/modules/asset_cache/transport/asset_transport.dm b/code/modules/asset_cache/transport/asset_transport.dm
new file mode 100644
index 00000000000..abebf1f39a2
--- /dev/null
+++ b/code/modules/asset_cache/transport/asset_transport.dm
@@ -0,0 +1,142 @@
+/// When sending mutiple assets, how many before we give the client a quaint little sending resources message
+#define ASSET_CACHE_TELL_CLIENT_AMOUNT 8
+
+/// Base browse_rsc asset transport
+/datum/asset_transport
+ var/name = "Simple browse_rsc asset transport"
+ var/static/list/preload
+ /// Don't mutate the filename of assets when sending via browse_rsc.
+ /// This is to make it easier to debug issues with assets, and allow server operators to bypass issues that make it to production.
+ /// If turning this on fixes asset issues, something isn't using get_asset_url and the asset isn't marked legacy, fix one of those.
+ var/dont_mutate_filenames = FALSE
+
+/// Called when the transport is loaded by the config controller, not called on the default transport unless it gets loaded by a config change.
+/datum/asset_transport/proc/Load()
+ if (CONFIG_GET(flag/asset_simple_preload))
+ for(var/client/C in GLOB.clients)
+ addtimer(CALLBACK(src, PROC_REF(send_assets_slow), C, preload), 1 SECONDS)
+
+/// Initialize - Called when SSassets initializes.
+/datum/asset_transport/proc/Initialize(list/assets)
+ preload = assets.Copy()
+ if (!CONFIG_GET(flag/asset_simple_preload))
+ return
+ for(var/client/C in GLOB.clients)
+ addtimer(CALLBACK(src, PROC_REF(send_assets_slow), C, preload), 1 SECONDS)
+
+
+/// Register a browser asset with the asset cache system
+/// asset_name - the identifier of the asset
+/// asset - the actual asset file (or an asset_cache_item datum)
+/// returns a /datum/asset_cache_item.
+/// mutiple calls to register the same asset under the same asset_name return the same datum
+/datum/asset_transport/proc/register_asset(asset_name, asset)
+ var/datum/asset_cache_item/ACI = asset
+ if (!istype(ACI))
+ ACI = new(asset_name, asset)
+ if (!ACI || !ACI.hash)
+ CRASH("ERROR: Invalid asset: [asset_name]:[asset]:[ACI]")
+ if (SSassets.cache[asset_name])
+ var/datum/asset_cache_item/OACI = SSassets.cache[asset_name]
+ OACI.legacy = ACI.legacy = (ACI.legacy|OACI.legacy)
+ OACI.namespace_parent = ACI.namespace_parent = (ACI.namespace_parent | OACI.namespace_parent)
+ OACI.namespace = OACI.namespace || ACI.namespace
+ if (OACI.hash != ACI.hash)
+ var/error_msg = "ERROR: new asset added to the asset cache with the same name as another asset: [asset_name] existing asset hash: [OACI.hash] new asset hash:[ACI.hash]"
+ stack_trace(error_msg)
+ log_asset(error_msg)
+ else
+ if (length(ACI.namespace))
+ return ACI
+ return OACI
+
+ SSassets.cache[asset_name] = ACI
+ return ACI
+
+
+/// Returns a url for a given asset.
+/// asset_name - Name of the asset.
+/// asset_cache_item - asset cache item datum for the asset, optional, overrides asset_name
+/datum/asset_transport/proc/get_asset_url(asset_name, datum/asset_cache_item/asset_cache_item)
+ if (!istype(asset_cache_item))
+ asset_cache_item = SSassets.cache[asset_name]
+ if (dont_mutate_filenames || asset_cache_item.legacy || (asset_cache_item.namespace && !asset_cache_item.namespace_parent)) // to ensure code that breaks on cdns breaks in local testing, we only use the normal filename on legacy assets and name space assets.
+ return url_encode(asset_cache_item.name)
+ return url_encode("asset.[asset_cache_item.hash][asset_cache_item.ext]")
+
+
+/// Sends a list of browser assets to a client
+/// client - a client or mob
+/// asset_list - A list of asset filenames to be sent to the client. Can optionally be assoicated with the asset's asset_cache_item datum.
+/// Returns TRUE if any assets were sent.
+/datum/asset_transport/proc/send_assets(client/client, list/asset_list)
+ if(isnull(client))
+ return
+ if (!istype(client))
+ if (ismob(client))
+ var/mob/M = client
+ if (M.client)
+ client = M.client
+ else //no stacktrace because this will mainly happen because the client went away
+ return
+ else
+ CRASH("Invalid argument: client: `[client]`")
+ if (!islist(asset_list))
+ asset_list = list(asset_list)
+ var/list/unreceived = list()
+
+ for (var/asset_name in asset_list)
+ var/datum/asset_cache_item/ACI = asset_list[asset_name]
+ if (!istype(ACI) && !(ACI = SSassets.cache[asset_name]))
+ log_asset("ERROR: can't send asset `[asset_name]`: unregistered or invalid state: `[ACI]`")
+ continue
+ var/asset_file = ACI.resource
+ if (!asset_file)
+ log_asset("ERROR: can't send asset `[asset_name]`: invalid registered resource: `[ACI.resource]`")
+ continue
+
+ var/asset_hash = ACI.hash
+ var/new_asset_name = asset_name
+ if (!dont_mutate_filenames && !ACI.legacy && (!ACI.namespace || ACI.namespace_parent))
+ new_asset_name = "asset.[ACI.hash][ACI.ext]"
+ if (client.sent_assets[new_asset_name] == asset_hash)
+ if (GLOB.Debug2)
+ log_asset("DEBUG: Skipping send of `[asset_name]` (as `[new_asset_name]`) for `[client]` because it already exists in the client's sent_assets list")
+ continue
+ unreceived[asset_name] = ACI
+
+ if (unreceived.len)
+ if (unreceived.len >= ASSET_CACHE_TELL_CLIENT_AMOUNT)
+ to_chat(client, "Sending Resources...")
+
+ for (var/asset_name in unreceived)
+ var/new_asset_name = asset_name
+ var/datum/asset_cache_item/ACI = unreceived[asset_name]
+ if (!dont_mutate_filenames && !ACI.legacy && (!ACI.namespace || ACI.namespace_parent))
+ new_asset_name = "asset.[ACI.hash][ACI.ext]"
+ log_asset("Sending asset `[asset_name]` to client `[client]` as `[new_asset_name]`")
+ client << browse_rsc(ACI.resource, new_asset_name)
+
+ client.sent_assets[new_asset_name] = ACI.hash
+
+ addtimer(CALLBACK(client, /client/proc/asset_cache_update_json), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE)
+ return TRUE
+ return FALSE
+
+
+/// Precache files without clogging up the browse() queue, used for passively sending files on connection start.
+/datum/asset_transport/proc/send_assets_slow(client/client, list/files, filerate = 3)
+ var/startingfilerate = filerate
+ for (var/file in files)
+ if (!client)
+ break
+ if (send_assets(client, file))
+ if (!(--filerate))
+ filerate = startingfilerate
+ client.browse_queue_flush()
+ stoplag(0) //queuing calls like this too quickly can cause issues in some client versions
+
+/// Check the config is valid to load this transport
+/// Returns TRUE or FALSE
+/datum/asset_transport/proc/validate_config(log = TRUE)
+ return TRUE
diff --git a/code/modules/asset_cache/transport/webroot.dm b/code/modules/asset_cache/transport/webroot.dm
new file mode 100644
index 00000000000..8ff734460df
--- /dev/null
+++ b/code/modules/asset_cache/transport/webroot.dm
@@ -0,0 +1,87 @@
+/// CDN Webroot asset transport.
+/datum/asset_transport/webroot
+ name = "CDN Webroot asset transport"
+
+/datum/asset_transport/webroot/Load()
+ if (validate_config(log = FALSE))
+ load_existing_assets()
+
+/// Processes thru any assets that were registered before we were loaded as a transport.
+/datum/asset_transport/webroot/proc/load_existing_assets()
+ for (var/asset_name in SSassets.cache)
+ var/datum/asset_cache_item/ACI = SSassets.cache[asset_name]
+ save_asset_to_webroot(ACI)
+
+/// Register a browser asset with the asset cache system
+/// We also save it to the CDN webroot at this step instead of waiting for send_assets()
+/// asset_name - the identifier of the asset
+/// asset - the actual asset file or an asset_cache_item datum.
+/datum/asset_transport/webroot/register_asset(asset_name, asset)
+ . = ..()
+ var/datum/asset_cache_item/ACI = .
+
+ if (istype(ACI) && ACI.hash)
+ save_asset_to_webroot(ACI)
+
+/// Saves the asset to the webroot taking into account namespaces and hashes.
+/datum/asset_transport/webroot/proc/save_asset_to_webroot(datum/asset_cache_item/ACI)
+ var/webroot = CONFIG_GET(string/asset_cdn_webroot)
+ var/newpath = "[webroot][get_asset_suffex(ACI)]"
+ if (fexists(newpath))
+ return
+ if (fexists("[newpath].gz")) //its a common pattern in webhosting to save gzip'ed versions of text files and let the webserver serve them up as gzip compressed normal files, sometimes without keeping the original version.
+ return
+ return fcopy(ACI.resource, newpath)
+
+/// Returns a url for a given asset.
+/// asset_name - Name of the asset.
+/// asset_cache_item - asset cache item datum for the asset, optional, overrides asset_name
+/datum/asset_transport/webroot/get_asset_url(asset_name, datum/asset_cache_item/asset_cache_item)
+ if (!istype(asset_cache_item))
+ asset_cache_item = SSassets.cache[asset_name]
+ var/url = CONFIG_GET(string/asset_cdn_url) //config loading will handle making sure this ends in a /
+ return "[url][get_asset_suffex(asset_cache_item)]"
+
+/datum/asset_transport/webroot/proc/get_asset_suffex(datum/asset_cache_item/asset_cache_item)
+ var/base = ""
+ var/filename = "asset.[asset_cache_item.hash][asset_cache_item.ext]"
+ if (length(asset_cache_item.namespace))
+ base = "namespaces/[asset_cache_item.namespace]/"
+ if (!asset_cache_item.namespace_parent)
+ filename = "[asset_cache_item.name]"
+ return base + filename
+
+
+/// webroot asset sending - does nothing unless passed legacy assets
+/datum/asset_transport/webroot/send_assets(client/client, list/asset_list)
+ . = FALSE
+ var/list/legacy_assets = list()
+ if (!islist(asset_list))
+ asset_list = list(asset_list)
+ for (var/asset_name in asset_list)
+ var/datum/asset_cache_item/ACI = asset_list[asset_name]
+ if (!istype(ACI))
+ ACI = SSassets.cache[asset_name]
+ if (!ACI)
+ legacy_assets += asset_name //pass it on to base send_assets so it can output an error
+ continue
+ if (ACI.legacy)
+ legacy_assets[asset_name] = ACI
+ if (length(legacy_assets))
+ . = ..(client, legacy_assets)
+
+
+/// webroot slow asset sending - does nothing.
+/datum/asset_transport/webroot/send_assets_slow(client/client, list/files, filerate)
+ return FALSE
+
+/datum/asset_transport/webroot/validate_config(log = TRUE)
+ if (!CONFIG_GET(string/asset_cdn_url))
+ if (log)
+ log_asset("ERROR: [type]: Invalid Config: ASSET_CDN_URL")
+ return FALSE
+ if (!CONFIG_GET(string/asset_cdn_webroot))
+ if (log)
+ log_asset("ERROR: [type]: Invalid Config: ASSET_CDN_WEBROOT")
+ return FALSE
+ return TRUE
diff --git a/code/modules/asset_cache/validate_assets.html b/code/modules/asset_cache/validate_assets.html
new file mode 100644
index 00000000000..70fdca8a9d7
--- /dev/null
+++ b/code/modules/asset_cache/validate_assets.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm
deleted file mode 100644
index dde8816ebb2..00000000000
--- a/code/modules/client/asset_cache.dm
+++ /dev/null
@@ -1,795 +0,0 @@
-/*
-Asset cache quick users guide:
-
-Make a datum at the bottom of this file with your assets for your thing.
-The simple subsystem will most like be of use for most cases.
-Then call get_asset_datum() with the type of the datum you created and store the return
-Then call .send(client) on that stored return value.
-
-You can set verify to TRUE if you want send() to sleep until the client has the assets.
-*/
-
-
-// Amount of time(ds) MAX to send per asset, if this get exceeded we cancel the sleeping.
-// This is doubled for the first asset, then added per asset after
-#define ASSET_CACHE_SEND_TIMEOUT 7
-
-//When sending mutiple assets, how many before we give the client a quaint little sending resources message
-#define ASSET_CACHE_TELL_CLIENT_AMOUNT 8
-
-//When passively preloading assets, how many to send at once? Too high creates noticable lag where as too low can flood the client's cache with "verify" files
-#define ASSET_CACHE_PRELOAD_CONCURRENT 3
-
-/client
- var/list/cache = list() // List of all assets sent to this client by the asset cache.
- var/list/completed_asset_jobs = list() // List of all completed jobs, awaiting acknowledgement.
- var/list/sending = list()
- var/last_asset_job = 0 // Last job done.
-
-//This proc sends the asset to the client, but only if it needs it.
-//This proc blocks(sleeps) unless verify is set to false
-/proc/send_asset(client/client, asset_name, verify = TRUE)
- if(!istype(client))
- if(ismob(client))
- var/mob/M = client
- if(M.client)
- client = M.client
-
- else
- return 0
-
- else
- return 0
-
- if(client.cache.Find(asset_name) || client.sending.Find(asset_name))
- return 0
-
- log_asset("Sending asset [asset_name] to client [client]")
- client << browse_rsc(SSassets.cache[asset_name], asset_name)
- if(!verify)
- client.cache += asset_name
- return 1
-
- client.sending |= asset_name
- var/job = ++client.last_asset_job
-
- client << browse({"
-
- "}, "window=asset_cache_browser")
-
- var/t = 0
- var/timeout_time = (ASSET_CACHE_SEND_TIMEOUT * client.sending.len) + ASSET_CACHE_SEND_TIMEOUT
- while(client && !client.completed_asset_jobs.Find(job) && t < timeout_time) // Reception is handled in Topic()
- stoplag(1) // Lock up the caller until this is received.
- t++
-
- if(client)
- client.sending -= asset_name
- client.cache |= asset_name
- client.completed_asset_jobs -= job
-
- return 1
-
-//This proc blocks(sleeps) unless verify is set to false
-/proc/send_asset_list(client/client, list/asset_list, verify = TRUE)
- if(!istype(client))
- if(ismob(client))
- var/mob/M = client
- if(M.client)
- client = M.client
-
- else
- return 0
-
- else
- return 0
-
- var/list/unreceived = asset_list - (client.cache + client.sending)
- if(!unreceived || !unreceived.len)
- return 0
-// if (unreceived.len >= ASSET_CACHE_TELL_CLIENT_AMOUNT)
-// to_chat(client, "Sending Resources...")
- for(var/asset in unreceived)
- if (asset in SSassets.cache)
- log_asset("Sending asset [asset] to client [client]")
- client << browse_rsc(SSassets.cache[asset], asset)
-
- if(!verify) // Can't access the asset cache browser, rip.
- client.cache += unreceived
- return 1
-
- client.sending |= unreceived
- var/job = ++client.last_asset_job
-
- client << browse({"
-
- "}, "window=asset_cache_browser")
-
- var/t = 0
- var/timeout_time = ASSET_CACHE_SEND_TIMEOUT * client.sending.len
- while(client && !client.completed_asset_jobs.Find(job) && t < timeout_time) // Reception is handled in Topic()
- stoplag(1) // Lock up the caller until this is received.
- t++
-
- if(client)
- client.sending -= unreceived
- client.cache |= unreceived
- client.completed_asset_jobs -= job
-
- return 1
-
-//This proc will download the files without clogging up the browse() queue, used for passively sending files on connection start.
-//The proc calls procs that sleep for long times.
-/proc/getFilesSlow(client/client, list/files, register_asset = TRUE)
- var/concurrent_tracker = 1
- for(var/file in files)
- if (!client)
- break
- if (register_asset)
- register_asset(file, files[file])
- if (concurrent_tracker >= ASSET_CACHE_PRELOAD_CONCURRENT)
- concurrent_tracker = 1
- send_asset(client, file)
- else
- concurrent_tracker++
- send_asset(client, file, verify=FALSE)
-
- stoplag(0) //queuing calls like this too quickly can cause issues in some client versions
-
-//This proc "registers" an asset, it adds it to the cache for further use, you cannot touch it from this point on or you'll fuck things up.
-//if it's an icon or something be careful, you'll have to copy it before further use.
-/proc/register_asset(asset_name, asset)
- SSassets.cache[asset_name] = asset
-
-//Generated names do not include file extention.
-//Used mainly for code that deals with assets in a generic way
-//The same asset will always lead to the same asset name
-/proc/generate_asset_name(file)
- return "asset.[md5(fcopy_rsc(file))]"
-
-
-//These datums are used to populate the asset cache, the proc "register()" does this.
-
-//all of our asset datums, used for referring to these later
-GLOBAL_LIST_EMPTY(asset_datums)
-
-//get an assetdatum or make a new one
-/proc/get_asset_datum(type)
- return GLOB.asset_datums[type] || new type()
-
-/datum/asset
- var/_abstract = /datum/asset
-
-/datum/asset/New()
- GLOB.asset_datums[type] = src
- register()
-
-/datum/asset/proc/register()
- return
-
-/datum/asset/proc/send(client)
- return
-
-
-//If you don't need anything complicated.
-/datum/asset/simple
- _abstract = /datum/asset/simple
- var/assets = list()
- var/verify = FALSE
-
-/datum/asset/simple/register()
- for(var/asset_name in assets)
- register_asset(asset_name, assets[asset_name])
-
-/datum/asset/simple/send(client)
- send_asset_list(client,assets,verify)
-
-
-// For registering or sending multiple others at once
-/datum/asset/group
- _abstract = /datum/asset/group
- var/list/children
-
-/datum/asset/group/register()
- for(var/type in children)
- get_asset_datum(type)
-
-/datum/asset/group/send(client/C)
- for(var/type in children)
- var/datum/asset/A = get_asset_datum(type)
- A.send(C)
-
-
-// spritesheet implementation - coalesces various icons into a single .png file
-// and uses CSS to select icons out of that file - saves on transferring some
-// 1400-odd individual PNG files
-#define SPR_SIZE 1
-#define SPR_IDX 2
-#define SPRSZ_COUNT 1
-#define SPRSZ_ICON 2
-#define SPRSZ_STRIPPED 3
-
-/datum/asset/spritesheet
- _abstract = /datum/asset/spritesheet
- var/name
- var/list/sizes = list() // "32x32" -> list(10, icon/normal, icon/stripped)
- var/list/sprites = list() // "foo_bar" -> list("32x32", 5)
- var/verify = FALSE
-
-/datum/asset/spritesheet/register()
- if (!name)
- CRASH("spritesheet [type] cannot register without a name")
- ensure_stripped()
-
- var/res_name = "test.css"
- var/fname = "data/spritesheets/[res_name]"
- fdel(fname)
- text2file(generate_css(), fname)
- register_asset(res_name, fcopy_rsc(fname))
- fdel(fname)
-
- for(var/size_id in sizes)
- var/size = sizes[size_id]
- register_asset("[name]_[size_id].png", size[SPRSZ_STRIPPED])
-
-/datum/asset/spritesheet/send(client/C)
- if (!name)
- return
- var/all = list("spritesheet_[name].css")
- for(var/size_id in sizes)
- all += "[name]_[size_id].png"
- send_asset_list(C, all, verify)
-
-/datum/asset/spritesheet/proc/ensure_stripped(sizes_to_strip = sizes)
- for(var/size_id in sizes_to_strip)
- var/size = sizes[size_id]
- if (size[SPRSZ_STRIPPED])
- continue
-
- // save flattened version
- var/fname = "data/spritesheets/[name]_[size_id].png"
- fcopy(size[SPRSZ_ICON], fname)
- var/error = rustg_dmi_strip_metadata(fname)
- if(length(error))
- stack_trace("Failed to strip [name]_[size_id].png: [error]")
- size[SPRSZ_STRIPPED] = icon(fname)
- fdel(fname)
-
-/datum/asset/spritesheet/proc/generate_css()
- var/list/out = list()
-
- for (var/size_id in sizes)
- var/size = sizes[size_id]
- var/icon/tiny = size[SPRSZ_ICON]
- out += ".[name][size_id]{display:inline-block;width:[tiny.Width()]px;height:[tiny.Height()]px;background:url('[name]_[size_id].png') no-repeat;}"
-
- for (var/sprite_id in sprites)
- var/sprite = sprites[sprite_id]
- var/size_id = sprite[SPR_SIZE]
- var/idx = sprite[SPR_IDX]
- var/size = sizes[size_id]
-
- var/icon/tiny = size[SPRSZ_ICON]
- var/icon/big = size[SPRSZ_STRIPPED]
- var/per_line = big.Width() / tiny.Width()
- var/x = (idx % per_line) * tiny.Width()
- var/y = round(idx / per_line) * tiny.Height()
-
- out += ".[name][size_id].[sprite_id]{background-position:-[x]px -[y]px;}"
-
- return out.Join("\n")
-
-/datum/asset/spritesheet/proc/Insert(sprite_name, icon/I, icon_state="", dir=SOUTH, frame=1, moving=FALSE)
- I = icon(I, icon_state=icon_state, dir=dir, frame=frame, moving=moving)
- if (!I || !length(icon_states(I))) // that direction or state doesn't exist
- return
- var/size_id = "[I.Width()]x[I.Height()]"
- var/size = sizes[size_id]
-
- if (sprites[sprite_name])
- CRASH("duplicate sprite \"[sprite_name]\" in sheet [name] ([type])")
-
- if (size)
- var/position = size[SPRSZ_COUNT]++
- var/icon/sheet = size[SPRSZ_ICON]
- size[SPRSZ_STRIPPED] = null
- sheet.Insert(I, icon_state=sprite_name)
- sprites[sprite_name] = list(size_id, position)
- else
- sizes[size_id] = size = list(1, I, null)
- sprites[sprite_name] = list(size_id, 0)
-
-/datum/asset/spritesheet/proc/InsertAll(prefix, icon/I, list/directions)
- if (length(prefix))
- prefix = "[prefix]-"
-
- if (!directions)
- directions = list(SOUTH)
-
- for (var/icon_state_name in icon_states(I))
- for (var/direction in directions)
- var/prefix2 = (directions.len > 1) ? "[dir2text(direction)]-" : ""
- Insert("[prefix][prefix2][icon_state_name]", I, icon_state=icon_state_name, dir=direction)
-
-/datum/asset/spritesheet/proc/css_tag()
- return {""}
-
-/datum/asset/spritesheet/proc/icon_tag(sprite_name)
- var/sprite = sprites[sprite_name]
- if (!sprite)
- return null
- var/size_id = sprite[SPR_SIZE]
- return {""}
-
-/datum/asset/spritesheet/proc/icon_class_name(sprite_name)
- var/sprite = sprites[sprite_name]
- if (!sprite)
- return null
- var/size_id = sprite[SPR_SIZE]
- return {"[name][size_id] [sprite_name]"}
-
-#undef SPR_SIZE
-#undef SPR_IDX
-#undef SPRSZ_COUNT
-#undef SPRSZ_ICON
-#undef SPRSZ_STRIPPED
-
-
-/datum/asset/spritesheet/simple
- _abstract = /datum/asset/spritesheet/simple
- var/list/assets
-
-/datum/asset/spritesheet/simple/register()
- for (var/key in assets)
- Insert(key, assets[key])
- ..()
-
-//Generates assets based on iconstates of a single icon
-/datum/asset/simple/icon_states
- _abstract = /datum/asset/simple/icon_states
- var/icon
- var/list/directions = list(SOUTH)
- var/frame = 1
- var/movement_states = FALSE
-
- var/prefix = "default" //asset_name = "[prefix].[icon_state_name].png"
- var/generic_icon_names = FALSE //generate icon filenames using generate_asset_name() instead the above format
-
- verify = FALSE
-
-/datum/asset/simple/icon_states/register(_icon = icon)
- for(var/icon_state_name in icon_states(_icon))
- for(var/direction in directions)
- var/asset = icon(_icon, icon_state_name, direction, frame, movement_states)
- if (!asset)
- continue
- asset = fcopy_rsc(asset) //dedupe
- var/prefix2 = (directions.len > 1) ? "[dir2text(direction)]." : ""
- var/asset_name = sanitize_filename("[prefix].[prefix2][icon_state_name].png")
- if (generic_icon_names)
- asset_name = "[generate_asset_name(asset)].png"
-
- register_asset(asset_name, asset)
-
-/datum/asset/simple/icon_states/multiple_icons
- _abstract = /datum/asset/simple/icon_states/multiple_icons
- var/list/icons
-
-/datum/asset/simple/icon_states/multiple_icons/register()
- for(var/i in icons)
- ..(i)
-
-
-//DEFINITIONS FOR ASSET DATUMS START HERE.
-
-/datum/asset/simple/tgui
-/* assets = list(
- // tgui
- "tgui.css" = 'tgui/assets/tgui.css',
- "tgui.js" = 'tgui/assets/tgui.js',
- // tgui-next
- "tgui-main.html" = 'tgui-next/packages/tgui/public/tgui-main.html',
- "tgui-fallback.html" = 'tgui-next/packages/tgui/public/tgui-fallback.html',
- "tgui.bundle.js" = 'tgui-next/packages/tgui/public/tgui.bundle.js',
- "tgui.bundle.css" = 'tgui-next/packages/tgui/public/tgui.bundle.css',
- "shim-html5shiv.js" = 'tgui-next/packages/tgui/public/shim-html5shiv.js',
- "shim-ie8.js" = 'tgui-next/packages/tgui/public/shim-ie8.js',
- "shim-dom4.js" = 'tgui-next/packages/tgui/public/shim-dom4.js',
- "shim-css-om.js" = 'tgui-next/packages/tgui/public/shim-css-om.js',
- )*/
-
-/datum/asset/group/tgui
- children = list(
- /datum/asset/simple/tgui,
- /datum/asset/simple/fontawesome
- )
-
-/datum/asset/simple/headers
-/* assets = list(
- "alarm_green.gif" = 'icons/program_icons/alarm_green.gif',
- "alarm_red.gif" = 'icons/program_icons/alarm_red.gif',
- "batt_5.gif" = 'icons/program_icons/batt_5.gif',
- "batt_20.gif" = 'icons/program_icons/batt_20.gif',
- "batt_40.gif" = 'icons/program_icons/batt_40.gif',
- "batt_60.gif" = 'icons/program_icons/batt_60.gif',
- "batt_80.gif" = 'icons/program_icons/batt_80.gif',
- "batt_100.gif" = 'icons/program_icons/batt_100.gif',
- "charging.gif" = 'icons/program_icons/charging.gif',
- "downloader_finished.gif" = 'icons/program_icons/downloader_finished.gif',
- "downloader_running.gif" = 'icons/program_icons/downloader_running.gif',
- "ntnrc_idle.gif" = 'icons/program_icons/ntnrc_idle.gif',
- "ntnrc_new.gif" = 'icons/program_icons/ntnrc_new.gif',
- "power_norm.gif" = 'icons/program_icons/power_norm.gif',
- "power_warn.gif" = 'icons/program_icons/power_warn.gif',
- "sig_high.gif" = 'icons/program_icons/sig_high.gif',
- "sig_low.gif" = 'icons/program_icons/sig_low.gif',
- "sig_lan.gif" = 'icons/program_icons/sig_lan.gif',
- "sig_none.gif" = 'icons/program_icons/sig_none.gif',
- "smmon_0.gif" = 'icons/program_icons/smmon_0.gif',
- "smmon_1.gif" = 'icons/program_icons/smmon_1.gif',
- "smmon_2.gif" = 'icons/program_icons/smmon_2.gif',
- "smmon_3.gif" = 'icons/program_icons/smmon_3.gif',
- "smmon_4.gif" = 'icons/program_icons/smmon_4.gif',
- "smmon_5.gif" = 'icons/program_icons/smmon_5.gif',
- "smmon_6.gif" = 'icons/program_icons/smmon_6.gif'
- )*/
-
-/datum/asset/spritesheet/simple/pda
- name = "pda"
-/* assets = list(
- "atmos" = 'icons/pda_icons/pda_atmos.png',
- "back" = 'icons/pda_icons/pda_back.png',
- "bell" = 'icons/pda_icons/pda_bell.png',
- "blank" = 'icons/pda_icons/pda_blank.png',
- "boom" = 'icons/pda_icons/pda_boom.png',
- "bucket" = 'icons/pda_icons/pda_bucket.png',
- "medbot" = 'icons/pda_icons/pda_medbot.png',
- "floorbot" = 'icons/pda_icons/pda_floorbot.png',
- "cleanbot" = 'icons/pda_icons/pda_cleanbot.png',
- "crate" = 'icons/pda_icons/pda_crate.png',
- "cuffs" = 'icons/pda_icons/pda_cuffs.png',
- "eject" = 'icons/pda_icons/pda_eject.png',
- "flashlight" = 'icons/pda_icons/pda_flashlight.png',
- "honk" = 'icons/pda_icons/pda_honk.png',
- "mail" = 'icons/pda_icons/pda_mail.png',
- "medical" = 'icons/pda_icons/pda_medical.png',
- "menu" = 'icons/pda_icons/pda_menu.png',
- "mule" = 'icons/pda_icons/pda_mule.png',
- "notes" = 'icons/pda_icons/pda_notes.png',
- "power" = 'icons/pda_icons/pda_power.png',
- "rdoor" = 'icons/pda_icons/pda_rdoor.png',
- "reagent" = 'icons/pda_icons/pda_reagent.png',
- "refresh" = 'icons/pda_icons/pda_refresh.png',
- "scanner" = 'icons/pda_icons/pda_scanner.png',
- "signaler" = 'icons/pda_icons/pda_signaler.png',
- "status" = 'icons/pda_icons/pda_status.png',
- "dronephone" = 'icons/pda_icons/pda_dronephone.png',
- "emoji" = 'icons/pda_icons/pda_emoji.png'
- )*/
-
-/datum/asset/spritesheet/simple/paper
- name = "paper"
-/* assets = list(
- "stamp-clown" = 'icons/stamp_icons/large_stamp-clown.png',
- "stamp-deny" = 'icons/stamp_icons/large_stamp-deny.png',
- "stamp-ok" = 'icons/stamp_icons/large_stamp-ok.png',
- "stamp-hop" = 'icons/stamp_icons/large_stamp-hop.png',
- "stamp-cmo" = 'icons/stamp_icons/large_stamp-cmo.png',
- "stamp-ce" = 'icons/stamp_icons/large_stamp-ce.png',
- "stamp-hos" = 'icons/stamp_icons/large_stamp-hos.png',
- "stamp-rd" = 'icons/stamp_icons/large_stamp-rd.png',
- "stamp-cap" = 'icons/stamp_icons/large_stamp-cap.png',
- "stamp-qm" = 'icons/stamp_icons/large_stamp-qm.png',
- "stamp-law" = 'icons/stamp_icons/large_stamp-law.png'
- )*/
-
-
-/datum/asset/simple/IRV
-/* assets = list(
- "jquery-ui.custom-core-widgit-mouse-sortable-min.js" = 'html/IRV/jquery-ui.custom-core-widgit-mouse-sortable-min.js',
- )*/
-
-/datum/asset/group/IRV
- children = list(
- /datum/asset/simple/jquery,
- /datum/asset/simple/IRV
- )
-
-/datum/asset/simple/changelog
-/* assets = list(
- "88x31.png" = 'html/88x31.png',
- "bug-minus.png" = 'html/bug-minus.png',
- "cross-circle.png" = 'html/cross-circle.png',
- "hard-hat-exclamation.png" = 'html/hard-hat-exclamation.png',
- "image-minus.png" = 'html/image-minus.png',
- "image-plus.png" = 'html/image-plus.png',
- "music-minus.png" = 'html/music-minus.png',
- "music-plus.png" = 'html/music-plus.png',
- "tick-circle.png" = 'html/tick-circle.png',
- "wrench-screwdriver.png" = 'html/wrench-screwdriver.png',
- "spell-check.png" = 'html/spell-check.png',
- "burn-exclamation.png" = 'html/burn-exclamation.png',
- "chevron.png" = 'html/chevron.png',
- "chevron-expand.png" = 'html/chevron-expand.png',
- "scales.png" = 'html/scales.png',
- "coding.png" = 'html/coding.png',
- "ban.png" = 'html/ban.png',
- "chrome-wrench.png" = 'html/chrome-wrench.png',
- "changelog.css" = 'html/changelog.css'
- )*/
-
-/datum/asset/group/goonchat
- children = list(
- /datum/asset/simple/jquery,
- /datum/asset/simple/goonchat,
- /datum/asset/spritesheet/goonchat,
- /datum/asset/simple/fontawesome
- )
-
-
-/datum/asset/simple/jquery
- verify = FALSE
-/* assets = list(
- "jquery.min.js" = 'code/modules/goonchat/browserassets/js/jquery.min.js',
- )*/
-
-/datum/asset/simple/goonchat
- verify = FALSE
-/* assets = list(
- "json2.min.js" = 'code/modules/goonchat/browserassets/js/json2.min.js',
- "browserOutput.js" = 'code/modules/goonchat/browserassets/js/browserOutput.js',
- "browserOutput.css" = 'code/modules/goonchat/browserassets/css/browserOutput.css',
- "browserOutput_white.css" = 'code/modules/goonchat/browserassets/css/browserOutput.css',
- )*/
-
-/datum/asset/simple/fontawesome
- verify = FALSE
-/* assets = list(
- "fa-regular-400.eot" = 'html/font-awesome/webfonts/fa-regular-400.eot',
- "fa-regular-400.woff" = 'html/font-awesome/webfonts/fa-regular-400.woff',
- "fa-solid-900.eot" = 'html/font-awesome/webfonts/fa-solid-900.eot',
- "fa-solid-900.woff" = 'html/font-awesome/webfonts/fa-solid-900.woff',
- "font-awesome.css" = 'html/font-awesome/css/all.min.css',
- "v4shim.css" = 'html/font-awesome/css/v4-shims.min.css'
- )*/
-
-/datum/asset/simple/blackedstone_class_menu_slop_layout
- verify = FALSE
- assets = list(
- "try4.png" = 'icons/roguetown/misc/try4.png',
- "try4_border.png" = 'icons/roguetown/misc/try4_border.png',
- "slop_menustyle2.css" = 'html/browser/slop_menustyle2.css',
- "haha_skull.gif" = 'icons/roguetown/misc/haha_skull.gif'
- )
-
-/datum/asset/simple/blackedstone_triumph_buy_menu_slop_layout
- verify = FALSE
- assets = list(
- "try5.png" = 'icons/roguetown/misc/try5.png',
- "try5_border.png" = 'icons/roguetown/misc/try5_border.png',
- "slop_menustyle3.css" = 'html/browser/slop_menustyle3.css'
- )
-
-/datum/asset/simple/roguefonts
- verify = TRUE
- assets = list(
- "pterra.ttf" = 'interface/fonts/pterra.ttf',
- "chiseld.ttf" = 'interface/fonts/chiseld.ttf',
- "blackmoor.ttf" = 'interface/fonts/blackmoor.ttf',
- "handwrite.ttf" = 'interface/fonts/handwrite.ttf',
- "book1.ttf" = 'interface/fonts/book1.ttf',
- "book2.ttf" = 'interface/fonts/book1.ttf',
- "book3.ttf" = 'interface/fonts/book1.ttf',
- "book4.ttf" = 'interface/fonts/book1.ttf',
- "dwarf.ttf" = 'interface/fonts/languages/dwarf.ttf',
- "elf.ttf" = 'interface/fonts/languages/elf.ttf',
- "hell.ttf" = 'interface/fonts/languages/hell.ttf',
- "orc.ttf" = 'interface/fonts/languages/orc.ttf',
- "sand.ttf" = 'interface/fonts/languages/sand.ttf',
- "undead.ttf" = 'interface/fonts/languages/undead.ttf',
- "draconic.ttf" = 'interface/fonts/languages/draconic.ttf',
- "fae.ttf" = 'interface/fonts/languages/fae.ttf',
- "lupian.ttf" = 'interface/fonts/languages/lupian.ttf',
- "felid.ttf" = 'interface/fonts/languages/felid.ttf'
- )
-
-/datum/asset/spritesheet/goonchat
- name = "chat"
-
-/datum/asset/spritesheet/goonchat/register()
-/* InsertAll("emoji", 'icons/emoji.dmi')
-
- // pre-loading all lanugage icons also helps to avoid meta
- InsertAll("language", 'icons/misc/language.dmi')
- // catch languages which are pulling icons from another file
- for(var/path in typesof(/datum/language))
- var/datum/language/L = path
- var/icon = initial(L.icon)
- if (icon != 'icons/misc/language.dmi')
- var/icon_state = initial(L.icon_state)
- Insert("language-[icon_state]", icon, icon_state=icon_state)
-*/
- ..()
-
-/datum/asset/simple/permissions
-/* assets = list(
- "padlock.png" = 'html/padlock.png'
- )*/
-
-/datum/asset/simple/notes
-/* assets = list(
- "high_button.png" = 'html/high_button.png',
- "medium_button.png" = 'html/medium_button.png',
- "minor_button.png" = 'html/minor_button.png',
- "none_button.png" = 'html/none_button.png',
- )*/
-
-/datum/asset/spritesheet/simple/achievements
- name ="achievements"
-/* assets = list(
- "default" = 'icons/UI_Icons/Achievements/default.png'
- )*/
-
-/datum/asset/spritesheet/simple/pills
- name ="pills"
-/* assets = list(
- "pill1" = 'icons/UI_Icons/Pills/pill1.png',
- "pill2" = 'icons/UI_Icons/Pills/pill2.png',
- "pill3" = 'icons/UI_Icons/Pills/pill3.png',
- "pill4" = 'icons/UI_Icons/Pills/pill4.png',
- "pill5" = 'icons/UI_Icons/Pills/pill5.png',
- "pill6" = 'icons/UI_Icons/Pills/pill6.png',
- "pill7" = 'icons/UI_Icons/Pills/pill7.png',
- "pill8" = 'icons/UI_Icons/Pills/pill8.png',
- "pill9" = 'icons/UI_Icons/Pills/pill9.png',
- "pill10" = 'icons/UI_Icons/Pills/pill10.png',
- "pill11" = 'icons/UI_Icons/Pills/pill11.png',
- "pill12" = 'icons/UI_Icons/Pills/pill12.png',
- "pill13" = 'icons/UI_Icons/Pills/pill13.png',
- "pill14" = 'icons/UI_Icons/Pills/pill14.png',
- "pill15" = 'icons/UI_Icons/Pills/pill15.png',
- "pill16" = 'icons/UI_Icons/Pills/pill16.png',
- "pill17" = 'icons/UI_Icons/Pills/pill17.png',
- "pill18" = 'icons/UI_Icons/Pills/pill18.png',
- "pill19" = 'icons/UI_Icons/Pills/pill19.png',
- "pill20" = 'icons/UI_Icons/Pills/pill20.png',
- "pill21" = 'icons/UI_Icons/Pills/pill21.png',
- "pill22" = 'icons/UI_Icons/Pills/pill22.png',
- )*/
-
-
-/datum/asset/spritesheet/simple/roulette
- name = "roulette"
-/* assets = list(
- "black" = 'icons/UI_Icons/Roulette/black.png',
- "red" = 'icons/UI_Icons/Roulette/red.png',
- "odd" = 'icons/UI_Icons/Roulette/odd.png',
- "even" = 'icons/UI_Icons/Roulette/even.png',
- "low" = 'icons/UI_Icons/Roulette/1-18.png',
- "high" = 'icons/UI_Icons/Roulette/19-36.png',
- "nano" = 'icons/UI_Icons/Roulette/nano.png',
- "zero" = 'icons/UI_Icons/Roulette/0.png'
- )*/
-
-
-
-
-//this exists purely to avoid meta by pre-loading all language icons.
-/datum/asset/language/register()
- for(var/path in typesof(/datum/language))
- set waitfor = FALSE
- var/datum/language/L = new path ()
- L.get_icon()
-
-/datum/asset/spritesheet/pipes
- name = "pipes"
-
-/datum/asset/spritesheet/pipes/register()
-/* for (var/each in list('icons/obj/atmospherics/pipes/pipe_item.dmi', 'icons/obj/atmospherics/pipes/disposal.dmi', 'icons/obj/atmospherics/pipes/transit_tube.dmi', 'icons/obj/plumbing/fluid_ducts.dmi'))
- InsertAll("", each, GLOB.alldirs)*/
- ..()
-
-// Representative icons for each research design
-/datum/asset/spritesheet/research_designs
- name = "design"
-
-/datum/asset/spritesheet/research_designs/register()
-/* for (var/path in subtypesof(/datum/design))
- var/datum/design/D = path
-
- var/icon_file
- var/icon_state
- var/icon/I
-
- if(initial(D.research_icon) && initial(D.research_icon_state)) //If the design has an icon replacement skip the rest
- icon_file = initial(D.research_icon)
- icon_state = initial(D.research_icon_state)
-// if(!(icon_state in icon_states(icon_file)))
-// warning("design [D] with icon '[icon_file]' missing state '[icon_state]'")
-// continue
- I = icon(icon_file, icon_state, SOUTH)
-
- else
- // construct the icon and slap it into the resource cache
- var/atom/item = initial(D.build_path)
- if (!ispath(item, /atom))
- // biogenerator outputs to beakers by default
- if (initial(D.build_type) & BIOGENERATOR)
- item = /obj/item/reagent_containers/glass/beaker/large
- else
- continue // shouldn't happen, but just in case
-
- // circuit boards become their resulting machines or computers
- if (ispath(item, /obj/item/circuitboard))
- var/obj/item/circuitboard/C = item
- var/machine = initial(C.build_path)
- if (machine)
- item = machine
-
- icon_file = initial(item.icon)
- icon_state = initial(item.icon_state)
-
-// if(!(icon_state in icon_states(icon_file)))
-// warning("design [D] with icon '[icon_file]' missing state '[icon_state]'")
-// continue
- I = icon(icon_file, icon_state, SOUTH)
-
- // computers (and snowflakes) get their screen and keyboard sprites
- if (ispath(item, /obj/machinery/computer) || ispath(item, /obj/machinery/power/solar_control))
- var/obj/machinery/computer/C = item
- var/screen = initial(C.icon_screen)
- var/keyboard = initial(C.icon_keyboard)
- var/all_states = icon_states(icon_file)
- if (screen && (screen in all_states))
- I.Blend(icon(icon_file, screen, SOUTH), ICON_OVERLAY)
- if (keyboard && (keyboard in all_states))
- I.Blend(icon(icon_file, keyboard, SOUTH), ICON_OVERLAY)
-
- Insert(initial(D.id), I)*/
- return ..()
-
-/datum/asset/spritesheet/vending
- name = "vending"
-
-/datum/asset/spritesheet/vending/register()
-/* for (var/k in GLOB.vending_products)
- var/atom/item = k
- if (!ispath(item, /atom))
- continue
-
- var/icon_file = initial(item.icon)
- var/icon_state = initial(item.icon_state)
- var/icon/I
-
- var/icon_states_list = icon_states(icon_file)
- if(icon_state in icon_states_list)
- I = icon(icon_file, icon_state, SOUTH)
- var/c = initial(item.color)
- if (!isnull(c) && c != "#FFFFFF")
- I.Blend(c, ICON_MULTIPLY)
- else
- var/icon_states_string
- for (var/an_icon_state in icon_states_list)
- if (!icon_states_string)
- icon_states_string = "[json_encode(an_icon_state)](\ref[an_icon_state])"
- else
- icon_states_string += ", [json_encode(an_icon_state)](\ref[an_icon_state])"
-// stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)](\ref[icon_state]), icon_states=[icon_states_string]")
- I = icon('icons/turf/floors.dmi', "", SOUTH)
-
- var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-")
-
- Insert(imgid, I)*/
- return ..()
-
-/datum/asset/simple/genetics
-/* assets = list(
- "dna_discovered.gif" = 'html/dna_discovered.gif',
- "dna_undiscovered.gif" = 'html/dna_undiscovered.gif',
- "dna_extra.gif" = 'html/dna_extra.gif'
- )*/
- assets = null
diff --git a/code/modules/client/asset_configs.dm b/code/modules/client/asset_configs.dm
new file mode 100644
index 00000000000..c839ccc078d
--- /dev/null
+++ b/code/modules/client/asset_configs.dm
@@ -0,0 +1,30 @@
+/datum/config_entry/keyed_list/external_rsc_urls
+ key_mode = KEY_MODE_TEXT
+ value_mode = VALUE_MODE_FLAG
+
+/datum/config_entry/flag/asset_simple_preload
+
+/datum/config_entry/string/asset_transport
+/datum/config_entry/string/asset_transport/ValidateAndSet(str_val)
+ return (lowertext(str_val) in list("simple", "webroot")) && ..(lowertext(str_val))
+
+/datum/config_entry/string/asset_cdn_webroot
+ protection = CONFIG_ENTRY_LOCKED
+
+/datum/config_entry/string/asset_cdn_webroot/ValidateAndSet(str_var)
+ if (!str_var || trim(str_var) == "")
+ return FALSE
+ if (str_var && str_var[length(str_var)] != "/")
+ str_var += "/"
+ return ..(str_var)
+
+/datum/config_entry/string/asset_cdn_url
+ protection = CONFIG_ENTRY_LOCKED
+ default = null
+
+/datum/config_entry/string/asset_cdn_url/ValidateAndSet(str_var)
+ if (!str_var || trim(str_var) == "")
+ return FALSE
+ if (str_var && str_var[length(str_var)] != "/")
+ str_var += "/"
+ return ..(str_var)
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 549069fea3b..af1805c9939 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -223,13 +223,10 @@ GLOBAL_LIST_EMPTY(respawncounts)
///////////
//CONNECT//
///////////
-#if (PRELOAD_RSC == 0)
-GLOBAL_LIST_EMPTY(external_rsc_urls)
-#endif
/client/New(TopicData)
var/tdata = TopicData //save this for later use
-// chatOutput = new /datum/chatOutput(src)
+ chatOutput = new /datum/chatOutput(src)
TopicData = null //Prevent calls to client.Topic from connect
if(connection != "seeker" && connection != "web")//Invalid connection type.
@@ -282,6 +279,10 @@ GLOBAL_LIST_EMPTY(external_rsc_urls)
prefs.last_id = computer_id //these are gonna be used for banning
fps = prefs.clientfps
+ if(prefs.prefer_old_chat == FALSE)
+ spawn() // Goonchat does some non-instant checks in start()
+ chatOutput.start()
+
if(fexists(roundend_report_file()))
verbs += /client/proc/show_previous_roundend_report
@@ -963,27 +964,20 @@ GLOBAL_LIST_EMPTY(external_rsc_urls)
/client/proc/send_resources()
#if (PRELOAD_RSC == 0)
var/static/next_external_rsc = 0
- if(GLOB.external_rsc_urls && GLOB.external_rsc_urls.len)
- next_external_rsc = WRAP(next_external_rsc+1, 1, GLOB.external_rsc_urls.len+1)
- preload_rsc = GLOB.external_rsc_urls[next_external_rsc]
+ var/list/external_rsc_urls = CONFIG_GET(keyed_list/external_rsc_urls)
+ if(length(external_rsc_urls))
+ next_external_rsc = WRAP(next_external_rsc+1, 1, external_rsc_urls.len+1)
+ preload_rsc = external_rsc_urls[next_external_rsc]
#endif
- //get the common files
- getFiles(
- 'html/search.js',
- 'html/panels.css',
- 'html/browser/common.css',
- 'html/browser/scannernew.css',
- 'html/browser/playeroptions.css',
- )
- spawn (10) //removing this spawn causes all clients to not get verbs.
+
+ spawn (10) //removing this spawn causes all clients to not get verbs. (this can't be addtimer because these assets may be needed before the mc inits)
+
+ //load info on what assets the client has
+ src << browse('code/modules/asset_cache/validate_assets.html', "window=asset_cache_browser")
+
//Precache the client with all other assets slowly, so as to not block other browse() calls
- getFilesSlow(src, SSassets.preload, register_asset = FALSE)
-// #if (PRELOAD_RSC == 0)
-// for (var/name in GLOB.vox_sounds)
-// var/file = GLOB.vox_sounds[name]
-// Export("##action=load_rsc", file)
-// stoplag()
-// #endif
+ if (CONFIG_GET(flag/asset_simple_preload))
+ addtimer(CALLBACK(SSassets.transport, TYPE_PROC_REF(/datum/asset_transport, send_assets_slow), src, SSassets.transport.preload), 5 SECONDS)
//Hook, override it to run code when dir changes
//Like for /atoms, but clients are their own snowflake FUCK
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index b6393a0ebaa..c373f7f8ff0 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -161,7 +161,7 @@ GLOBAL_LIST_INIT(name_adjustments, list())
var/char_accent = "No accent"
/// Tracker to whether the person has ever spawned into the round, for purposes of applying the respawn ban
var/has_spawned = FALSE
-
+ var/prefer_old_chat = FALSE
/datum/preferences/New(client/C)
parent = C
@@ -838,13 +838,13 @@ GLOBAL_LIST_INIT(name_adjustments, list())
dat += "
- If this takes longer than 30 seconds, it will automatically reload a maximum of 5 times.
- If it still doesn't work, use the bug report button at the top right of the window.
-
+ If this takes longer than 30 seconds, it will automatically reload a maximum of 5 times.
+ If it still doesn't work, use the bug report button at the top right of the window.
+
").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
+
\ No newline at end of file
diff --git a/code/modules/goonchat/browserassets/js/json2.min.js b/code/modules/goonchat/browserassets/js/json2.min.js
index d867407f265..16662c50d5b 100644
--- a/code/modules/goonchat/browserassets/js/json2.min.js
+++ b/code/modules/goonchat/browserassets/js/json2.min.js
@@ -1 +1 @@
-"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(t){return 10>t?"0"+t:t}function this_value(){return this.valueOf()}function quote(t){return rx_escapable.lastIndex=0,rx_escapable.test(t)?'"'+t.replace(rx_escapable,function(t){var e=meta[t];return"string"==typeof e?e:"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+t+'"'}function str(t,e){var r,n,o,u,f,a=gap,i=e[t];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(t)),"function"==typeof rep&&(i=rep.call(e,t,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,f=[],"[object Array]"===Object.prototype.toString.apply(i)){for(u=i.length,r=0;u>r;r+=1)f[r]=str(r,i)||"null";return o=0===f.length?"[]":gap?"[\n"+gap+f.join(",\n"+gap)+"\n"+a+"]":"["+f.join(",")+"]",gap=a,o}if(rep&&"object"==typeof rep)for(u=rep.length,r=0;u>r;r+=1)"string"==typeof rep[r]&&(n=rep[r],o=str(n,i),o&&f.push(quote(n)+(gap?": ":":")+o));else for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(o=str(n,i),o&&f.push(quote(n)+(gap?": ":":")+o));return o=0===f.length?"{}":gap?"{\n"+gap+f.join(",\n"+gap)+"\n"+a+"}":"{"+f.join(",")+"}",gap=a,o}}var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},Boolean.prototype.toJSON=this_value,Number.prototype.toJSON=this_value,String.prototype.toJSON=this_value);var gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(t,e,r){var n;if(gap="",indent="","number"==typeof r)for(n=0;r>n;n+=1)indent+=" ";else"string"==typeof r&&(indent=r);if(rep=e,e&&"function"!=typeof e&&("object"!=typeof e||"number"!=typeof e.length))throw new Error("JSON.stringify");return str("",{"":t})}),"function"!=typeof JSON.parse&&(JSON.parse=function(text,reviver){function walk(t,e){var r,n,o=t[e];if(o&&"object"==typeof o)for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(n=walk(o,r),void 0!==n?o[r]=n:delete o[r]);return reviver.call(t,e,o)}var j;if(text=String(text),rx_dangerous.lastIndex=0,rx_dangerous.test(text)&&(text=text.replace(rx_dangerous,function(t){return"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})),rx_one.test(text.replace(rx_two,"@").replace(rx_three,"]").replace(rx_four,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}();
\ No newline at end of file
+"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(t){return 10>t?"0"+t:t}function this_value(){return this.valueOf()}function quote(t){return rx_escapable.lastIndex=0,rx_escapable.test(t)?'"'+t.replace(rx_escapable,function(t){var e=meta[t];return"string"==typeof e?e:"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+t+'"'}function str(t,e){var r,n,o,u,f,a=gap,i=e[t];switch(i&&"object"==typeof i&&"function"==typeof i.toJSON&&(i=i.toJSON(t)),"function"==typeof rep&&(i=rep.call(e,t,i)),typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";if(gap+=indent,f=[],"[object Array]"===Object.prototype.toString.apply(i)){for(u=i.length,r=0;u>r;r+=1)f[r]=str(r,i)||"null";return o=0===f.length?"[]":gap?"[\n"+gap+f.join(",\n"+gap)+"\n"+a+"]":"["+f.join(",")+"]",gap=a,o}if(rep&&"object"==typeof rep)for(u=rep.length,r=0;u>r;r+=1)"string"==typeof rep[r]&&(n=rep[r],o=str(n,i),o&&f.push(quote(n)+(gap?": ":":")+o));else for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(o=str(n,i),o&&f.push(quote(n)+(gap?": ":":")+o));return o=0===f.length?"{}":gap?"{\n"+gap+f.join(",\n"+gap)+"\n"+a+"}":"{"+f.join(",")+"}",gap=a,o}}var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},Boolean.prototype.toJSON=this_value,Number.prototype.toJSON=this_value,String.prototype.toJSON=this_value);var gap,indent,meta,rep;"function"!=typeof JSON.stringify&&(meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(t,e,r){var n;if(gap="",indent="","number"==typeof r)for(n=0;r>n;n+=1)indent+=" ";else"string"==typeof r&&(indent=r);if(rep=e,e&&"function"!=typeof e&&("object"!=typeof e||"number"!=typeof e.length))throw new Error("JSON.stringify");return str("",{"":t})}),"function"!=typeof JSON.parse&&(JSON.parse=function(text,reviver){function walk(t,e){var r,n,o=t[e];if(o&&"object"==typeof o)for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(n=walk(o,r),void 0!==n?o[r]=n:delete o[r]);return reviver.call(t,e,o)}var j;if(text=String(text),rx_dangerous.lastIndex=0,rx_dangerous.test(text)&&(text=text.replace(rx_dangerous,function(t){return"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})),rx_one.test(text.replace(rx_two,"@").replace(rx_three,"]").replace(rx_four,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}();
diff --git a/code/modules/goonchat/browserassets/js/purify.min.js b/code/modules/goonchat/browserassets/js/purify.min.js
new file mode 100644
index 00000000000..0360b41fcb1
--- /dev/null
+++ b/code/modules/goonchat/browserassets/js/purify.min.js
@@ -0,0 +1,3 @@
+/*! @license DOMPurify 2.5.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.5.8/LICENSE */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,n){return t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},t(e,n)}function n(e,r,o){return n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}()?Reflect.construct:function(e,n,r){var o=[null];o.push.apply(o,n);var a=new(Function.bind.apply(e,o));return r&&t(a,r.prototype),a},n.apply(null,arguments)}function r(e){return function(e){if(Array.isArray(e))return o(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return o(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return o(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?n-1:0),o=1;o/gm),q=m(/\${[\w\W]*}/gm),$=m(/^data-[\-\w.\u00B7-\uFFFF]+$/),Y=m(/^aria-[\-\w]+$/),K=m(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),V=m(/^(?:\w+script|data):/i),X=m(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Z=m(/^html$/i),J=m(/^[a-z][.\w]*(-[.\w]+)+$/i),Q=function(){return"undefined"==typeof window?null:window};var ee=function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Q(),o=function(e){return t(e)};if(o.version="2.5.8",o.removed=[],!n||!n.document||9!==n.document.nodeType)return o.isSupported=!1,o;var a=n.document,i=n.document,l=n.DocumentFragment,c=n.HTMLTemplateElement,u=n.Node,m=n.Element,f=n.NodeFilter,p=n.NamedNodeMap,d=void 0===p?n.NamedNodeMap||n.MozNamedAttrMap:p,h=n.HTMLFormElement,g=n.DOMParser,O=n.trustedTypes,ee=m.prototype,te=C(ee,"cloneNode"),ne=C(ee,"nextSibling"),re=C(ee,"childNodes"),oe=C(ee,"parentNode");if("function"==typeof c){var ae=i.createElement("template");ae.content&&ae.content.ownerDocument&&(i=ae.content.ownerDocument)}var ie=function(t,n){if("object"!==e(t)||"function"!=typeof t.createPolicy)return null;var r=null,o="data-tt-policy-suffix";n.currentScript&&n.currentScript.hasAttribute(o)&&(r=n.currentScript.getAttribute(o));var a="dompurify"+(r?"#"+r:"");try{return t.createPolicy(a,{createHTML:function(e){return e},createScriptURL:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+a+" could not be created."),null}}(O,a),le=ie?ie.createHTML(""):"",ce=i,ue=ce.implementation,se=ce.createNodeIterator,me=ce.createDocumentFragment,fe=ce.getElementsByTagName,pe=a.importNode,de={};try{de=L(i).documentMode?i.documentMode:{}}catch(e){}var he={};o.isSupported="function"==typeof oe&&ue&&void 0!==ue.createHTMLDocument&&9!==de;var ge,ye,be=G,Te=W,ve=q,Ne=$,Ee=Y,Ae=V,Se=X,_e=J,we=K,xe=null,Oe=k({},[].concat(r(D),r(R),r(M),r(F),r(H))),ke=null,Le=k({},[].concat(r(z),r(P),r(B),r(j))),Ce=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),De=null,Re=null,Me=!0,Ie=!0,Fe=!1,Ue=!0,He=!1,ze=!0,Pe=!1,Be=!1,je=!1,Ge=!1,We=!1,qe=!1,$e=!0,Ye=!1,Ke=!0,Ve=!1,Xe={},Ze=null,Je=k({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Qe=null,et=k({},["audio","video","img","source","image","track"]),tt=null,nt=k({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),rt="http://www.w3.org/1998/Math/MathML",ot="http://www.w3.org/2000/svg",at="http://www.w3.org/1999/xhtml",it=at,lt=!1,ct=null,ut=k({},[rt,ot,at],N),st=["application/xhtml+xml","text/html"],mt=null,ft=i.createElement("form"),pt=function(e){return e instanceof RegExp||e instanceof Function},dt=function(t){mt&&mt===t||(t&&"object"===e(t)||(t={}),t=L(t),ge=ge=-1===st.indexOf(t.PARSER_MEDIA_TYPE)?"text/html":t.PARSER_MEDIA_TYPE,ye="application/xhtml+xml"===ge?N:v,xe="ALLOWED_TAGS"in t?k({},t.ALLOWED_TAGS,ye):Oe,ke="ALLOWED_ATTR"in t?k({},t.ALLOWED_ATTR,ye):Le,ct="ALLOWED_NAMESPACES"in t?k({},t.ALLOWED_NAMESPACES,N):ut,tt="ADD_URI_SAFE_ATTR"in t?k(L(nt),t.ADD_URI_SAFE_ATTR,ye):nt,Qe="ADD_DATA_URI_TAGS"in t?k(L(et),t.ADD_DATA_URI_TAGS,ye):et,Ze="FORBID_CONTENTS"in t?k({},t.FORBID_CONTENTS,ye):Je,De="FORBID_TAGS"in t?k({},t.FORBID_TAGS,ye):{},Re="FORBID_ATTR"in t?k({},t.FORBID_ATTR,ye):{},Xe="USE_PROFILES"in t&&t.USE_PROFILES,Me=!1!==t.ALLOW_ARIA_ATTR,Ie=!1!==t.ALLOW_DATA_ATTR,Fe=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Ue=!1!==t.ALLOW_SELF_CLOSE_IN_ATTR,He=t.SAFE_FOR_TEMPLATES||!1,ze=!1!==t.SAFE_FOR_XML,Pe=t.WHOLE_DOCUMENT||!1,Ge=t.RETURN_DOM||!1,We=t.RETURN_DOM_FRAGMENT||!1,qe=t.RETURN_TRUSTED_TYPE||!1,je=t.FORCE_BODY||!1,$e=!1!==t.SANITIZE_DOM,Ye=t.SANITIZE_NAMED_PROPS||!1,Ke=!1!==t.KEEP_CONTENT,Ve=t.IN_PLACE||!1,we=t.ALLOWED_URI_REGEXP||we,it=t.NAMESPACE||at,Ce=t.CUSTOM_ELEMENT_HANDLING||{},t.CUSTOM_ELEMENT_HANDLING&&pt(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ce.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&pt(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ce.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ce.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),He&&(Ie=!1),We&&(Ge=!0),Xe&&(xe=k({},r(H)),ke=[],!0===Xe.html&&(k(xe,D),k(ke,z)),!0===Xe.svg&&(k(xe,R),k(ke,P),k(ke,j)),!0===Xe.svgFilters&&(k(xe,M),k(ke,P),k(ke,j)),!0===Xe.mathMl&&(k(xe,F),k(ke,B),k(ke,j))),t.ADD_TAGS&&(xe===Oe&&(xe=L(xe)),k(xe,t.ADD_TAGS,ye)),t.ADD_ATTR&&(ke===Le&&(ke=L(ke)),k(ke,t.ADD_ATTR,ye)),t.ADD_URI_SAFE_ATTR&&k(tt,t.ADD_URI_SAFE_ATTR,ye),t.FORBID_CONTENTS&&(Ze===Je&&(Ze=L(Ze)),k(Ze,t.FORBID_CONTENTS,ye)),Ke&&(xe["#text"]=!0),Pe&&k(xe,["html","head","body"]),xe.table&&(k(xe,["tbody"]),delete De.tbody),s&&s(t),mt=t)},ht=k({},["mi","mo","mn","ms","mtext"]),gt=k({},["annotation-xml"]),yt=k({},["title","style","font","a","script"]),bt=k({},R);k(bt,M),k(bt,I);var Tt=k({},F);k(Tt,U);var vt=function(e){T(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=le}catch(t){e.remove()}}},Nt=function(e,t){try{T(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){T(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!ke[e])if(Ge||We)try{vt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Et=function(e){var t,n;if(je)e=""+e;else{var r=E(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===ge&&it===at&&(e=''+e+"");var o=ie?ie.createHTML(e):e;if(it===at)try{t=(new g).parseFromString(o,ge)}catch(e){}if(!t||!t.documentElement){t=ue.createDocument(it,"template",null);try{t.documentElement.innerHTML=lt?le:o}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(i.createTextNode(n),a.childNodes[0]||null),it===at?fe.call(t,Pe?"html":"body")[0]:Pe?t.documentElement:a},At=function(e){return se.call(e.ownerDocument||e,e,f.SHOW_ELEMENT|f.SHOW_COMMENT|f.SHOW_TEXT|f.SHOW_PROCESSING_INSTRUCTION|f.SHOW_CDATA_SECTION,null,!1)},St=function(e){return e instanceof h&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof d)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},_t=function(t){return"object"===e(u)?t instanceof u:t&&"object"===e(t)&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},wt=function(e,t,n){he[e]&&y(he[e],(function(e){e.call(o,t,n,mt)}))},xt=function(e){var t;if(wt("beforeSanitizeElements",e,null),St(e))return vt(e),!0;if(w(/[\u0080-\uFFFF]/,e.nodeName))return vt(e),!0;var n=ye(e.nodeName);if(wt("uponSanitizeElement",e,{tagName:n,allowedTags:xe}),e.hasChildNodes()&&!_t(e.firstElementChild)&&(!_t(e.content)||!_t(e.content.firstElementChild))&&w(/<[/\w]/g,e.innerHTML)&&w(/<[/\w]/g,e.textContent))return vt(e),!0;if("select"===n&&w(/=0;--i){var l=te(a[i],!0);l.__removalCount=(e.__removalCount||0)+1,r.insertBefore(l,ne(e))}}return vt(e),!0}return e instanceof m&&!function(e){var t=oe(e);t&&t.tagName||(t={namespaceURI:it,tagName:"template"});var n=v(e.tagName),r=v(t.tagName);return!!ct[e.namespaceURI]&&(e.namespaceURI===ot?t.namespaceURI===at?"svg"===n:t.namespaceURI===rt?"svg"===n&&("annotation-xml"===r||ht[r]):Boolean(bt[n]):e.namespaceURI===rt?t.namespaceURI===at?"math"===n:t.namespaceURI===ot?"math"===n&>[r]:Boolean(Tt[n]):e.namespaceURI===at?!(t.namespaceURI===ot&&!gt[r])&&!(t.namespaceURI===rt&&!ht[r])&&!Tt[n]&&(yt[n]||!bt[n]):!("application/xhtml+xml"!==ge||!ct[e.namespaceURI]))}(e)?(vt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!w(/<\/no(script|embed|frames)/i,e.innerHTML)?(He&&3===e.nodeType&&(t=e.textContent,t=A(t,be," "),t=A(t,Te," "),t=A(t,ve," "),e.textContent!==t&&(T(o.removed,{element:e.cloneNode()}),e.textContent=t)),wt("afterSanitizeElements",e,null),!1):(vt(e),!0)},Ot=function(e,t,n){if($e&&("id"===t||"name"===t)&&(n in i||n in ft))return!1;if(Ie&&!Re[t]&&w(Ne,t));else if(Me&&w(Ee,t));else if(!ke[t]||Re[t]){if(!(kt(e)&&(Ce.tagNameCheck instanceof RegExp&&w(Ce.tagNameCheck,e)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(e))&&(Ce.attributeNameCheck instanceof RegExp&&w(Ce.attributeNameCheck,t)||Ce.attributeNameCheck instanceof Function&&Ce.attributeNameCheck(t))||"is"===t&&Ce.allowCustomizedBuiltInElements&&(Ce.tagNameCheck instanceof RegExp&&w(Ce.tagNameCheck,n)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))))return!1}else if(tt[t]);else if(w(we,A(n,Se,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==S(n,"data:")||!Qe[e]){if(Fe&&!w(Ae,A(n,Se,"")));else if(n)return!1}else;return!0},kt=function(e){return"annotation-xml"!==e&&E(e,_e)},Lt=function(t){var n,r,a,i;wt("beforeSanitizeAttributes",t,null);var l=t.attributes;if(l&&!St(t)){var c={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:ke};for(i=l.length;i--;){var u=n=l[i],s=u.name,m=u.namespaceURI;if(r="value"===s?n.value:_(n.value),a=ye(s),c.attrName=a,c.attrValue=r,c.keepAttr=!0,c.forceKeepAttr=void 0,wt("uponSanitizeAttribute",t,c),r=c.attrValue,!c.forceKeepAttr&&(Nt(s,t),c.keepAttr))if(Ue||!w(/\/>/i,r)){He&&(r=A(r,be," "),r=A(r,Te," "),r=A(r,ve," "));var f=ye(t.nodeName);if(Ot(f,a,r))if(!Ye||"id"!==a&&"name"!==a||(Nt(s,t),r="user-content-"+r),ze&&w(/((--!?|])>)|<\/(style|title)/i,r))Nt(s,t);else{if(ie&&"object"===e(O)&&"function"==typeof O.getAttributeType)if(m);else switch(O.getAttributeType(f,a)){case"TrustedHTML":r=ie.createHTML(r);break;case"TrustedScriptURL":r=ie.createScriptURL(r)}try{m?t.setAttributeNS(m,s,r):t.setAttribute(s,r),St(t)?vt(t):b(o.removed)}catch(e){}}}else Nt(s,t)}wt("afterSanitizeAttributes",t,null)}},Ct=function e(t){var n,r=At(t);for(wt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)wt("uponSanitizeShadowNode",n,null),xt(n),Lt(n),n.content instanceof l&&e(n.content);wt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(t){var r,i,c,s,m,f=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if((lt=!t)&&(t="\x3c!--\x3e"),"string"!=typeof t&&!_t(t)){if("function"!=typeof t.toString)throw x("toString is not a function");if("string"!=typeof(t=t.toString()))throw x("dirty is not a string, aborting")}if(!o.isSupported){if("object"===e(n.toStaticHTML)||"function"==typeof n.toStaticHTML){if("string"==typeof t)return n.toStaticHTML(t);if(_t(t))return n.toStaticHTML(t.outerHTML)}return t}if(Be||dt(f),o.removed=[],"string"==typeof t&&(Ve=!1),Ve){if(t.nodeName){var p=ye(t.nodeName);if(!xe[p]||De[p])throw x("root node is forbidden and cannot be sanitized in-place")}}else if(t instanceof u)1===(i=(r=Et("\x3c!----\x3e")).ownerDocument.importNode(t,!0)).nodeType&&"BODY"===i.nodeName||"HTML"===i.nodeName?r=i:r.appendChild(i);else{if(!Ge&&!He&&!Pe&&-1===t.indexOf("<"))return ie&&qe?ie.createHTML(t):t;if(!(r=Et(t)))return Ge?null:qe?le:""}r&&je&&vt(r.firstChild);for(var d=At(Ve?t:r);c=d.nextNode();)3===c.nodeType&&c===s||(xt(c),Lt(c),c.content instanceof l&&Ct(c.content),s=c);if(s=null,Ve)return t;if(Ge){if(We)for(m=me.call(r.ownerDocument);r.firstChild;)m.appendChild(r.firstChild);else m=r;return(ke.shadowroot||ke.shadowrootmod)&&(m=pe.call(a,m,!0)),m}var h=Pe?r.outerHTML:r.innerHTML;return Pe&&xe["!doctype"]&&r.ownerDocument&&r.ownerDocument.doctype&&r.ownerDocument.doctype.name&&w(Z,r.ownerDocument.doctype.name)&&(h="\n"+h),He&&(h=A(h,be," "),h=A(h,Te," "),h=A(h,ve," ")),ie&&qe?ie.createHTML(h):h},o.setConfig=function(e){dt(e),Be=!0},o.clearConfig=function(){mt=null,Be=!1},o.isValidAttribute=function(e,t,n){mt||dt({});var r=ye(e),o=ye(t);return Ot(r,o,n)},o.addHook=function(e,t){"function"==typeof t&&(he[e]=he[e]||[],T(he[e],t))},o.removeHook=function(e){if(he[e])return b(he[e])},o.removeHooks=function(e){he[e]&&(he[e]=[])},o.removeAllHooks=function(){he={}},o}();return ee}));
+//# sourceMappingURL=purify.min.js.map
diff --git a/code/modules/mob/dead/new_player/poll.dm b/code/modules/mob/dead/new_player/poll.dm
index 15664f9ca53..538ee8946eb 100644
--- a/code/modules/mob/dead/new_player/poll.dm
+++ b/code/modules/mob/dead/new_player/poll.dm
@@ -32,8 +32,6 @@
poll_player_rating(poll)
if(POLLTYPE_MULTI)
poll_player_multi(poll)
- if(POLLTYPE_IRV)
- poll_player_irv(poll)
/**
* Shows voting window for an option type poll, listing its options and relevant details.
@@ -215,94 +213,6 @@
output += "
"
src << browse(jointext(output, ""),"window=playerpoll;size=500x300")
-/**
- * Shows voting window for an IRV type poll, listing its options and relevant details.
- *
- * If already voted on, the options are sorted how a player voted for them, otherwise they are randomly shuffled.
- *
- */
-/mob/dead/new_player/proc/poll_player_irv(datum/poll_question/poll)
- var/datum/asset/irv_assets = get_asset_datum(/datum/asset/group/IRV)
- irv_assets.send(src)
- var/datum/DBQuery/query_irv_get_votes = SSdbcore.NewQuery({"
- SELECT optionid FROM [format_table_name("poll_vote")]
- WHERE pollid = :pollid AND ckey = :ckey AND deleted = 0
- "}, list("pollid" = poll.poll_id, "ckey" = ckey))
- if(!query_irv_get_votes.warn_execute())
- qdel(query_irv_get_votes)
- return
- var/list/voted_for = list()
- while(query_irv_get_votes.NextRow())
- voted_for += text2num(query_irv_get_votes.item[1])
- qdel(query_irv_get_votes)
- var/list/prepared_options = list()
- //if they've already voted we use the order they voted in plus a shuffle of any options they haven't voted for, if any
- if(length(voted_for))
- for(var/vote_id in voted_for)
- for(var/o in poll.options)
- var/datum/poll_option/option = o
- if(option.option_id == vote_id)
- prepared_options += option
- var/list/shuffle_options = poll.options - prepared_options
- if(length(shuffle_options))
- shuffle_options = shuffle(shuffle_options)
- for(var/shuffled in shuffle_options)
- prepared_options += shuffled
- //otherwise just shuffle the options
- else
- prepared_options = shuffle(poll.options)
- var/list/output = list({"
-
-
-
-
-
-
-
-
Player pollQuestion: [poll.question] "})
- if(poll.subtitle)
- output += "[poll.subtitle] "
- output += "Poll runs from [poll.start_datetime] until [poll.end_datetime] "
- if(poll.allow_revoting)
- output += "Revoting is enabled."
- output += "Please sort the options in the order of most preferred to least preferred