diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm
index 55aea1682e..3b3067bddf 100644
--- a/code/__defines/subsystems.dm
+++ b/code/__defines/subsystems.dm
@@ -62,6 +62,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
#define INIT_ORDER_DEFAULT 0
#define INIT_ORDER_LIGHTING 0
#define INIT_ORDER_AIR -1
+#define INIT_ORDER_ASSETS -3
#define INIT_ORDER_PLANETS -4
#define INIT_ORDER_HOLOMAPS -5
#define INIT_ORDER_OVERLAY -6
diff --git a/code/controllers/subsystems/assets.dm b/code/controllers/subsystems/assets.dm
new file mode 100644
index 0000000000..cd531db614
--- /dev/null
+++ b/code/controllers/subsystems/assets.dm
@@ -0,0 +1,17 @@
+SUBSYSTEM_DEF(assets)
+ name = "Assets"
+ init_order = INIT_ORDER_ASSETS
+ flags = SS_NO_FIRE
+ var/list/cache = list()
+ var/list/preload = list()
+
+/datum/controller/subsystem/assets/Initialize(timeofday)
+ for(var/type in typesof(/datum/asset) - list(/datum/asset, /datum/asset/simple))
+ var/datum/asset/A = new type()
+ A.register()
+
+ preload = cache.Copy() //don't preload assets generated during the round
+
+ for(var/client/C in GLOB.clients)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/getFilesSlow, C, preload, FALSE), 10)
+ return ..()
\ No newline at end of file
diff --git a/code/controllers/subsystems/nanoui.dm b/code/controllers/subsystems/nanoui.dm
index 520973514d..78f47e8c7b 100644
--- a/code/controllers/subsystems/nanoui.dm
+++ b/code/controllers/subsystems/nanoui.dm
@@ -24,7 +24,7 @@ SUBSYSTEM_DEF(nanoui)
for(var/filename in filenames)
if(copytext(filename, length(filename)) != "/") // filenames which end in "/" are actually directories, which we want to ignore
if(fexists(path + filename))
- asset_files.Add(fcopy_rsc(path + filename)) // add this file to asset_files for sending to clients when they connect
+ asset_files[filename] = fcopy_rsc(path + filename) // add this file to asset_files for sending to clients when they connect
.=..()
for(var/i in GLOB.clients)
send_resources(i)
@@ -49,5 +49,4 @@ SUBSYSTEM_DEF(nanoui)
/datum/controller/subsystem/nanoui/proc/send_resources(client)
if(!subsystem_initialized)
return
- for(var/file in asset_files)
- client << browse_rsc(file) // send the file to the client
+ getFilesSlow(client, asset_files)
\ No newline at end of file
diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm
index 771684775f..05f50f3eff 100644
--- a/code/game/objects/items/devices/PDA/PDA.dm
+++ b/code/game/objects/items/devices/PDA/PDA.dm
@@ -647,8 +647,9 @@ var/global/list/obj/item/device/pda/PDAs = list()
// auto update every Master Controller tick
ui.set_auto_update(auto_update)
-//NOTE: graphic resources are loaded on client login
/obj/item/device/pda/attack_self(mob/user as mob)
+ var/datum/asset/assets = get_asset_datum(/datum/asset/simple/pda)
+ assets.send(user)
user.set_machine(src)
diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm
new file mode 100644
index 0000000000..0e2f2f9f21
--- /dev/null
+++ b/code/modules/client/asset_cache.dm
@@ -0,0 +1,287 @@
+/*
+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(var/client/client, var/asset_name, var/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
+
+ client << browse_rsc(SSassets.cache[asset_name], asset_name)
+ if(!verify) // Can't access the asset cache browser, rip.
+ 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()
+ sleep(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(var/client/client, var/list/asset_list, var/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)
+ 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()
+ sleep(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(var/client/client, var/list/files, var/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)
+ sleep(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(var/asset_name, var/asset)
+ SSassets.cache[asset_name] = asset
+
+//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
+/var/global/list/asset_datums = list()
+
+//get a assetdatum or make a new one
+/proc/get_asset_datum(var/type)
+ if(!(type in asset_datums))
+ return new type()
+ return asset_datums[type]
+
+/datum/asset/New()
+ asset_datums[type] = src
+
+/datum/asset/proc/register()
+ return
+
+/datum/asset/proc/send(client)
+ return
+
+//If you don't need anything complicated.
+/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)
+
+
+//DEFINITIONS FOR ASSET DATUMS START HERE.
+/datum/asset/simple/pda
+ assets = list(
+ "pda_atmos.png" = 'icons/pda_icons/pda_atmos.png',
+ "pda_back.png" = 'icons/pda_icons/pda_back.png',
+ "pda_bell.png" = 'icons/pda_icons/pda_bell.png',
+ "pda_blank.png" = 'icons/pda_icons/pda_blank.png',
+ "pda_boom.png" = 'icons/pda_icons/pda_boom.png',
+ "pda_bucket.png" = 'icons/pda_icons/pda_bucket.png',
+ "pda_crate.png" = 'icons/pda_icons/pda_crate.png',
+ "pda_cuffs.png" = 'icons/pda_icons/pda_cuffs.png',
+ "pda_eject.png" = 'icons/pda_icons/pda_eject.png',
+ "pda_exit.png" = 'icons/pda_icons/pda_exit.png',
+ "pda_flashlight.png" = 'icons/pda_icons/pda_flashlight.png',
+ "pda_honk.png" = 'icons/pda_icons/pda_honk.png',
+ "pda_mail.png" = 'icons/pda_icons/pda_mail.png',
+ "pda_medical.png" = 'icons/pda_icons/pda_medical.png',
+ "pda_menu.png" = 'icons/pda_icons/pda_menu.png',
+ "pda_mule.png" = 'icons/pda_icons/pda_mule.png',
+ "pda_notes.png" = 'icons/pda_icons/pda_notes.png',
+ "pda_power.png" = 'icons/pda_icons/pda_power.png',
+ "pda_rdoor.png" = 'icons/pda_icons/pda_rdoor.png',
+ "pda_reagent.png" = 'icons/pda_icons/pda_reagent.png',
+ "pda_refresh.png" = 'icons/pda_icons/pda_refresh.png',
+ "pda_scanner.png" = 'icons/pda_icons/pda_scanner.png',
+ "pda_signaler.png" = 'icons/pda_icons/pda_signaler.png',
+ "pda_status.png" = 'icons/pda_icons/pda_status.png'
+ )
+
+/datum/asset/simple/generic
+ assets = list(
+ "search.js" = 'html/search.js',
+ "panels.css" = 'html/panels.css',
+ "loading.gif" = 'html/images/loading.gif',
+ "ntlogo.png" = 'html/images/ntlogo.png',
+ "sglogo.png" = 'html/images/sglogo.png',
+ "talisman.png" = 'html/images/talisman.png',
+ "paper_bg.png" = 'html/images/paper_bg.png',
+ "no_image32.png" = 'html/images/no_image32.png',
+ "sos_1.png" = 'icons/spideros_icons/sos_1.png',
+ "sos_2.png" = 'icons/spideros_icons/sos_2.png',
+ "sos_3.png" = 'icons/spideros_icons/sos_3.png',
+ "sos_4.png" = 'icons/spideros_icons/sos_4.png',
+ "sos_5.png" = 'icons/spideros_icons/sos_5.png',
+ "sos_6.png" = 'icons/spideros_icons/sos_6.png',
+ "sos_7.png" = 'icons/spideros_icons/sos_7.png',
+ "sos_8.png" = 'icons/spideros_icons/sos_8.png',
+ "sos_9.png" = 'icons/spideros_icons/sos_9.png',
+ "sos_10.png" = 'icons/spideros_icons/sos_10.png',
+ "sos_11.png" = 'icons/spideros_icons/sos_11.png',
+ "sos_12.png" = 'icons/spideros_icons/sos_12.png',
+ "sos_13.png" = 'icons/spideros_icons/sos_13.png',
+ "sos_14.png" = 'icons/spideros_icons/sos_14.png'
+ )
+
+/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',
+ "map-pencil.png" = 'html/map-pencil.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',
+ "changelog.css" = 'html/changelog.css',
+ "changelog.js" = 'html/changelog.js',
+ "changelog.html" = 'html/changelog.html'
+ )
+
+/datum/asset/nanoui
+ var/list/common = list()
+
+ var/list/common_dirs = list(
+ "nano/css/",
+ "nano/images/",
+ "nano/js/"
+ )
+ var/list/uncommon_dirs = list(
+ "nano/templates/"
+ )
+
+/datum/asset/nanoui/register()
+ // Crawl the directories to find files.
+ for(var/path in common_dirs)
+ var/list/filenames = flist(path)
+ for(var/filename in filenames)
+ if(copytext(filename, length(filename)) != "/") // Ignore directories.
+ if(fexists(path + filename))
+ common[filename] = fcopy_rsc(path + filename)
+ register_asset(filename, common[filename])
+ for(var/path in uncommon_dirs)
+ var/list/filenames = flist(path)
+ for(var/filename in filenames)
+ if(copytext(filename, length(filename)) != "/") // Ignore directories.
+ if(fexists(path + filename))
+ register_asset(filename, fcopy_rsc(path + filename))
+
+/datum/asset/nanoui/send(client, uncommon)
+ if(!islist(uncommon))
+ uncommon = list(uncommon)
+
+ send_asset_list(client, uncommon)
+ send_asset_list(client, common)
diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm
index aac5e440a1..1c16ec249c 100644
--- a/code/modules/client/client procs.dm
+++ b/code/modules/client/client procs.dm
@@ -34,6 +34,11 @@
#endif
+ if(href_list["asset_cache_confirm_arrival"])
+ var/job = text2num(href_list["asset_cache_confirm_arrival"])
+ completed_asset_jobs += job
+ return
+
//search the href for script injection
if( findtext(href,"