diff --git a/.travis.yml b/.travis.yml
index 432d409314..bfdb40e23b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,9 +6,11 @@ env:
BYOND_MAJOR="510"
BYOND_MINOR="1346"
MACRO_COUNT=986
+ NODE_VERSION="4"
cache:
directories:
+ - tgui/node_modules
- $HOME/BYOND-${BYOND_MAJOR}.${BYOND_MINOR}
addons:
@@ -19,10 +21,13 @@ addons:
- libstdc++6:i386
before_script:
+ - cd tgui && npm install && cd ..
- chmod +x ./install-byond.sh
- ./install-byond.sh
install:
+ - rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $NODE_VERSION
+ - npm install -g gulp-cli
- pip install --user PyYaml -q
- pip install --user beautifulsoup4 -q
@@ -34,6 +39,8 @@ script:
- awk -f tools/indentation.awk **/*.dm
- md5sum -c - <<< "88490b460c26947f5ec1ab1bb9fa9f17 *html/changelogs/example.yml"
- (num=`grep -E '\\\\(red|blue|green|black|b|i[^mc])' **/*.dm | wc -l`; echo "$num escapes (expecting ${MACRO_COUNT} or less)"; [ $num -le ${MACRO_COUNT} ])
+ - cd tgui && gulp
+ - cd ..
- source $HOME/BYOND-${BYOND_MAJOR}.${BYOND_MINOR}/byond/bin/byondsetup
- python tools/TagMatcher/tag-matcher.py ../..
- echo "#define UNIT_TEST 1" > code/_unit_tests.dm
diff --git a/code/__defines/tgui.dm b/code/__defines/tgui.dm
new file mode 100644
index 0000000000..5ba0096d1b
--- /dev/null
+++ b/code/__defines/tgui.dm
@@ -0,0 +1,4 @@
+#define UI_INTERACTIVE 2 // Green/Interactive
+#define UI_UPDATE 1 // Orange/Updates Only
+#define UI_DISABLED 0 // Red/Disabled
+#define UI_CLOSE -1 // Closed
\ No newline at end of file
diff --git a/code/__defines/tick.dm b/code/__defines/tick.dm
new file mode 100644
index 0000000000..71a63af628
--- /dev/null
+++ b/code/__defines/tick.dm
@@ -0,0 +1,5 @@
+#define TICK_LIMIT_RUNNING 90
+#define TICK_LIMIT_TO_RUN 85
+
+#define TICK_CHECK ( world.tick_usage > TICK_LIMIT_RUNNING ? stoplag() : 0 )
+#define CHECK_TICK if(world.tick_usage > TICK_LIMIT_RUNNING) stoplag()
\ No newline at end of file
diff --git a/code/_compatibility/509/text.dm b/code/_compatibility/509/text.dm
index c22790ccb8..7e17e82795 100644
--- a/code/_compatibility/509/text.dm
+++ b/code/_compatibility/509/text.dm
@@ -1,6 +1,22 @@
#if DM_VERSION < 510
+//Case Sensitive!
+/proc/text2listEx(text, delimiter="\n")
+ var/delim_len = length(delimiter)
+ if(delim_len < 1) return list(text)
+ . = list()
+ var/last_found = 1
+ var/found
+ do
+ found = findtextEx(text, delimiter, last_found, 0)
+ . += copytext(text, last_found, found)
+ last_found = found + delim_len
+ while(found)
+
/proc/replacetext(text, find, replacement)
return jointext(splittext(text, find), replacement)
+/proc/replacetextEx(text, find, replacement)
+ return jointext(text2listEx(text, find), replacement)
+
#endif
\ No newline at end of file
diff --git a/code/_helpers/atom_movables.dm b/code/_helpers/atom_movables.dm
new file mode 100644
index 0000000000..1dc8ba2749
--- /dev/null
+++ b/code/_helpers/atom_movables.dm
@@ -0,0 +1,28 @@
+/proc/get_turf_pixel(atom/movable/AM)
+ if(!istype(AM))
+ return
+
+ //Find AM's matrix so we can use it's X/Y pixel shifts
+ var/matrix/M = matrix(AM.transform)
+
+ var/pixel_x_offset = AM.pixel_x + M.get_x_shift()
+ var/pixel_y_offset = AM.pixel_y + M.get_y_shift()
+
+ //Irregular objects
+ if(AM.bound_height != world.icon_size || AM.bound_width != world.icon_size)
+ var/icon/AMicon = icon(AM.icon, AM.icon_state)
+ pixel_x_offset += ((AMicon.Width()/world.icon_size)-1)*(world.icon_size*0.5)
+ pixel_y_offset += ((AMicon.Height()/world.icon_size)-1)*(world.icon_size*0.5)
+ qdel(AMicon)
+
+ //DY and DX
+ var/rough_x = round(round(pixel_x_offset,world.icon_size)/world.icon_size)
+ var/rough_y = round(round(pixel_y_offset,world.icon_size)/world.icon_size)
+
+ //Find coordinates
+ var/turf/T = get_turf(AM) //use AM's turfs, as it's coords are the same as AM's AND AM's coords are lost if it is inside another atom
+ var/final_x = T.x + rough_x
+ var/final_y = T.y + rough_y
+
+ if(final_x || final_y)
+ return locate(final_x, final_y, T.z)
\ No newline at end of file
diff --git a/code/_helpers/lists.dm b/code/_helpers/lists.dm
index d4e6710b35..cd4f1d2f4c 100644
--- a/code/_helpers/lists.dm
+++ b/code/_helpers/lists.dm
@@ -323,6 +323,30 @@ proc/listclearnulls(list/list)
return (result + L.Copy(Li, 0))
return (result + R.Copy(Ri, 0))
+/proc/sortByVar(var/list/L, var/key)
+ if(L.len < 2)
+ return L
+ var/middle = L.len / 2 + 1
+ return mergeVaredLists(sortByVar(L.Copy(0, middle), key), sortByVar(L.Copy(middle), key), key)
+
+/proc/mergeVaredLists(var/list/L, var/list/R, var/key)
+ var/Li=1
+ var/Ri=1
+ var/list/result = new()
+ while(Li <= L.len && Ri <= R.len)
+ var/datum/LO = L[Li]
+ var/datum/RO = R[Ri]
+ if(LO.vars[key] > RO.vars[key])
+ // Works around list += list2 merging lists; it's not pretty but it works
+ result += "temp item"
+ result[result.len] = R[Ri++]
+ else
+ result += "temp item"
+ result[result.len] = L[Li++]
+
+ if(Li <= L.len)
+ return (result + L.Copy(Li, 0))
+ return (result + R.Copy(Ri, 0))
//Mergesort: any value in a list, preserves key=value structure
/proc/sortAssoc(var/list/L)
diff --git a/code/_helpers/matrices.dm b/code/_helpers/matrices.dm
index abb0366382..52edb17088 100644
--- a/code/_helpers/matrices.dm
+++ b/code/_helpers/matrices.dm
@@ -15,3 +15,11 @@
animate(src, transform = m120, time = speed, loops)
animate(transform = m240, time = speed)
animate(transform = m360, time = speed)
+
+//The X pixel offset of this matrix
+/matrix/proc/get_x_shift()
+ . = c
+
+//The Y pixel offset of this matrix
+/matrix/proc/get_y_shift()
+ . = f
\ No newline at end of file
diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm
index d8623fa3eb..ff349379b1 100644
--- a/code/_helpers/unsorted.dm
+++ b/code/_helpers/unsorted.dm
@@ -1315,3 +1315,15 @@ var/mob/dview/dview_mob = new
tY = max(1, min(world.maxy, origin.y + (text2num(tY) - (world.view + 1))))
return locate(tX, tY, tZ)
+//Key thing that stops lag. Cornerstone of performance in ss13, Just sitting here, in unsorted.dm.
+/proc/stoplag()
+ . = 1
+ sleep(world.tick_lag)
+ if(world.tick_usage > TICK_LIMIT_TO_RUN) //woke up, still not enough tick, sleep for more.
+ . += 2
+ sleep(world.tick_lag*2)
+ if(world.tick_usage > TICK_LIMIT_TO_RUN) //woke up, STILL not enough tick, sleep for more.
+ . += 4
+ sleep(world.tick_lag*4)
+ //you might be thinking of adding more steps to this, or making it use a loop and a counter var
+ // not worth it.
\ No newline at end of file
diff --git a/code/controllers/Processes/garbage.dm b/code/controllers/Processes/garbage.dm
index 2d56dde1a2..b993c46222 100644
--- a/code/controllers/Processes/garbage.dm
+++ b/code/controllers/Processes/garbage.dm
@@ -66,6 +66,9 @@ world/loop_checks = 0
testing("GC: [refID] old enough to test: GCd_at_time: [GCd_at_time] time_to_kill: [time_to_kill] current: [world.time]")
#endif
if(A && A.gcDestroyed == GCd_at_time) // So if something else coincidently gets the same ref, it's not deleted by mistake
+ #ifdef GC_FINDREF
+ LocateReferences(A)
+ #endif
// Something's still referring to the qdel'd object. Kill it.
testing("GC: -- \ref[A] | [A.type] was unable to be GC'd and was deleted --")
logging["[A.type]"]++
@@ -88,29 +91,47 @@ world/loop_checks = 0
#undef GC_COLLECTIONS_PER_TICK
#ifdef GC_FINDREF
-/datum/controller/process/garbage_collector/proc/LookForRefs(var/datum/D, var/list/targ)
+
+/datum/controller/process/garbage_collector/proc/LocateReferences(var/atom/A)
+ testing("GC: Attempting to locate references to [A] | [A.type]. This is a potentially long-running operation.")
+ if(istype(A))
+ if(A.loc != null)
+ testing("GC: [A] | [A.type] is located in [A.loc] instead of null")
+ if(A.contents.len)
+ testing("GC: [A] | [A.type] has contents: [jointext(A.contents)]")
+ var/ref_count = 0
+ for(var/atom/atom)
+ ref_count += LookForRefs(atom, A)
+ for(var/datum/datum) // This is strictly /datum, not subtypes.
+ ref_count += LookForRefs(datum, A)
+ for(var/client/client)
+ ref_count += LookForRefs(client, A)
+ var/message = "GC: References found to [A] | [A.type]: [ref_count]."
+ if(!ref_count)
+ message += " Has likely been supplied as an 'in list' argment to a proc."
+ testing(message)
+
+/datum/controller/process/garbage_collector/proc/LookForRefs(var/datum/D, var/datum/A)
. = 0
for(var/V in D.vars)
if(V == "contents")
continue
- if(istype(D.vars[V], /atom))
- var/atom/A = D.vars[V]
- if(A in targ)
+ if(!islist(D.vars[V]))
+ if(D.vars[V] == A)
testing("GC: [A] | [A.type] referenced by [D] | [D.type], var [V]")
. += 1
- else if(islist(D.vars[V]))
- . += LookForListRefs(D.vars[V], targ, D, V)
+ else
+ . += LookForListRefs(D.vars[V], A, D, V)
-/datum/controller/process/garbage_collector/proc/LookForListRefs(var/list/L, var/list/targ, var/datum/D, var/V)
+/datum/controller/process/garbage_collector/proc/LookForListRefs(var/list/L, var/datum/A, var/datum/D, var/V)
. = 0
for(var/F in L)
- if(istype(F, /atom))
- var/atom/A = F
- if(A in targ)
+ if(!islist(F))
+ if(F == A || L[F] == A)
testing("GC: [A] | [A.type] referenced by [D] | [D.type], list [V]")
. += 1
- if(islist(F))
- . += LookForListRefs(F, targ, D, "[F] in list [V]")
+ else
+ . += LookForListRefs(F, A, D, "[F] in list [V]")
#endif
/datum/controller/process/garbage_collector/proc/AddTrash(datum/A)
diff --git a/code/controllers/Processes/tgui.dm b/code/controllers/Processes/tgui.dm
new file mode 100644
index 0000000000..4eef090f2e
--- /dev/null
+++ b/code/controllers/Processes/tgui.dm
@@ -0,0 +1,28 @@
+var/global/datum/controller/process/tgui/tgui_process
+
+/datum/controller/process/tgui
+ var/list/tg_open_uis = list() // A list of open UIs, grouped by src_object and ui_key.
+ var/list/processing_uis = list() // A list of processing UIs, ungrouped.
+ var/basehtml // The HTML base used for all UIs.
+
+/datum/controller/process/tgui/setup()
+ name = "tgui"
+ schedule_interval = 10 // every 2 seconds
+ start_delay = 23
+
+ basehtml = file2text('tgui/tgui.html') // Read the HTML from disk.
+ tgui_process = src
+
+/datum/controller/process/tgui/doWork()
+ for(var/gui in processing_uis)
+ var/datum/tgui/ui = gui
+ if(ui && ui.user && ui.src_object)
+ ui.process()
+ SCHECK
+ continue
+ processing_uis.Remove(ui)
+ SCHECK
+
+/datum/controller/process/tgui/statProcess()
+ ..()
+ stat(null, "[tgui_process.processing_uis.len] UI\s")
\ No newline at end of file
diff --git a/code/datums/weakref.dm b/code/datums/weakref.dm
new file mode 100644
index 0000000000..3c04580c40
--- /dev/null
+++ b/code/datums/weakref.dm
@@ -0,0 +1,31 @@
+/datum
+ var/weakref
+
+/datum/Destroy()
+ weakref = null // Clear this reference to ensure it's kept for as brief duration as possible.
+ . = ..()
+
+//obtain a weak reference to a datum
+/proc/weakref(datum/D)
+ if(D.gcDestroyed)
+ return
+ if(!D.weakref)
+ D.weakref = new /datum/weakref(D)
+ return D.weakref
+
+/datum/weakref
+ var/ref
+
+/datum/weakref/New(datum/D)
+ ref = "\ref[D]"
+
+/datum/weakref/Destroy()
+ // A weakref datum should not be manually destroyed as it is a shared resource,
+ // rather it should be automatically collected by the BYOND GC when all references are gone.
+ return 0
+
+/datum/weakref/proc/resolve()
+ var/datum/D = locate(ref)
+ if(D && D.weakref == src)
+ return D
+ return null
\ No newline at end of file
diff --git a/code/game/machinery/atmoalter/canister.dm b/code/game/machinery/atmoalter/canister.dm
index 80b33cfaa0..f24e2fa388 100644
--- a/code/game/machinery/atmoalter/canister.dm
+++ b/code/game/machinery/atmoalter/canister.dm
@@ -265,20 +265,17 @@ update_flag
return src.attack_hand(user)
/obj/machinery/portable_atmospherics/canister/attack_hand(var/mob/user as mob)
- return src.ui_interact(user)
+ return src.tg_ui_interact(user)
-/obj/machinery/portable_atmospherics/canister/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
- if (src.destroyed)
- return
-
- // this is the data which will be sent to the ui
- var/data[0]
+/obj/machinery/portable_atmospherics/canister/ui_data(mob/user)
+ var/list/data = list()
data["name"] = name
data["canLabel"] = can_label ? 1 : 0
data["portConnected"] = connected_port ? 1 : 0
data["tankPressure"] = round(air_contents.return_pressure() ? air_contents.return_pressure() : 0)
data["releasePressure"] = round(release_pressure ? release_pressure : 0)
data["minReleasePressure"] = round(ONE_ATMOSPHERE/10)
+ data["defaultReleasePressure"] = ONE_ATMOSPHERE
data["maxReleasePressure"] = round(10*ONE_ATMOSPHERE)
data["valveOpen"] = valve_open ? 1 : 0
@@ -286,83 +283,70 @@ update_flag
if (holding)
data["holdingTank"] = list("name" = holding.name, "tankPressure" = round(holding.air_contents.return_pressure()))
- // update the ui if it exists, returns null if no ui is passed/found
- ui = nanomanager.try_update_ui(user, src, ui_key, ui, data, force_open)
- if (!ui)
- // the ui does not exist, so we'll create a new() one
- // for a list of parameters and their descriptions see the code docs in \code\modules\nano\nanoui.dm
- ui = new(user, src, ui_key, "canister.tmpl", "Canister", 480, 400)
- // when the ui is first opened this is the data it will use
- ui.set_initial_data(data)
- // open the new ui window
- ui.open()
- // auto update every Master Controller tick
- ui.set_auto_update(1)
+ return data
-/obj/machinery/portable_atmospherics/canister/Topic(href, href_list)
-
- //Do not use "if(..()) return" here, canisters will stop working in unpowered areas like space or on the derelict. // yeah but without SOME sort of Topic check any dick can mess with them via exploits as he pleases -walter0o
- //First comment might be outdated.
- if (!istype(src.loc, /turf))
- return 0
-
- if(!usr.canmove || usr.stat || usr.restrained() || !in_range(loc, usr)) // exploit protection -walter0o
- usr << browse(null, "window=canister")
- onclose(usr, "canister")
+/obj/machinery/portable_atmospherics/canister/tg_ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = tg_physical_state)
+ if (src.destroyed)
return
+ ui = tgui_process.try_update_ui(user, src, ui_key, ui, force_open)
+ if(!ui)
+ ui = new(user, src, ui_key, "canister", name, 400, 400, master_ui, state)
+ ui.open()
- if(href_list["toggle"])
- if (valve_open)
- if (holding)
- release_log += "Valve was closed by [usr] ([usr.ckey]), stopping the transfer into the [holding]
"
- else
- release_log += "Valve was closed by [usr] ([usr.ckey]), stopping the transfer into the air
"
- else
- if (holding)
- release_log += "Valve was opened by [usr] ([usr.ckey]), starting the transfer into the [holding]
"
- else
- release_log += "Valve was opened by [usr] ([usr.ckey]), starting the transfer into the air
"
- log_open()
- valve_open = !valve_open
+/obj/machinery/portable_atmospherics/canister/ui_status(mob/user, datum/ui_state/state)
+ if(!istype(src.loc, /turf))
+ return UI_CLOSE
+ return ..()
- if (href_list["remove_tank"])
- if(holding)
+/obj/machinery/portable_atmospherics/canister/ui_act(action, params)
+ switch(action)
+ if("relabel")
+ if (can_label)
+ var/list/colors = list(\
+ "\[N2O\]" = "redws", \
+ "\[N2\]" = "red", \
+ "\[O2\]" = "blue", \
+ "\[Phoron\]" = "orange", \
+ "\[CO2\]" = "black", \
+ "\[Air\]" = "grey", \
+ "\[CAUTION\]" = "yellow", \
+ )
+ var/label = input("Choose canister label", "Gas canister") as null|anything in colors
+ if (label)
+ src.canister_color = colors[label]
+ src.icon_state = colors[label]
+ src.name = "\improper Canister: [label]"
+ if("pressure")
+ var/diff = text2num(params["adjust"])
+ if(diff > 0)
+ release_pressure = min(10*ONE_ATMOSPHERE, release_pressure+diff)
+ else
+ release_pressure = max(ONE_ATMOSPHERE/10, release_pressure+diff)
+
+ if("valve")
if (valve_open)
- valve_open = 0
- release_log += "Valve was closed by [usr] ([usr.ckey]), stopping the transfer into the [holding]
"
- if(istype(holding, /obj/item/weapon/tank))
- holding.manipulated_by = usr.real_name
- holding.loc = loc
- holding = null
+ if (holding)
+ release_log += "Valve was closed by [usr] ([usr.ckey]), stopping the transfer into the [holding]
"
+ else
+ release_log += "Valve was closed by [usr] ([usr.ckey]), stopping the transfer into the air
"
+ else
+ if (holding)
+ release_log += "Valve was opened by [usr] ([usr.ckey]), starting the transfer into the [holding]
"
+ else
+ release_log += "Valve was opened by [usr] ([usr.ckey]), starting the transfer into the air
"
+ log_open()
+ valve_open = !valve_open
- if (href_list["pressure_adj"])
- var/diff = text2num(href_list["pressure_adj"])
- if(diff > 0)
- release_pressure = min(10*ONE_ATMOSPHERE, release_pressure+diff)
- else
- release_pressure = max(ONE_ATMOSPHERE/10, release_pressure+diff)
-
- if (href_list["relabel"])
- if (can_label)
- var/list/colors = list(\
- "\[N2O\]" = "redws", \
- "\[N2\]" = "red", \
- "\[O2\]" = "blue", \
- "\[Phoron\]" = "orange", \
- "\[CO2\]" = "black", \
- "\[Air\]" = "grey", \
- "\[CAUTION\]" = "yellow", \
- )
- var/label = input("Choose canister label", "Gas canister") as null|anything in colors
- if (label)
- src.canister_color = colors[label]
- src.icon_state = colors[label]
- src.name = "Canister: [label]"
-
- src.add_fingerprint(usr)
- update_icon()
-
- return 1
+ if("eject")
+ if(holding)
+ if (valve_open)
+ valve_open = 0
+ release_log += "Valve was closed by [usr] ([usr.ckey]), stopping the transfer into the [holding]
"
+ if(istype(holding, /obj/item/weapon/tank))
+ holding.manipulated_by = usr.real_name
+ holding.loc = loc
+ holding = null
+ return TRUE
/obj/machinery/portable_atmospherics/canister/phoron/New()
..()
diff --git a/code/game/machinery/doors/airlock_electronics.dm b/code/game/machinery/doors/airlock_electronics.dm
index 3ae0891b89..b09997c2e0 100644
--- a/code/game/machinery/doors/airlock_electronics.dm
+++ b/code/game/machinery/doors/airlock_electronics.dm
@@ -11,103 +11,95 @@
req_access = list(access_engine)
var/secure = 0 //if set, then wires will be randomized and bolts will drop if the door is broken
- var/list/conf_access = null
+ var/list/conf_access = list()
var/one_access = 0 //if set to 1, door would receive req_one_access instead of req_access
var/last_configurator = null
var/locked = 1
- attack_self(mob/user as mob)
- if (!ishuman(user) && !istype(user,/mob/living/silicon/robot))
- return ..(user)
- var/t1 = text("Access control
\n")
- if (last_configurator)
- t1 += "Operator: [last_configurator]
"
+/obj/item/weapon/airlock_electronics/attack_self(mob/user as mob)
+ if (!ishuman(user) && !istype(user,/mob/living/silicon/robot))
+ return ..(user)
- if (locked)
- t1 += "Swipe ID
"
- else
- t1 += "Block
"
+ tg_ui_interact(user)
- t1 += "Access requirement is set to "
- t1 += one_access ? "ONE
" : "ALL
"
- t1 += conf_access == null ? "All
" : "All
"
- t1 += "
"
+//tgui interact code generously lifted from tgstation.
+/obj/item/weapon/airlock_electronics/tg_ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, \
+ datum/tgui/master_ui = null, datum/ui_state/state = hands_state)
- var/list/accesses = get_all_station_access()
- for (var/acc in accesses)
- var/aname = get_access_desc(acc)
+ tgui_process.try_update_ui(user, src, ui_key, ui, force_open)
+ if(!ui)
+ ui = new(user, src, ui_key, "airlock_electronics", src.name, 1000, 500, master_ui, state)
+ ui.open()
- if (!conf_access || !conf_access.len || !(acc in conf_access))
- t1 += "[aname]
"
- else if(one_access)
- t1 += "[aname]
"
- else
- t1 += "[aname]
"
+/obj/item/weapon/airlock_electronics/ui_data(mob/user)
+ var/list/data = list()
+ var/list/regions = list()
- t1 += text("Close
\n", src)
+ for(var/i in ACCESS_REGION_SECURITY to ACCESS_REGION_SUPPLY) //code/game/jobs/_access_defs.dm
+ var/list/region = list()
+ var/list/accesses = list()
+ for(var/j in get_region_accesses(i))
+ var/list/access = list()
+ access["name"] = get_access_desc(j)
+ access["id"] = j
+ access["req"] = (j in src.conf_access)
+ accesses[++accesses.len] = access
+ region["name"] = get_region_accesses_name(i)
+ region["accesses"] = accesses
+ regions[++regions.len] = region
+ data["regions"] = regions
+ data["oneAccess"] = one_access
+ data["locked"] = locked
- user << browse(t1, "window=airlock_electronics")
- onclose(user, "airlock")
+ return data
- Topic(href, href_list)
- ..()
- if (usr.stat || usr.restrained() || (!ishuman(usr) && !istype(usr,/mob/living/silicon)))
- return
- if (href_list["close"])
- usr << browse(null, "window=airlock")
- return
-
- if (href_list["login"])
+/obj/item/weapon/airlock_electronics/ui_act(action, params)
+ if(..())
+ return TRUE
+ switch(action)
+ if("clear")
+ conf_access = list()
+ one_access = 0
+ return TRUE
+ if("one_access")
+ one_access = !one_access
+ return TRUE
+ if("set")
+ var/access = text2num(params["access"])
+ if (!(access in conf_access))
+ conf_access += access
+ else
+ conf_access -= access
+ return TRUE
+ if("unlock")
if(istype(usr,/mob/living/silicon))
- src.locked = 0
- src.last_configurator = usr.name
+ locked = 0
+ last_configurator = usr.name
+ return TRUE
else
var/obj/item/I = usr.get_active_hand()
if (istype(I, /obj/item/device/pda))
var/obj/item/device/pda/pda = I
I = pda.id
+ if(!istype(I, /obj/item/weapon/card/id))
+ usr << "[\src] flashes a yellow LED near the ID scanner. Did you remember to scan your ID or PDA?"
+ return TRUE
if (I && src.check_access(I))
- src.locked = 0
- src.last_configurator = I:registered_name
-
- if (locked)
- return
-
- if (href_list["logout"])
- locked = 1
-
- if (href_list["one_access"])
- one_access = !one_access
-
- if (href_list["access"])
- toggle_access(href_list["access"])
-
- attack_self(usr)
-
- proc
- toggle_access(var/acc)
- if (acc == "all")
- conf_access = null
- else
- var/req = text2num(acc)
-
- if (conf_access == null)
- conf_access = list()
-
- if (!(req in conf_access))
- conf_access += req
+ locked = 0
+ last_configurator = I:registered_name
else
- conf_access -= req
- if (!conf_access.len)
- conf_access = null
-
+ usr << "[\src] flashes a red LED near the ID scanner, indicating your access has been denied."
+ return TRUE
+ if("lock")
+ locked = 1
+ . = TRUE
/obj/item/weapon/airlock_electronics/secure
name = "secure airlock electronics"
desc = "designed to be somewhat more resistant to hacking than standard electronics."
origin_tech = list(TECH_DATA = 2)
- secure = 1
+ secure = 1
\ No newline at end of file
diff --git a/code/game/machinery/doors/brigdoors.dm b/code/game/machinery/doors/brigdoors.dm
index 27ce595a77..44a8ea20e6 100644
--- a/code/game/machinery/doors/brigdoors.dm
+++ b/code/game/machinery/doors/brigdoors.dm
@@ -53,7 +53,6 @@
return
return
-
//Main door timer loop, if it's timing and time is >0 reduce time by 1.
// if it's less than 0, open door, reset timer
// update the door_timer window and the icon
@@ -68,12 +67,10 @@
if(timeleft > 1e5)
src.releasetime = 0
-
if(world.timeofday > src.releasetime)
src.timer_end() // open doors, reset timer, clear status screen
src.timing = 0
- src.updateUsrDialog()
src.update_icon()
else
@@ -81,14 +78,12 @@
return
-
// has the door power situation changed, if so update icon.
/obj/machinery/door_timer/power_change()
..()
update_icon()
return
-
// open/closedoor checks if door_timer has power, if so it checks if the
// linked door is open/closed (by density) then opens it/closes it.
@@ -99,6 +94,9 @@
// Set releasetime
releasetime = world.timeofday + timetoset
+ //set timing
+ timing = 1
+
for(var/obj/machinery/door/window/brigdoor/door in targets)
if(door.density) continue
spawn(0)
@@ -111,7 +109,6 @@
C.icon_state = C.icon_locked
return 1
-
// Opens and unlocks doors, power check
/obj/machinery/door_timer/proc/timer_end()
if(stat & (NOPOWER|BROKEN)) return 0
@@ -119,6 +116,9 @@
// Reset releasetime
releasetime = 0
+ //reset timing
+ timing = 0
+
for(var/obj/machinery/door/window/brigdoor/door in targets)
if(!door.density) continue
spawn(0)
@@ -132,7 +132,6 @@
return 1
-
// Check for releasetime timeleft
/obj/machinery/door_timer/proc/timeleft()
. = (releasetime - world.timeofday)/10
@@ -152,116 +151,62 @@
/obj/machinery/door_timer/attack_ai(var/mob/user as mob)
return src.attack_hand(user)
-
-//Allows humans to use door_timer
-//Opens dialog window when someone clicks on door timer
-// Allows altering timer and the timing boolean.
-// Flasher activation limited to 150 seconds
/obj/machinery/door_timer/attack_hand(var/mob/user as mob)
- if(..())
- return
+ tg_ui_interact(user)
- // Used for the 'time left' display
- var/second = round(timeleft() % 60)
- var/minute = round((timeleft() - second) / 60)
+/obj/machinery/door_timer/ui_data(mob/user)
+ var/list/data = list()
- // Used for 'set timer'
- var/setsecond = round((timetoset / 10) % 60)
- var/setminute = round(((timetoset / 10) - setsecond) / 60)
+ data["timing"] = timing
+ data["releasetime"] = releasetime
+ data["timetoset"] = timetoset
+ data["timeleft"] = timeleft()
- user.set_machine(src)
+ var/list/flashes = list()
- // dat
- var/dat = ""
-
- dat += "
Timer System:"
- dat += " Door [src.id] controls
"
-
- // Start/Stop timer
- if (src.timing)
- dat += "Stop Timer and open door
"
- else
- dat += "Activate Timer and close door
"
-
- // Time Left display (uses releasetime)
- dat += "Time Left: [(minute ? text("[minute]:") : null)][second]
"
- dat += "
"
-
- // Set Timer display (uses timetoset)
- if(src.timing)
- dat += "Set Timer: [(setminute ? text("[setminute]:") : null)][setsecond] Set
"
- else
- dat += "Set Timer: [(setminute ? text("[setminute]:") : null)][setsecond]
"
-
- // Controls
- dat += "- - + +
"
-
- // Mounted flash controls
- for(var/obj/machinery/flasher/F in targets)
- if(F.last_flash && (F.last_flash + 150) > world.time)
- dat += "
Flash Charging"
+ for(var/obj/machinery/flasher/flash in targets)
+ var/list/flashdata = list()
+ if(flash.last_flash && (flash.last_flash + 150) > world.time)
+ flashdata["status"] = 0
else
- dat += "
Activate Flash"
+ flashdata["status"] = 1
+ flashes[++flashes.len] = flashdata
- dat += "
Close"
- dat += ""
+ data["flashes"] = flashes
+ return data
- user << browse(dat, "window=computer;size=400x500")
- onclose(user, "computer")
- return
-
-
-//Function for using door_timer dialog input, checks if user has permission
-// href_list to
-// "timing" turns on timer
-// "tp" value to modify timer
-// "fc" activates flasher
-// "change" resets the timer to the timetoset amount while the timer is counting down
-// Also updates dialog window and timer icon
-/obj/machinery/door_timer/Topic(href, href_list)
+/obj/machinery/door_timer/ui_act(action, params)
if(..())
- return
- if(!src.allowed(usr))
- return
-
- usr.set_machine(src)
-
- if(href_list["timing"])
- src.timing = text2num(href_list["timing"])
-
- if(src.timing)
- src.timer_start()
- else
- src.timer_end()
-
- else
- if(href_list["tp"]) //adjust timer, close door if not already closed
- var/tp = text2num(href_list["tp"])
- var/addtime = (timetoset / 10)
- addtime += tp
- addtime = min(max(round(addtime), 0), 3600)
-
- timeset(addtime)
-
- if(href_list["fc"])
- for(var/obj/machinery/flasher/F in targets)
- F.flash()
-
- if(href_list["change"])
- src.timer_start()
+ return TRUE
src.add_fingerprint(usr)
- src.updateUsrDialog()
+
+ if(!src.allowed(usr))
+ return TRUE
+
+ switch (action)
+ if("start")
+ if(timetoset > 18000)
+ log_admin("[key_name(usr)] has started a brig timer over 30 minutes in length!")
+ message_admins("[key_name_admin(usr)] has started a brig timer over 30 minutes in length!")
+ timer_start()
+ if("stop")
+ timer_end()
+ if("flash")
+ for(var/obj/machinery/flasher/F in targets)
+ F.flash()
+ if("time")
+ timetoset += text2num(params["adjust"])
+ timetoset = Clamp(timetoset, 0, 36000)
+
src.update_icon()
+ return TRUE
- /* if(src.timing)
- src.timer_start()
-
- else
- src.timer_end() */
-
- return
-
+/obj/machinery/door_timer/tg_ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = default_state)
+ ui = tgui_process.try_update_ui(user, src, ui_key, ui, force_open)
+ if(!ui)
+ ui = new(user, src, ui_key, "brig_timer", name , 300, 150, master_ui, state)
+ ui.open()
//icon update function
// if NOPOWER, display blank
@@ -282,7 +227,9 @@
disp2 = "Error"
update_display(disp1, disp2)
else
- if(maptext) maptext = ""
+ if(maptext)
+ maptext = ""
+ update_display("Set","Time") // would be nice to have some default printed text
return
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index ac2c156fa4..3f0d9e4613 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -31,7 +31,6 @@
var/close_door_at = 0 //When to automatically close the door, if possible
//Multi-tile doors
- dir = EAST
var/width = 1
// turf animation
diff --git a/code/game/machinery/doors/multi_tile.dm b/code/game/machinery/doors/multi_tile.dm
index 4fc4b2fad7..a87939bf25 100644
--- a/code/game/machinery/doors/multi_tile.dm
+++ b/code/game/machinery/doors/multi_tile.dm
@@ -1,6 +1,7 @@
//Terribly sorry for the code doubling, but things go derpy otherwise.
/obj/machinery/door/airlock/multi_tile
width = 2
+ dir = EAST
/obj/machinery/door/airlock/multi_tile/New()
..()
diff --git a/code/game/machinery/jukebox.dm b/code/game/machinery/jukebox.dm
index 3cc537be49..4a27cafa90 100644
--- a/code/game/machinery/jukebox.dm
+++ b/code/game/machinery/jukebox.dm
@@ -8,7 +8,7 @@ datum/track/New(var/title_name, var/audio)
title = title_name
sound = audio
-/obj/machinery/media/jukebox/
+/obj/machinery/media/jukebox
name = "space jukebox"
icon = 'icons/obj/jukebox.dmi'
icon_state = "jukebox2-nopower"
@@ -44,10 +44,11 @@ datum/track/New(var/title_name, var/audio)
component_parts += new /obj/item/weapon/stock_parts/console_screen(src)
component_parts += new /obj/item/stack/cable_coil(src, 5)
RefreshParts()
+ update_icon()
/obj/machinery/media/jukebox/Destroy()
StopPlaying()
- ..()
+ . = ..()
/obj/machinery/media/jukebox/power_change()
if(!powered(power_channel) || !anchored)
@@ -74,10 +75,7 @@ datum/track/New(var/title_name, var/audio)
else
overlays += "[state_base]-running"
-/obj/machinery/media/jukebox/Topic(href, href_list)
- if(..() || !(Adjacent(usr) || istype(usr, /mob/living/silicon)))
- return
-
+/obj/machinery/media/jukebox/interact(mob/user)
if(!anchored)
usr << "You must secure \the [src] first."
return
@@ -86,70 +84,74 @@ datum/track/New(var/title_name, var/audio)
usr << "\The [src] doesn't appear to function."
return
- if(href_list["change_track"])
- for(var/datum/track/T in tracks)
- if(T.title == href_list["title"])
- current_track = T
- StartPlaying()
- break
- else if(href_list["stop"])
- StopPlaying()
- else if(href_list["play"])
- if(emagged)
- playsound(src.loc, 'sound/items/AirHorn.ogg', 100, 1)
- for(var/mob/living/carbon/M in ohearers(6, src))
- if(M.get_ear_protection() >= 2)
- continue
- M.sleeping = 0
- M.stuttering += 20
- M.ear_deaf += 30
- M.Weaken(3)
- if(prob(30))
- M.Stun(10)
- M.Paralyse(4)
- else
- M.make_jittery(500)
- spawn(15)
- explode()
- else if(current_track == null)
- usr << "No track selected."
- else
- StartPlaying()
+ tg_ui_interact(user)
- return 1
+/obj/machinery/media/jukebox/ui_status(mob/user, datum/ui_state/state)
+ if(!anchored || inoperable())
+ return UI_CLOSE
+ return ..()
-/obj/machinery/media/jukebox/interact(mob/user)
- if(stat & (NOPOWER|BROKEN))
- usr << "\The [src] doesn't appear to function."
- return
-
- ui_interact(user)
-
-/obj/machinery/media/jukebox/ui_interact(mob/user, ui_key = "jukebox", var/datum/nanoui/ui = null, var/force_open = 1)
- var/title = "RetroBox - Space Style"
- var/data[0]
-
- if(!(stat & (NOPOWER|BROKEN)))
- data["current_track"] = current_track != null ? current_track.title : ""
- data["playing"] = playing
-
- var/list/nano_tracks = new
- for(var/datum/track/T in tracks)
- nano_tracks[++nano_tracks.len] = list("track" = T.title)
-
- data["tracks"] = nano_tracks
-
- // update the ui if it exists, returns null if no ui is passed/found
- ui = nanomanager.try_update_ui(user, src, ui_key, ui, data, force_open)
- if (!ui)
- // the ui does not exist, so we'll create a new() one
- // for a list of parameters and their descriptions see the code docs in \code\modules\nano\nanoui.dm
- ui = new(user, src, ui_key, "jukebox.tmpl", title, 450, 600)
- // when the ui is first opened this is the data it will use
- ui.set_initial_data(data)
- // open the new ui window
+/obj/machinery/media/jukebox/tg_ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = tg_default_state)
+ ui = tgui_process.try_update_ui(user, src, ui_key, ui, force_open)
+ if(!ui)
+ ui = new(user, src, ui_key, "jukebox", "RetroBox - Space Style", 340, 440, master_ui, state)
ui.open()
+/obj/machinery/media/jukebox/ui_data()
+ var/list/juke_tracks = new
+ for(var/datum/track/T in tracks)
+ juke_tracks.Add(T.title)
+
+ var/list/data = list(
+ "current_track" = current_track != null ? current_track.title : "No track selected",
+ "playing" = playing,
+ "tracks" = juke_tracks
+ )
+
+ return data
+
+/obj/machinery/media/jukebox/ui_act(action, params)
+ if(..())
+ return TRUE
+ switch(action)
+ if("change_track")
+ for(var/datum/track/T in tracks)
+ if(T.title == params["title"])
+ current_track = T
+ StartPlaying()
+ break
+ . = TRUE
+ if("stop")
+ StopPlaying()
+ . = TRUE
+ if("play")
+ if(emagged)
+ emag_play()
+ else if(!current_track)
+ usr << "No track selected."
+ else
+ StartPlaying()
+ . = TRUE
+
+/obj/machinery/media/jukebox/proc/emag_play()
+ playsound(loc, 'sound/items/AirHorn.ogg', 100, 1)
+ for(var/mob/living/carbon/M in ohearers(6, src))
+ if(istype(M, /mob/living/carbon/human))
+ var/mob/living/carbon/human/H = M
+ if(istype(H.l_ear, /obj/item/clothing/ears/earmuffs) || istype(H.r_ear, /obj/item/clothing/ears/earmuffs))
+ continue
+ M.sleeping = 0
+ M.stuttering += 20
+ M.ear_deaf += 30
+ M.Weaken(3)
+ if(prob(30))
+ M.Stun(10)
+ M.Paralyse(4)
+ else
+ M.make_jittery(500)
+ spawn(15)
+ explode()
+
/obj/machinery/media/jukebox/attack_ai(mob/user as mob)
return src.attack_hand(user)
@@ -172,10 +174,6 @@ datum/track/New(var/title_name, var/audio)
/obj/machinery/media/jukebox/attackby(obj/item/W as obj, mob/user as mob)
src.add_fingerprint(user)
- if(default_deconstruction_screwdriver(user, W))
- return
- if(default_deconstruction_crowbar(user, W))
- return
if(istype(W, /obj/item/weapon/wrench))
if(playing)
StopPlaying()
@@ -200,7 +198,6 @@ datum/track/New(var/title_name, var/audio)
// Always kill the current sound
for(var/mob/living/M in mobs_in_area(main_area))
M << sound(null, channel = 1)
-
main_area.forced_ambience = null
playing = 0
update_use_power(1)
@@ -220,4 +217,4 @@ datum/track/New(var/title_name, var/audio)
playing = 1
update_use_power(2)
- update_icon()
+ update_icon()
\ No newline at end of file
diff --git a/code/game/machinery/newscaster.dm b/code/game/machinery/newscaster.dm
index 7bfe32d719..6d77713ce6 100644
--- a/code/game/machinery/newscaster.dm
+++ b/code/game/machinery/newscaster.dm
@@ -90,6 +90,8 @@
/datum/feed_network/proc/insert_message_in_channel(var/datum/feed_channel/FC, var/datum/feed_message/newMsg)
FC.messages += newMsg
+ if(newMsg.img)
+ register_asset("newscaster_photo_[sanitize(FC.channel_name)]_[FC.messages.len].png", newMsg.img)
newMsg.parent_channel = FC
FC.update()
alert_readers(FC.announcement)
@@ -377,11 +379,12 @@ var/list/obj/machinery/newscaster/allCasters = list() //Global list that will co
else
var/i = 0
for(var/datum/feed_message/MESSAGE in src.viewing_channel.messages)
- i++
+ ++i
dat+="-[MESSAGE.body]
"
if(MESSAGE.img)
- usr << browse_rsc(MESSAGE.img, "tmp_photo[i].png")
- dat+="
"
+ var/resourc_name = "newscaster_photo_[sanitize(viewing_channel.channel_name)]_[i].png"
+ send_asset(usr.client, resourc_name)
+ dat+="
"
if(MESSAGE.caption)
dat+="[MESSAGE.caption]
"
dat+="
"
@@ -874,11 +877,12 @@ obj/item/weapon/newspaper/attack_self(mob/user as mob)
dat+=""
var/i = 0
for(var/datum/feed_message/MESSAGE in C.messages)
- i++
+ ++i
dat+="-[MESSAGE.body]
"
if(MESSAGE.img)
- user << browse_rsc(MESSAGE.img, "tmp_photo[i].png")
- dat+="
"
+ var/resourc_name = "newscaster_photo_[sanitize(C.channel_name)]_[i].png"
+ send_asset(user.client, resourc_name)
+ dat+="
"
dat+="\[[MESSAGE.message_type] by [MESSAGE.author]\]
"
dat+="
"
if(scribble_page==curr_page)
diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm
index f0bb82efd0..2c208ac62b 100644
--- a/code/game/objects/items/devices/PDA/PDA.dm
+++ b/code/game/objects/items/devices/PDA/PDA.dm
@@ -620,9 +620,9 @@ var/global/list/obj/item/device/pda/PDAs = list()
if(!FC.censored)
var/index = 0
for(var/datum/feed_message/FM in FC.messages)
- index++
+ ++index
if(FM.img)
- usr << browse_rsc(FM.img, "pda_news_tmp_photo_[feed["channel"]]_[index].png")
+ send_asset(usr.client, "newscaster_photo_[sanitize(FC.channel_name)]_[index].png")
// News stories are HTML-stripped but require newline replacement to be properly displayed in NanoUI
var/body = replacetext(FM.body, "\n", "
")
messages[++messages.len] = list("author" = FM.author, "body" = body, "message_type" = FM.message_type, "time_stamp" = FM.time_stamp, "has_image" = (FM.img != null), "caption" = FM.caption, "index" = index)
@@ -651,6 +651,8 @@ var/global/list/obj/item/device/pda/PDAs = list()
//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/game/objects/objs.dm b/code/game/objects/objs.dm
index a3f77757f2..51e2d86e8a 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -115,6 +115,7 @@
/obj/attack_ghost(mob/user)
ui_interact(user)
+ tg_ui_interact(user)
..()
/obj/proc/interact(mob/user)
diff --git a/code/modules/admin/callproc/callproc.dm b/code/modules/admin/callproc/callproc.dm
index 9b3da2b92f..cb05d1fe5b 100644
--- a/code/modules/admin/callproc/callproc.dm
+++ b/code/modules/admin/callproc/callproc.dm
@@ -125,7 +125,7 @@
return
if("marked datum")
- current = holder.marked_datum
+ current = holder.marked_datum()
if(!current)
switch(alert("You do not currently have a marked datum; do you want to pass null instead?",, "Yes", "Cancel"))
if("Yes")
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index 9c73dd9e86..779207861c 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -6,13 +6,17 @@ var/list/admin_datums = list()
var/rights = 0
var/fakekey = null
- var/datum/marked_datum
+ var/datum/weakref/marked_datum_weak
var/admincaster_screen = 0 //See newscaster.dm under machinery for a full description
var/datum/feed_message/admincaster_feed_message = new /datum/feed_message //These two will act as holders.
var/datum/feed_channel/admincaster_feed_channel = new /datum/feed_channel
var/admincaster_signature //What you'll sign the newsfeeds as
+/datum/admins/proc/marked_datum()
+ if(marked_datum_weak)
+ return marked_datum_weak.resolve()
+
/datum/admins/New(initial_rank = "Temporary Admin", initial_rights = 0, ckey)
if(!ckey)
error("Admin datum created without a ckey argument. Datum has been deleted")
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index cf98ede72f..a59a88f12a 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1603,6 +1603,7 @@
where = "onfloor"
if ( where == "inmarked" )
+ var/marked_datum = marked_datum()
if ( !marked_datum )
usr << "You don't have any object marked. Abandoning spawn."
return
@@ -1620,7 +1621,7 @@
if ("relative")
target = locate(loc.x + X,loc.y + Y,loc.z + Z)
if ( "inmarked" )
- target = marked_datum
+ target = marked_datum()
if(target)
for (var/path in paths)
diff --git a/code/modules/admin/verbs/modifyvariables.dm b/code/modules/admin/verbs/modifyvariables.dm
index 51f1fbceb3..90a040c4c3 100644
--- a/code/modules/admin/verbs/modifyvariables.dm
+++ b/code/modules/admin/verbs/modifyvariables.dm
@@ -27,20 +27,20 @@ var/list/VVckey_edit = list("key", "ckey")
src.modify_variables(ticker)
feedback_add_details("admin_verb","ETV") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
-/client/proc/mod_list_add_ass() //haha
-
+/client/proc/mod_list_add_ass()
var/class = "text"
- if(src.holder && src.holder.marked_datum)
- class = input("What kind of variable?","Variable Type") as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default","marked datum ([holder.marked_datum.type])")
- else
- class = input("What kind of variable?","Variable Type") as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default")
+ var/list/class_input = list("text","num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default")
+ if(src.holder)
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum)
+ class_input += "marked datum ([marked_datum.type])"
+ class = input("What kind of variable?","Variable Type") as null|anything in class_input
if(!class)
return
- if(holder.marked_datum && class == "marked datum ([holder.marked_datum.type])")
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum && class == "marked datum ([marked_datum.type])")
class = "marked datum"
var/var_value = null
@@ -69,7 +69,7 @@ var/list/VVckey_edit = list("key", "ckey")
var_value = input("Pick icon:","Icon") as null|icon
if("marked datum")
- var_value = holder.marked_datum
+ var_value = holder.marked_datum()
if(!var_value) return
@@ -79,17 +79,18 @@ var/list/VVckey_edit = list("key", "ckey")
/client/proc/mod_list_add(var/list/L, atom/O, original_name, objectvar)
var/class = "text"
- if(src.holder && src.holder.marked_datum)
- class = input("What kind of variable?","Variable Type") as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default","marked datum ([holder.marked_datum.type])")
- else
- class = input("What kind of variable?","Variable Type") as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default")
+ var/list/class_input = list("text","num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default")
+ if(src.holder)
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum)
+ class_input += "marked datum ([marked_datum.type])"
+ class = input("What kind of variable?","Variable Type") as null|anything in class_input
if(!class)
return
- if(holder.marked_datum && class == "marked datum ([holder.marked_datum.type])")
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum && class == "marked datum ([marked_datum.type])")
class = "marked datum"
var/var_value = null
@@ -118,7 +119,7 @@ var/list/VVckey_edit = list("key", "ckey")
var_value = input("Pick icon:","Icon") as icon
if("marked datum")
- var_value = holder.marked_datum
+ var_value = holder.marked_datum()
if(!var_value) return
@@ -244,17 +245,21 @@ var/list/VVckey_edit = list("key", "ckey")
usr << "If a direction, direction is: [dir]"
var/class = "text"
- if(src.holder && src.holder.marked_datum)
- class = input("What kind of variable?","Variable Type",default) as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default","marked datum ([holder.marked_datum.type])", "DELETE FROM LIST")
- else
- class = input("What kind of variable?","Variable Type",default) as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default", "DELETE FROM LIST")
+ var/list/class_input = list("text","num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default")
+
+ if(src.holder)
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum)
+ class_input += "marked datum ([marked_datum.type])"
+
+ class_input += "DELETE FROM LIST"
+ class = input("What kind of variable?","Variable Type",default) as null|anything in class_input
if(!class)
return
- if(holder.marked_datum && class == "marked datum ([holder.marked_datum.type])")
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum && class == "marked datum ([marked_datum.type])")
class = "marked datum"
var/original_var
@@ -336,7 +341,9 @@ var/list/VVckey_edit = list("key", "ckey")
L[L.Find(variable)] = new_var
if("marked datum")
- new_var = holder.marked_datum
+ new_var = holder.marked_datum()
+ if(!new_var)
+ return
if(assoc)
L[assoc_key] = new_var
else
@@ -501,12 +508,12 @@ var/list/VVckey_edit = list("key", "ckey")
if(dir)
usr << "If a direction, direction is: [dir]"
- if(src.holder && src.holder.marked_datum)
- class = input("What kind of variable?","Variable Type",default) as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default","marked datum ([holder.marked_datum.type])")
- else
- class = input("What kind of variable?","Variable Type",default) as null|anything in list("text",
- "num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default")
+ var/list/class_input = list("text","num","type","reference","mob reference", "icon","file","list","edit referenced object","restore to default")
+ if(src.holder)
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum)
+ class_input += "marked datum ([marked_datum.type])"
+ class = input("What kind of variable?","Variable Type",default) as null|anything in class_input
if(!class)
return
@@ -518,7 +525,8 @@ var/list/VVckey_edit = list("key", "ckey")
else
original_name = O:name
- if(holder.marked_datum && class == "marked datum ([holder.marked_datum.type])")
+ var/datum/marked_datum = holder.marked_datum()
+ if(marked_datum && class == "marked datum ([marked_datum.type])")
class = "marked datum"
switch(class)
@@ -584,7 +592,7 @@ var/list/VVckey_edit = list("key", "ckey")
O.vars[variable] = var_new
if("marked datum")
- O.vars[variable] = holder.marked_datum
+ O.vars[variable] = holder.marked_datum()
world.log << "### VarEdit by [src]: [O.type] [variable]=[html_encode("[O.vars[variable]]")]"
log_admin("[key_name(src)] modified [original_name]'s [variable] to [O.vars[variable]]")
diff --git a/code/modules/admin/view_variables/topic.dm b/code/modules/admin/view_variables/topic.dm
index 71da9f81ed..9acc180fc2 100644
--- a/code/modules/admin/view_variables/topic.dm
+++ b/code/modules/admin/view_variables/topic.dm
@@ -223,7 +223,7 @@
usr << "This can only be done to instances of type /datum"
return
- src.holder.marked_datum = D
+ src.holder.marked_datum_weak = weakref(D)
href_list["datumrefresh"] = href_list["mark_object"]
else if(href_list["rotatedatum"])
@@ -476,7 +476,10 @@
usr << "This can only be done on mobs with clients"
return
- H.client.reload_nanoui_resources()
+ nanomanager.close_uis(H)
+ H.client.cache.Cut()
+ var/datum/asset/assets = get_asset_datum(/datum/asset/nanoui)
+ assets.send(H)
usr << "Resource files sent"
H << "Your NanoUI Resource files have been refreshed"
diff --git a/code/modules/admin/view_variables/view_variables.dm b/code/modules/admin/view_variables/view_variables.dm
index e624d2df05..571dfff560 100644
--- a/code/modules/admin/view_variables/view_variables.dm
+++ b/code/modules/admin/view_variables/view_variables.dm
@@ -45,7 +45,7 @@
[replacetext("[D.type]", "/", "/")]
- [holder.marked_datum == D ? "
Marked Object" : ""]
+ [holder.marked_datum() == D ? "
Marked Object" : ""]
diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm
index b4a42b0a96..3cfb75606d 100644
--- a/code/modules/client/asset_cache.dm
+++ b/code/modules/client/asset_cache.dm
@@ -200,10 +200,45 @@ proc/getFilesSlow(var/client/client, var/list/files, var/register_asset = TRUE)
"large_stamp-law.png" = 'icons/paper_icons/large_stamp-law.png',
"large_stamp-cent.png" = 'icons/paper_icons/large_stamp-cent.png',
"talisman.png" = 'icons/paper_icons/talisman.png',
- "ntlogo.png" = 'icons/paper_icons/ntlogo.png'
+ "ntlogo.png" = 'icons/paper_icons/ntlogo.png',
"sglogo.png" = 'icons/paper_icons/sglogo.png'
)
+/datum/asset/simple/tgui
+ assets = list(
+ "tgui.css" = 'tgui/assets/tgui.css',
+ "tgui.js" = 'tgui/assets/tgui.js'
+ )
+
+/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_chatroom.png" = 'icons/pda_icons/pda_chatroom.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_honk.png" = 'icons/pda_icons/pda_honk.png',
+ "pda_locked.png" = 'icons/pda_icons/pda_locked.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/nanoui
var/list/common = list()
@@ -239,4 +274,4 @@ proc/getFilesSlow(var/client/client, var/list/files, var/register_asset = TRUE)
uncommon = list(uncommon)
send_asset_list(client, uncommon)
- send_asset_list(client, common)
+ send_asset_list(client, common)
\ No newline at end of file
diff --git a/code/modules/client/preference_setup/global/setting_datums.dm b/code/modules/client/preference_setup/global/setting_datums.dm
index b584341fd5..61e20c986d 100644
--- a/code/modules/client/preference_setup/global/setting_datums.dm
+++ b/code/modules/client/preference_setup/global/setting_datums.dm
@@ -150,6 +150,18 @@ var/list/_client_preferences_by_type
enabled_description = "Fancy"
disabled_description = "Plain"
+/datum/client_preference/tgui_style
+ description ="tgui Style"
+ key = "TGUI_FANCY"
+ enabled_description = "Fancy"
+ disabled_description = "Plain"
+
+/datum/client_preference/tgui_monitor
+ description ="tgui Monitor"
+ key = "TGUI_MONITOR"
+ enabled_description = "Primary"
+ disabled_description = "All"
+
/********************
* Staff Preferences *
********************/
diff --git a/code/modules/clothing/spacesuits/rig/rig.dm b/code/modules/clothing/spacesuits/rig/rig.dm
index ae064dd2a8..a2b5fca3a7 100644
--- a/code/modules/clothing/spacesuits/rig/rig.dm
+++ b/code/modules/clothing/spacesuits/rig/rig.dm
@@ -798,6 +798,9 @@
return 0
return 1
+/obj/item/weapon/rig/check_access(obj/item/I)
+ return TRUE
+
/obj/item/weapon/rig/proc/force_rest(var/mob/user)
if(!ai_can_move_suit(user, check_user_module = 1))
return
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index d4a967dc7b..67c167ef13 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -720,7 +720,10 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
/mob/observer/dead/canface()
return 1
-/mob/observer/dead/proc/can_admin_interact()
+/mob/proc/can_admin_interact()
+ return 0
+
+/mob/observer/dead/can_admin_interact()
return check_rights(R_ADMIN, 0, src)
/mob/observer/dead/verb/toggle_ghostsee()
diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm
index bb01c846ca..f6f7087ebb 100644
--- a/code/modules/mob/logout.dm
+++ b/code/modules/mob/logout.dm
@@ -1,5 +1,6 @@
/mob/Logout()
nanomanager.user_logout(src) // this is used to clean up (remove) this user's Nano UIs
+ tgui_process.on_logout(src)
player_list -= src
log_access("Logout: [key_name(src)]")
if(admin_datums[src.ckey])
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 2f788862f8..fe5988a2f0 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -576,4 +576,10 @@ var/list/global/organ_rel_size = list(
)
/mob/proc/flash_eyes(intensity = FLASH_PROTECTION_MODERATE, override_blindness_check = FALSE, affect_silicon = FALSE, visual = FALSE, type = /obj/screen/fullscreen/flash)
- return
\ No newline at end of file
+ return
+
+/proc/get_both_hands(mob/living/carbon/M)
+ if(!istype(M))
+ return
+ var/list/hands = list(M.l_hand, M.r_hand)
+ return hands
\ No newline at end of file
diff --git a/code/modules/nano/interaction/hands.dm b/code/modules/nano/interaction/hands.dm
new file mode 100644
index 0000000000..34251ad416
--- /dev/null
+++ b/code/modules/nano/interaction/hands.dm
@@ -0,0 +1,9 @@
+/*
+ This state only checks if user is conscious.
+*/
+/var/global/datum/topic_state/hands/hands_state = new()
+
+/datum/topic_state/hands/can_use_topic(var/src_object, var/mob/user)
+ . = user.shared_ui_interaction(src_object)
+ if(. > STATUS_CLOSE)
+ . = min(., user.hands_can_use_topic(src_object))
\ No newline at end of file
diff --git a/code/modules/nano/modules/nano_module.dm b/code/modules/nano/modules/nano_module.dm
index 0cdf451d6f..40530db0fe 100644
--- a/code/modules/nano/modules/nano_module.dm
+++ b/code/modules/nano/modules/nano_module.dm
@@ -1,6 +1,6 @@
/datum/nano_module
var/name
- var/host
+ var/datum/host
/datum/nano_module/New(var/host)
src.host = host
diff --git a/code/modules/nano/nanomapgen.dm b/code/modules/nano/nanomapgen.dm
index 3b2b4158e8..7b9bf6050f 100644
--- a/code/modules/nano/nanomapgen.dm
+++ b/code/modules/nano/nanomapgen.dm
@@ -57,22 +57,21 @@
world.log << "NanoMapGen: GENERATE MAP ([startX],[startY],[currentZ]) to ([endX],[endY],[currentZ])"
usr << "NanoMapGen: GENERATE MAP ([startX],[startY],[currentZ]) to ([endX],[endY],[currentZ])"
- var/count = 0;
for(var/WorldX = startX, WorldX <= endX, WorldX++)
for(var/WorldY = startY, WorldY <= endY, WorldY++)
+ var/turf/T = locate(WorldX, WorldY, currentZ)
+ var/list/atoms = T.contents + T
+ atoms = sortByVar(atoms, "layer")
+ for(var/atom/A in atoms)
+ if(A.type == /turf/space|| istype(A, /mob) || A.invisibility > 0)
+ continue
- var/atom/Turf = locate(WorldX, WorldY, currentZ)
+ var/icon/TurfIcon = new(A.icon, A.icon_state, A.dir, 1, 0)
+ TurfIcon.Scale(NANOMAP_ICON_SIZE, NANOMAP_ICON_SIZE)
- var/icon/TurfIcon = new(Turf.icon, Turf.icon_state)
- TurfIcon.Scale(NANOMAP_ICON_SIZE, NANOMAP_ICON_SIZE)
+ Tile.Blend(TurfIcon, ICON_OVERLAY, ((WorldX - 1) * NANOMAP_ICON_SIZE) + (A.pixel_x * NANOMAP_ICON_SIZE / 32), ((WorldY - 1) * NANOMAP_ICON_SIZE) + (A.pixel_y * NANOMAP_ICON_SIZE / 32))
- Tile.Blend(TurfIcon, ICON_OVERLAY, ((WorldX - 1) * NANOMAP_ICON_SIZE), ((WorldY - 1) * NANOMAP_ICON_SIZE))
-
- count++
-
- if (count % 8000 == 0)
- world.log << "NanoMapGen: [count] tiles done"
- sleep(1)
+ CHECK_TICK
var/mapFilename = "nanomap_z[currentZ]-new.png"
diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm
new file mode 100644
index 0000000000..74ba1c114e
--- /dev/null
+++ b/code/modules/tgui/external.dm
@@ -0,0 +1,98 @@
+ /**
+ * tgui external
+ *
+ * Contains all external tgui declarations.
+ **/
+
+ /**
+ * public
+ *
+ * Used to open and update UIs.
+ * If this proc is not implemented properly, the UI will not update correctly.
+ *
+ * required user mob The mob who opened/is using the UI.
+ * optional ui_key string The ui_key of the UI.
+ * optional ui datum/tgui The UI to be updated, if it exists.
+ * optional force_open bool If the UI should be re-opened instead of updated.
+ * optional master_ui datum/tgui The parent UI.
+ * optional state datum/ui_state The state used to determine status.
+ **/
+/datum/proc/tg_ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = tg_default_state)
+ return -1 // Not implemented.
+
+ /**
+ * public
+ *
+ * Data to be sent to the UI.
+ * This must be implemented for a UI to work.
+ *
+ * required user mob The mob interacting with the UI.
+ *
+ * return list Data to be sent to the UI.
+ **/
+/datum/proc/ui_data(mob/user, ui_key = "main")
+ return list() // Not implemented.
+
+
+ /**
+ * public
+ *
+ * Called on a UI when the UI receieves a href.
+ * Think of this as Topic().
+ *
+ * required action string The action/button that has been invoked by the user.
+ * required params list A list of parameters attached to the button.
+ *
+ * return bool If the UI should be updated or not.
+ **/
+/datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ if(!ui || ui.status != UI_INTERACTIVE)
+ return 1 // If UI is not interactive or usr calling Topic is not the UI user, bail.
+
+
+ /**
+ * private
+ *
+ * The UI's host object (usually src_object).
+ * This allows modules/datums to have the UI attached to them,
+ * and be a part of another object.
+ **/
+/datum/proc/ui_host()
+ return src // Default src.
+
+ /**
+ * global
+ *
+ * Used to track the current screen.
+ **/
+/datum/var/ui_screen = "home"
+
+ /**
+ * global
+ *
+ * Used to track UIs for a mob.
+ **/
+/mob/var/list/tg_open_uis = list()
+
+ /**
+ * verb
+ *
+ * Called by UIs when they are closed.
+ * Must be a verb so winset() can call it.
+ *
+ * required uiref ref The UI that was closed.
+ **/
+/client/verb/uiclose(ref as text)
+ // Name the verb, and hide it from the user panel.
+ set name = "uiclose"
+ set hidden = 1
+
+ // Get the UI based on the ref.
+ var/datum/tgui/ui = locate(ref)
+
+ // If we found the UI, close it.
+ if(istype(ui))
+ ui.close()
+ // Unset machine just to be sure.
+ if(src && src.mob)
+ src.mob.unset_machine()
diff --git a/code/modules/tgui/process.dm b/code/modules/tgui/process.dm
new file mode 100644
index 0000000000..1afaae6df6
--- /dev/null
+++ b/code/modules/tgui/process.dm
@@ -0,0 +1,223 @@
+ /**
+ * tgui process
+ *
+ * Contains all tgui state and process code.
+ **/
+
+ /**
+ * public
+ *
+ * Get a open UI given a user, src_object, and ui_key and try to update it with data.
+ *
+ * required user mob The mob who opened/is using the UI.
+ * required src_object datum The object/datum which owns the UI.
+ * required ui_key string The ui_key of the UI.
+ * optional ui datum/tgui The UI to be updated, if it exists.
+ * optional force_open bool If the UI should be re-opened instead of updated.
+ *
+ * return datum/tgui The found UI.
+ **/
+/datum/controller/process/tgui/proc/try_update_ui(mob/user, datum/src_object, ui_key, datum/tgui/ui, force_open = 0)
+ if(isnull(ui)) // No UI was passed, so look for one.
+ ui = get_open_ui(user, src_object, ui_key)
+
+ if(!isnull(ui))
+ var/data = src_object.ui_data(user) // Get data from the src_object.
+ if(!force_open) // UI is already open; update it.
+ ui.push_data(data)
+ else // Re-open it anyways.
+ ui.reinitialize(null, data)
+ return ui // We found the UI, return it.
+ else
+ return null // We couldn't find a UI.
+
+ /**
+ * private
+ *
+ * Get a open UI given a user, src_object, and ui_key.
+ *
+ * required user mob The mob who opened/is using the UI.
+ * required src_object datum The object/datum which owns the UI.
+ * required ui_key string The ui_key of the UI.
+ *
+ * return datum/tgui The found UI.
+ **/
+/datum/controller/process/tgui/proc/get_open_ui(mob/user, datum/src_object, ui_key)
+ var/src_object_key = "\ref[src_object]"
+ if(isnull(tg_open_uis[src_object_key]) || !istype(tg_open_uis[src_object_key], /list))
+ return null // No UIs open.
+ else if(isnull(tg_open_uis[src_object_key][ui_key]) || !istype(tg_open_uis[src_object_key][ui_key], /list))
+ return null // No UIs open for this object.
+
+ for(var/datum/tgui/ui in tg_open_uis[src_object_key][ui_key]) // Find UIs for this object.
+ if(ui.user == user) // Make sure we have the right user
+ return ui
+
+ return null // Couldn't find a UI!
+
+ /**
+ * private
+ *
+ * Update all UIs attached to src_object.
+ *
+ * required src_object datum The object/datum which owns the UIs.
+ *
+ * return int The number of UIs updated.
+ **/
+/datum/controller/process/tgui/proc/update_uis(datum/src_object)
+ var/src_object_key = "\ref[src_object]"
+ if(isnull(tg_open_uis[src_object_key]) || !istype(tg_open_uis[src_object_key], /list))
+ return 0 // Couldn't find any UIs for this object.
+
+ var/update_count = 0
+ for(var/ui_key in tg_open_uis[src_object_key])
+ for(var/datum/tgui/ui in tg_open_uis[src_object_key][ui_key])
+ if(ui && ui.src_object && ui.user && ui.src_object.ui_host()) // Check the UI is valid.
+ ui.process(force = 1) // Update the UI.
+ update_count++ // Count each UI we update.
+ return update_count
+
+ /**
+ * private
+ *
+ * Close all UIs attached to src_object.
+ *
+ * required src_object datum The object/datum which owns the UIs.
+ *
+ * return int The number of UIs closed.
+ **/
+/datum/controller/process/tgui/proc/close_uis(datum/src_object)
+ var/src_object_key = "\ref[src_object]"
+ if(isnull(tg_open_uis[src_object_key]) || !istype(tg_open_uis[src_object_key], /list))
+ return 0 // Couldn't find any UIs for this object.
+
+ var/close_count = 0
+ for(var/ui_key in tg_open_uis[src_object_key])
+ for(var/datum/tgui/ui in tg_open_uis[src_object_key][ui_key])
+ if(ui && ui.src_object && ui.user && ui.src_object.ui_host()) // Check the UI is valid.
+ ui.close() // Close the UI.
+ close_count++ // Count each UI we close.
+ return close_count
+
+ /**
+ * private
+ *
+ * Update all UIs belonging to a user.
+ *
+ * required user mob The mob who opened/is using the UI.
+ * optional src_object datum If provided, only update UIs belonging this src_object.
+ * optional ui_key string If provided, only update UIs with this UI key.
+ *
+ * return int The number of UIs updated.
+ **/
+/datum/controller/process/tgui/proc/update_user_uis(mob/user, datum/src_object = null, ui_key = null)
+ if(isnull(user.tg_open_uis) || !istype(user.tg_open_uis, /list) || tg_open_uis.len == 0)
+ return 0 // Couldn't find any UIs for this user.
+
+ var/update_count = 0
+ for(var/datum/tgui/ui in user.tg_open_uis)
+ if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key))
+ ui.process(force = 1) // Update the UI.
+ update_count++ // Count each UI we upadte.
+ return update_count
+
+ /**
+ * private
+ *
+ * Close all UIs belonging to a user.
+ *
+ * required user mob The mob who opened/is using the UI.
+ * optional src_object datum If provided, only close UIs belonging this src_object.
+ * optional ui_key string If provided, only close UIs with this UI key.
+ *
+ * return int The number of UIs closed.
+ **/
+/datum/controller/process/tgui/proc/close_user_uis(mob/user, datum/src_object = null, ui_key = null)
+ if(isnull(user.tg_open_uis) || !istype(user.tg_open_uis, /list) || tg_open_uis.len == 0)
+ return 0 // Couldn't find any UIs for this user.
+
+ var/close_count = 0
+ for(var/datum/tgui/ui in user.tg_open_uis)
+ if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key))
+ ui.close() // Close the UI.
+ close_count++ // Count each UI we close.
+ return close_count
+
+ /**
+ * private
+ *
+ * Add a UI to the list of open UIs.
+ *
+ * required ui datum/tgui The UI to be added.
+ **/
+/datum/controller/process/tgui/proc/on_open(datum/tgui/ui)
+ var/src_object_key = "\ref[ui.src_object]"
+ if(isnull(tg_open_uis[src_object_key]) || !istype(tg_open_uis[src_object_key], /list))
+ tg_open_uis[src_object_key] = list(ui.ui_key = list()) // Make a list for the ui_key and src_object.
+ else if(isnull(tg_open_uis[src_object_key][ui.ui_key]) || !istype(tg_open_uis[src_object_key][ui.ui_key], /list))
+ tg_open_uis[src_object_key][ui.ui_key] = list() // Make a list for the ui_key.
+
+ // Append the UI to all the lists.
+ ui.user.tg_open_uis |= ui
+ var/list/uis = tg_open_uis[src_object_key][ui.ui_key]
+ uis |= ui
+ processing_uis |= ui
+
+ /**
+ * private
+ *
+ * Remove a UI from the list of open UIs.
+ *
+ * required ui datum/tgui The UI to be removed.
+ *
+ * return bool If the UI was removed or not.
+ **/
+/datum/controller/process/tgui/proc/on_close(datum/tgui/ui)
+ var/src_object_key = "\ref[ui.src_object]"
+ if(isnull(tg_open_uis[src_object_key]) || !istype(tg_open_uis[src_object_key], /list))
+ return 0 // It wasn't open.
+ else if(isnull(tg_open_uis[src_object_key][ui.ui_key]) || !istype(tg_open_uis[src_object_key][ui.ui_key], /list))
+ return 0 // It wasn't open.
+
+ processing_uis.Remove(ui) // Remove it from the list of processing UIs.
+ if(ui.user) // If the user exists, remove it from them too.
+ ui.user.tg_open_uis.Remove(ui)
+ var/list/uis = tg_open_uis[src_object_key][ui.ui_key] // Remove it from the list of open UIs.
+ uis.Remove(ui)
+ return 1 // Let the caller know we did it.
+
+ /**
+ * private
+ *
+ * Handle client logout, by closing all their UIs.
+ *
+ * required user mob The mob which logged out.
+ *
+ * return int The number of UIs closed.
+ **/
+/datum/controller/process/tgui/proc/on_logout(mob/user)
+ return close_user_uis(user)
+
+ /**
+ * private
+ *
+ * Handle clients switching mobs, by transfering their UIs.
+ *
+ * required user source The client's original mob.
+ * required user target The client's new mob.
+ *
+ * return bool If the UIs were transferred.
+ **/
+/datum/controller/process/tgui/proc/on_transfer(mob/source, mob/target)
+ if(!source || isnull(source.tg_open_uis) || !istype(source.tg_open_uis, /list) || tg_open_uis.len == 0)
+ return 0 // The old mob had no open UIs.
+
+ if(isnull(target.tg_open_uis) || !istype(target.tg_open_uis, /list))
+ target.tg_open_uis = list() // Create a list for the new mob if needed.
+
+ for(var/datum/tgui/ui in source.tg_open_uis)
+ ui.user = target // Inform the UIs of their new owner.
+ target.tg_open_uis.Add(ui) // Transfer all the UIs.
+
+ source.tg_open_uis.Cut() // Clear the old list.
+ return 1 // Let the caller know we did it.
diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm
new file mode 100644
index 0000000000..186ca954a4
--- /dev/null
+++ b/code/modules/tgui/states.dm
@@ -0,0 +1,110 @@
+ /**
+ * tgui states
+ *
+ * Base state and helpers for states. Just does some sanity checks, implement a state for in-depth checks.
+ **/
+
+ /**
+ * public
+ *
+ * Checks the UI state for a mob.
+ *
+ * required user mob The mob who opened/is using the UI.
+ * required state datum/ui_state The state to check.
+ *
+ * return UI_state The state of the UI.
+ **/
+/datum/proc/ui_status(mob/user, datum/ui_state/state)
+ var/datum/src_object = ui_host()
+ if(src_object != src)
+ return src_object.ui_status(user, state)
+
+ if(isobserver(user)) // Special-case ghosts.
+ if(user.can_admin_interact())
+ return UI_INTERACTIVE // If they turn it on, admins can interact.
+ if(get_dist(src_object, src) < user.client.view)
+ return UI_UPDATE // Regular ghosts can only view.
+ return UI_CLOSE // To keep too many UIs from being opened.
+ return state.can_use_topic(src_object, user) // Check if the state allows interaction.
+
+ /**
+ * private
+ *
+ * Checks if a user can use src_object's UI, and returns the state.
+ * Can call a mob proc, which allows overrides for each mob.
+ *
+ * required src_object datum The object/datum which owns the UI.
+ * required user mob The mob who opened/is using the UI.
+ *
+ * return UI_state The state of the UI.
+ **/
+/datum/ui_state/proc/can_use_topic(src_object, mob/user)
+ return UI_CLOSE // Don't allow interaction by default.
+
+ /**
+ * public
+ *
+ * Standard interaction/sanity checks. Different mob types may have overrides.
+ *
+ * return UI_state The state of the UI.
+ **/
+/mob/proc/shared_ui_interaction(src_object)
+ if(!client) // Close UIs if mindless.
+ return UI_CLOSE
+ else if(stat) // Disable UIs if unconcious.
+ return UI_DISABLED
+ else if(incapacitated() || lying) // Update UIs if incapicitated but concious.
+ return UI_UPDATE
+ return UI_INTERACTIVE
+
+/mob/living/silicon/ai/shared_ui_interaction(src_object)
+ if(lacks_power()) // Disable UIs if the AI is unpowered.
+ return UI_DISABLED
+ return ..()
+
+/mob/living/silicon/robot/shared_ui_interaction(src_object)
+ if(cell.charge <= 0 || lockcharge) // Disable UIs if the Borg is unpowered or locked.
+ return UI_DISABLED
+ return ..()
+
+/**
+ * public
+ *
+ * Check the distance for a living mob.
+ * Really only used for checks outside the context of a mob.
+ * Otherwise, use shared_living_ui_distance().
+ *
+ * required src_object The object which owns the UI.
+ * required user mob The mob who opened/is using the UI.
+ *
+ * return UI_state The state of the UI.
+ **/
+/atom/proc/contents_ui_distance(src_object, mob/living/user)
+ return user.shared_living_ui_distance(src_object) // Just call this mob's check.
+
+ /**
+ * public
+ *
+ * Distance versus interaction check.
+ *
+ * required src_object atom/movable The object which owns the UI.
+ *
+ * return UI_state The state of the UI.
+ **/
+/mob/living/proc/shared_living_ui_distance(atom/movable/src_object)
+ if(!(src_object in view(src))) // If the object is obscured, close it.
+ return UI_CLOSE
+
+ var/dist = get_dist(src_object, src)
+ if(dist <= 1) // Open and interact if 1-0 tiles away.
+ return UI_INTERACTIVE
+ else if(dist <= 2) // View only if 2-3 tiles away.
+ return UI_UPDATE
+ else if(dist <= 5) // Disable if 5 tiles away.
+ return UI_DISABLED
+ return UI_CLOSE // Otherwise, we got nothing.
+
+/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object)
+ if((TK in mutations))
+ return UI_INTERACTIVE
+ return ..()
diff --git a/code/modules/tgui/states/admin.dm b/code/modules/tgui/states/admin.dm
new file mode 100644
index 0000000000..f68c6cb48e
--- /dev/null
+++ b/code/modules/tgui/states/admin.dm
@@ -0,0 +1,12 @@
+ /**
+ * tgui state: admin_state
+ *
+ * Checks that the user is an admin, end-of-story.
+ **/
+
+/var/global/datum/ui_state/admin_state/tg_admin_state = new()
+
+/datum/ui_state/admin_state/can_use_topic(src_object, mob/user)
+ if(check_rights(R_ADMIN, 0, user))
+ return UI_INTERACTIVE
+ return UI_CLOSE
diff --git a/code/modules/tgui/states/conscious.dm b/code/modules/tgui/states/conscious.dm
new file mode 100644
index 0000000000..39da5471ea
--- /dev/null
+++ b/code/modules/tgui/states/conscious.dm
@@ -0,0 +1,12 @@
+ /**
+ * tgui state: conscious_state
+ *
+ * Only checks if the user is conscious.
+ **/
+
+/var/global/datum/ui_state/conscious_state/tg_conscious_state = new()
+
+/datum/ui_state/conscious_state/can_use_topic(src_object, mob/user)
+ if(user.stat == CONSCIOUS)
+ return UI_INTERACTIVE
+ return UI_CLOSE
diff --git a/code/modules/tgui/states/contained.dm b/code/modules/tgui/states/contained.dm
new file mode 100644
index 0000000000..a8f8ef7ff3
--- /dev/null
+++ b/code/modules/tgui/states/contained.dm
@@ -0,0 +1,12 @@
+ /**
+ * tgui state: contained_state
+ *
+ * Checks that the user is inside the src_object.
+ **/
+
+/var/global/datum/ui_state/contained_state/tg_contained_state = new()
+
+/datum/ui_state/contained_state/can_use_topic(atom/src_object, mob/user)
+ if(!src_object.contains(user))
+ return UI_CLOSE
+ return user.shared_ui_interaction(src_object)
diff --git a/code/modules/tgui/states/deep_inventory.dm b/code/modules/tgui/states/deep_inventory.dm
new file mode 100644
index 0000000000..cfd74c1826
--- /dev/null
+++ b/code/modules/tgui/states/deep_inventory.dm
@@ -0,0 +1,12 @@
+ /**
+ * tgui state: deep_inventory_state
+ *
+ * Checks that the src_object is in the user's deep (backpack, box, toolbox, etc) inventory.
+ **/
+
+/var/global/datum/ui_state/deep_inventory_state/tg_deep_inventory_state = new()
+
+/datum/ui_state/deep_inventory_state/can_use_topic(src_object, mob/user)
+ if(!user.contains(src_object))
+ return UI_CLOSE
+ return user.shared_ui_interaction(src_object)
diff --git a/code/modules/tgui/states/default.dm b/code/modules/tgui/states/default.dm
new file mode 100644
index 0000000000..8f13176f2d
--- /dev/null
+++ b/code/modules/tgui/states/default.dm
@@ -0,0 +1,55 @@
+ /**
+ * tgui state: default_state
+ *
+ * Checks a number of things -- mostly physical distance for humans and view for robots.
+ **/
+
+/var/global/datum/ui_state/default/tg_default_state = new()
+
+/datum/ui_state/default/can_use_topic(src_object, mob/user)
+ return user.tg_default_can_use_topic(src_object) // Call the individual mob-overriden procs.
+
+/mob/proc/tg_default_can_use_topic(src_object)
+ return UI_CLOSE // Don't allow interaction by default.
+
+/mob/living/tg_default_can_use_topic(src_object)
+ . = shared_ui_interaction(src_object)
+ if(. > UI_CLOSE && loc)
+ . = min(., loc.contents_ui_distance(src_object, src)) // Check the distance...
+ if(. == UI_INTERACTIVE) // Non-human living mobs can only look, not touch.
+ return UI_UPDATE
+
+/mob/living/carbon/human/tg_default_can_use_topic(src_object)
+ . = shared_ui_interaction(src_object)
+ if(. > UI_CLOSE)
+ . = min(., shared_living_ui_distance(src_object)) // Check the distance...
+ // Derp a bit if we have brain loss.
+ if(prob(getBrainLoss()))
+ return UI_UPDATE
+
+/mob/living/silicon/robot/tg_default_can_use_topic(src_object)
+ . = shared_ui_interaction(src_object)
+ if(. <= UI_DISABLED)
+ return
+
+ // Robots can interact with anything they can see.
+ if(get_dist(src, src_object) <= client.view)
+ return UI_INTERACTIVE
+ return UI_DISABLED // Otherwise they can keep the UI open.
+
+/mob/living/silicon/ai/tg_default_can_use_topic(src_object)
+ . = shared_ui_interaction(src_object)
+ if(. < UI_INTERACTIVE)
+ return
+
+ // The AI can interact with anything it can see nearby, or with cameras.
+ if((get_dist(src, src_object) <= client.view) || cameranet.checkTurfVis(get_turf_pixel(src_object)))
+ return UI_INTERACTIVE
+ return UI_CLOSE
+
+/mob/living/silicon/pai/tg_default_can_use_topic(src_object)
+ // pAIs can only use themselves and the owner's radio.
+ if((src_object == src || src_object == radio) && !stat)
+ return UI_INTERACTIVE
+ else
+ return ..()
diff --git a/code/modules/tgui/states/hands.dm b/code/modules/tgui/states/hands.dm
new file mode 100644
index 0000000000..b2af57bb44
--- /dev/null
+++ b/code/modules/tgui/states/hands.dm
@@ -0,0 +1,20 @@
+ /**
+ * tgui state: hands_state
+ *
+ * Checks that the src_object is in the user's hands.
+ **/
+
+/var/global/datum/ui_state/hands_state/tg_hands_state = new()
+
+/datum/ui_state/hands_state/can_use_topic(src_object, mob/user)
+ . = user.shared_ui_interaction(src_object)
+ if(. > UI_CLOSE)
+ return min(., user.hands_can_use_topic(src_object))
+
+/mob/proc/hands_can_use_topic(src_object)
+ return UI_CLOSE
+
+/mob/living/hands_can_use_topic(src_object)
+ if(src_object in get_both_hands(src))
+ return UI_INTERACTIVE
+ return UI_CLOSE
\ No newline at end of file
diff --git a/code/modules/tgui/states/inventory.dm b/code/modules/tgui/states/inventory.dm
new file mode 100644
index 0000000000..101fd478de
--- /dev/null
+++ b/code/modules/tgui/states/inventory.dm
@@ -0,0 +1,12 @@
+ /**
+ * tgui state: inventory_state
+ *
+ * Checks that the src_object is in the user's top-level (hand, ear, pocket, belt, etc) inventory.
+ **/
+
+/var/global/datum/ui_state/inventory_state/tg_inventory_state = new()
+
+/datum/ui_state/inventory_state/can_use_topic(src_object, mob/user)
+ if(!(src_object in user))
+ return UI_CLOSE
+ return user.shared_ui_interaction(src_object)
diff --git a/code/modules/tgui/states/notcontained.dm b/code/modules/tgui/states/notcontained.dm
new file mode 100644
index 0000000000..13612b9ff6
--- /dev/null
+++ b/code/modules/tgui/states/notcontained.dm
@@ -0,0 +1,26 @@
+ /**
+ * tgui state: notcontained_state
+ *
+ * Checks that the user is not inside src_object, and then makes the default checks.
+ **/
+
+/var/global/datum/ui_state/notcontained_state/tg_notcontained_state = new()
+
+/datum/ui_state/notcontained_state/can_use_topic(atom/src_object, mob/user)
+ . = user.shared_ui_interaction(src_object)
+ if(. > UI_CLOSE)
+ return min(., user.notcontained_can_use_topic(src_object))
+
+/mob/proc/notcontained_can_use_topic(src_object)
+ return UI_CLOSE
+
+/mob/living/notcontained_can_use_topic(atom/src_object)
+ if(src_object.contains(src))
+ return UI_CLOSE // Close if we're inside it.
+ return tg_default_can_use_topic(src_object)
+
+/mob/living/silicon/notcontained_can_use_topic(src_object)
+ return tg_default_can_use_topic(src_object) // Silicons use default bevhavior.
+
+/mob/living/simple_animal/drone/notcontained_can_use_topic(src_object)
+ return tg_default_can_use_topic(src_object) // Drones use default bevhavior.
diff --git a/code/modules/tgui/states/physical.dm b/code/modules/tgui/states/physical.dm
new file mode 100644
index 0000000000..f0c5d163a0
--- /dev/null
+++ b/code/modules/tgui/states/physical.dm
@@ -0,0 +1,24 @@
+ /**
+ * tgui state: physical_state
+ *
+ * Short-circuits the default state to only check physical distance.
+ **/
+
+/var/global/datum/ui_state/physical/tg_physical_state = new()
+
+/datum/ui_state/physical/can_use_topic(src_object, mob/user)
+ . = user.shared_ui_interaction(src_object)
+ if(. > UI_CLOSE)
+ return min(., user.physical_can_use_topic(src_object))
+
+/mob/proc/physical_can_use_topic(src_object)
+ return UI_CLOSE
+
+/mob/living/physical_can_use_topic(src_object)
+ return shared_living_ui_distance(src_object)
+
+/mob/living/silicon/physical_can_use_topic(src_object)
+ return max(UI_UPDATE, shared_living_ui_distance(src_object)) // Silicons can always see.
+
+/mob/living/silicon/ai/physical_can_use_topic(src_object)
+ return UI_UPDATE // AIs are not physical.
diff --git a/code/modules/tgui/states/self.dm b/code/modules/tgui/states/self.dm
new file mode 100644
index 0000000000..30068291e5
--- /dev/null
+++ b/code/modules/tgui/states/self.dm
@@ -0,0 +1,12 @@
+ /**
+ * tgui state: self_state
+ *
+ * Only checks that the user and src_object are the same.
+ **/
+
+/var/global/datum/ui_state/self_state/tg_self_state = new()
+
+/datum/ui_state/self_state/can_use_topic(src_object, mob/user)
+ if(src_object != user)
+ return UI_CLOSE
+ return user.shared_ui_interaction(src_object)
diff --git a/code/modules/tgui/states/zlevel.dm b/code/modules/tgui/states/zlevel.dm
new file mode 100644
index 0000000000..c1f5578bf6
--- /dev/null
+++ b/code/modules/tgui/states/zlevel.dm
@@ -0,0 +1,14 @@
+ /**
+ * tgui state: z_state
+ *
+ * Only checks that the Z-level of the user and src_object are the same.
+ **/
+
+/var/global/datum/ui_state/z_state/tg_z_state = new()
+
+/datum/ui_state/z_state/can_use_topic(src_object, mob/user)
+ var/turf/turf_obj = get_turf(src_object)
+ var/turf/turf_usr = get_turf(user)
+ if(turf_obj && turf_usr && turf_obj.z == turf_usr.z)
+ return UI_INTERACTIVE
+ return UI_CLOSE
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
new file mode 100644
index 0000000000..3c3046b0a4
--- /dev/null
+++ b/code/modules/tgui/tgui.dm
@@ -0,0 +1,369 @@
+ /**
+ * tgui
+ *
+ * /tg/station user interface library
+ **/
+
+ /**
+ * tgui datum (represents a UI).
+ **/
+/datum/tgui
+ var/mob/user // The mob who opened/is using the UI.
+ var/datum/src_object // The object which owns the UI.
+ var/title // The title of te UI.
+ var/ui_key // The ui_key of the UI. This allows multiple UIs for one src_object.
+ var/window_id // The window_id for browse() and onclose().
+ var/width = 0 // The window width.
+ var/height = 0 // The window height
+ var/window_options = list( // Extra options to winset().
+ "focus" = FALSE,
+ "titlebar" = TRUE,
+ "can_resize" = TRUE,
+ "can_minimize" = TRUE,
+ "can_maximize" = FALSE,
+ "can_close" = TRUE,
+ "auto_format" = FALSE
+ )
+ var/style = "nanotrasen" // The style to be used for this UI.
+ var/interface // The interface (template) to be used for this UI.
+ var/autoupdate = TRUE // Update the UI every MC tick.
+ var/initialized = FALSE // If the UI has been initialized yet.
+ var/list/initial_data // The data (and datastructure) used to initialize the UI.
+ var/status = UI_INTERACTIVE // The status/visibility of the UI.
+ var/datum/ui_state/state = null // Topic state used to determine status/interactability.
+ var/datum/tgui/master_ui // The parent UI.
+ var/list/datum/tgui/children = list() // Children of this UI.
+
+ /**
+ * public
+ *
+ * Create a new UI.
+ *
+ * required user mob The mob who opened/is using the UI.
+ * required src_object datum The object or datum which owns the UI.
+ * required ui_key string The ui_key of the UI.
+ * required interface string The interface used to render the UI.
+ * optional title string The title of the UI.
+ * optional width int The window width.
+ * optional height int The window height.
+ * optional master_ui datum/tgui The parent UI.
+ * optional state datum/ui_state The state used to determine status.
+ *
+ * return datum/tgui The requested UI.
+ **/
+/datum/tgui/New(mob/user, datum/src_object, ui_key, interface, title, width = 0, height = 0, datum/tgui/master_ui = null, datum/ui_state/state = tg_default_state)
+ src.user = user
+ src.src_object = src_object
+ src.ui_key = ui_key
+ src.window_id = "\ref[src_object]-[ui_key]"
+
+ set_interface(interface)
+
+ if(title)
+ src.title = sanitize(title)
+ if(width)
+ src.width = width
+ if(height)
+ src.height = height
+
+ src.master_ui = master_ui
+ if(master_ui)
+ master_ui.children += src
+ src.state = state
+
+ var/datum/asset/assets = get_asset_datum(/datum/asset/simple/tgui)
+ assets.send(user)
+
+ /**
+ * public
+ *
+ * Open this UI (and initialize it with data).
+ **/
+/datum/tgui/proc/open()
+ if(!user.client)
+ return // Bail if there is no client.
+
+ update_status(push = 0) // Update the window status.
+ if(status < UI_UPDATE)
+ return // Bail if we're not supposed to open.
+
+ if(!initial_data)
+ set_initial_data(src_object.ui_data(user)) // Get the UI data.
+
+ var/window_size = ""
+ if(width && height) // If we have a width and height, use them.
+ window_size = "size=[width]x[height];"
+
+ var/debugable = check_rights(R_DEBUG, 0, user)
+ user << browse(get_html(debugable), "window=[window_id];[window_size][list2params(window_options)]") // Open the window.
+ winset(user, window_id, "on-close=\"uiclose \ref[src]\"") // Instruct the client to signal UI when the window is closed.
+ tgui_process.on_open(src)
+
+ /**
+ * public
+ *
+ * Reinitialize the UI.
+ * (Possibly with a new interface and/or data).
+ *
+ * optional template string The name of the new interface.
+ * optional data list The new initial data.
+ **/
+/datum/tgui/proc/reinitialize(interface, list/data)
+ if(interface)
+ set_interface(interface) // Set a new interface.
+ if(data)
+ set_initial_data(data) // Replace the initial_data.
+ open()
+
+ /**
+ * public
+ *
+ * Close the UI, and all its children.
+ **/
+/datum/tgui/proc/close()
+ user << browse(null, "window=[window_id]") // Close the window.
+ tgui_process.on_close(src)
+ for(var/datum/tgui/child in children) // Loop through and close all children.
+ child.close()
+ children.Cut()
+ state = null
+ master_ui = null
+ qdel(src)
+
+ /**
+ * public
+ *
+ * Sets the browse() window options for this UI.
+ *
+ * required window_options list The window options to set.
+ **/
+/datum/tgui/proc/set_window_options(list/window_options)
+ src.window_options = window_options
+
+ /**
+ * public
+ *
+ * Set the style for this UI.
+ *
+ * required style string The new UI style.
+ **/
+/datum/tgui/proc/set_style(style)
+ src.style = lowertext(style)
+
+ /**
+ * public
+ *
+ * Set the interface (template) for this UI.
+ *
+ * required interface string The new UI interface.
+ **/
+/datum/tgui/proc/set_interface(interface)
+ src.interface = lowertext(interface)
+
+ /**
+ * public
+ *
+ * Enable/disable auto-updating of the UI.
+ *
+ * required state bool Enable/disable auto-updating.
+ **/
+/datum/tgui/proc/set_autoupdate(state = 1)
+ autoupdate = state
+
+ /**
+ * private
+ *
+ * Set the data to initialize the UI with.
+ * The datastructure cannot be changed by subsequent updates.
+ *
+ * optional data list The data/datastructure to initialize the UI with.
+ **/
+/datum/tgui/proc/set_initial_data(list/data)
+ initial_data = data
+
+ /**
+ * private
+ *
+ * Generate HTML for this UI.
+ *
+ * optional bool inline If the JSON should be inlined into the HTML (for debugging).
+ *
+ * return string UI HTML output.
+ **/
+/datum/tgui/proc/get_html(var/inline)
+ var/html
+ // Poplate HTML with JSON if we're supposed to inline.
+ if(inline)
+ html = replacetextEx(tgui_process.basehtml, "{}", get_json(initial_data))
+ else
+ html = tgui_process.basehtml
+ html = replacetextEx(html, "\[ref]", "\ref[src]")
+ html = replacetextEx(html, "\[style]", style)
+ return html
+
+ /**
+ * private
+ *
+ * Get the config data/datastructure to initialize the UI with.
+ *
+ * return list The config data.
+ **/
+/datum/tgui/proc/get_config_data()
+ var/list/config_data = list(
+ "title" = title,
+ "status" = status,
+ "screen" = src_object.ui_screen,
+ "style" = style,
+ "interface" = interface,
+ "fancy" = user.is_preference_enabled(/datum/client_preference/tgui_style),
+ "locked" = user.is_preference_enabled(/datum/client_preference/tgui_monitor),
+ "window" = window_id,
+ "ref" = "\ref[src]",
+ "user" = list(
+ "name" = user.name,
+ "ref" = "\ref[user]"
+ ),
+ "srcObject" = list(
+ "name" = "[src_object]",
+ "ref" = "\ref[src_object]"
+ )
+ )
+ return config_data
+
+ /**
+ * private
+ *
+ * Package the data to send to the UI, as JSON.
+ * This includes the UI data and config_data.
+ *
+ * return string The packaged JSON.
+ **/
+/datum/tgui/proc/get_json(list/data)
+ var/list/json_data = list()
+
+ json_data["config"] = get_config_data()
+ if(!isnull(data))
+ json_data["data"] = data
+
+ // Generate the JSON.
+ var/json = json_encode(json_data)
+ // Strip #255/improper.
+ json = replacetext(json, "\proper", "")
+ json = replacetext(json, "\improper", "")
+ return json
+
+ /**
+ * private
+ *
+ * Handle clicks from the UI.
+ * Call the src_object's ui_act() if status is UI_INTERACTIVE.
+ * If the src_object's ui_act() returns 1, update all UIs attacked to it.
+ **/
+/datum/tgui/Topic(href, href_list)
+ if(user != usr)
+ return // Something is not right here.
+
+ var/action = href_list["action"]
+ var/params = href_list; params -= "action"
+
+ switch(action)
+ if("tgui:initialize")
+ user << output(url_encode(get_json(initial_data)), "[window_id].browser:initialize")
+ initialized = TRUE
+ if("tgui:view")
+ if(params["screen"])
+ src_object.ui_screen = params["screen"]
+ tgui_process.update_uis(src_object)
+ if("tgui:link")
+ user << link(params["url"])
+ if("tgui:fancy")
+ user.set_preference(/datum/client_preference/tgui_style, TRUE)
+ if("tgui:nofrills")
+ user.set_preference(/datum/client_preference/tgui_style, FALSE)
+ else
+ update_status(push = 0) // Update the window state.
+ if(src_object.ui_act(action, params, src, state)) // Call ui_act() on the src_object.
+ tgui_process.update_uis(src_object) // Update if the object requested it.
+
+ /**
+ * private
+ *
+ * Update the UI.
+ * Only updates the data if update is true, otherwise only updates the status.
+ *
+ * optional force bool If the UI should be forced to update.
+ **/
+/datum/tgui/proc/process(force = 0)
+ var/datum/host = src_object.ui_host()
+ if(!src_object || !host || !user) // If the object or user died (or something else), abort.
+ close()
+ return
+
+ if(status && (force || autoupdate))
+ update() // Update the UI if the status and update settings allow it.
+ else
+ update_status(push = 1) // Otherwise only update status.
+
+ /**
+ * private
+ *
+ * Push data to an already open UI.
+ *
+ * required data list The data to send.
+ * optional force bool If the update should be sent regardless of state.
+ **/
+/datum/tgui/proc/push_data(data, force = 0)
+ update_status(push = 0) // Update the window state.
+ if(!initialized)
+ return // Cannot update UI if it is not set up yet.
+ if(status <= UI_DISABLED && !force)
+ return // Cannot update UI, we have no visibility.
+
+ // Send the new JSON to the update() Javascript function.
+ user << output(url_encode(get_json(data)), "[window_id].browser:update")
+
+ /**
+ * private
+ *
+ * Updates the UI by interacting with the src_object again, which will hopefully
+ * call try_ui_update on it.
+ *
+ * optional force_open bool If force_open should be passed to ui_interact.
+ **/
+/datum/tgui/proc/update(force_open = 0)
+ src_object.tg_ui_interact(user, ui_key, src, force_open, master_ui, state)
+
+ /**
+ * private
+ *
+ * Update the status/visibility of the UI for its user.
+ *
+ * optional push bool Push an update to the UI (an update is always sent for UI_DISABLED).
+ **/
+/datum/tgui/proc/update_status(push = 0)
+ var/status = src_object.ui_status(user, state)
+ if(master_ui)
+ status = min(status, master_ui.status)
+
+ set_status(status, push)
+ if(status == UI_CLOSE)
+ close()
+
+ /**
+ * private
+ *
+ * Set the status/visibility of the UI.
+ *
+ * required status int The status to set (UI_CLOSE/UI_DISABLED/UI_UPDATE/UI_INTERACTIVE).
+ * optional push bool Push an update to the UI (an update is always sent for UI_DISABLED).
+ **/
+/datum/tgui/proc/set_status(status, push = 0)
+ if(src.status != status) // Only update if status has changed.
+ if(src.status == UI_DISABLED)
+ src.status = status
+ if(push)
+ update()
+ else
+ src.status = status
+ if(status == UI_DISABLED || push) // Update if the UI just because disabled, or a push is requested.
+ push_data(null, force = 1)
diff --git a/nano/assets/nanoui.css b/nano/assets/nanoui.css
index 929902ead6..9c46a7e592 100644
--- a/nano/assets/nanoui.css
+++ b/nano/assets/nanoui.css
@@ -3504,6 +3504,120 @@ div.resize
{
background-position: -112px -128px;
}
+/*pill and bottle icons used in chem master*/
+.pillIcon32
+{
+ float: left;
+
+ width: 32px;
+ height: 32px;
+}
+.pillIcon
+{
+ float: left;
+
+ width: 32px;
+ height: 32px;
+ margin: 2px 2px 0 2px;
+
+ background-image: url(pills32.png);
+}
+.pillIcon.pill1
+{
+ background-position: 0 0;
+}
+.pillIcon.pill2
+{
+ background-position: -32px 0;
+}
+.pillIcon.pill3
+{
+ background-position: -64px 0;
+}
+.pillIcon.pill4
+{
+ background-position: -96px 0;
+}
+.pillIcon.pill5
+{
+ background-position: -128px 0;
+}
+.pillIcon.pill6
+{
+ background-position: 0 -32px;
+}
+.pillIcon.pill7
+{
+ background-position: -32px -32px;
+}
+.pillIcon.pill8
+{
+ background-position: -64px -32px;
+}
+.pillIcon.pill9
+{
+ background-position: -96px -32px;
+}
+.pillIcon.pill10
+{
+ background-position: -128px -32px;
+}
+.pillIcon.pill11
+{
+ background-position: 0 -64px;
+}
+.pillIcon.pill12
+{
+ background-position: -32px -64px;
+}
+.pillIcon.pill13
+{
+ background-position: -64px -64px;
+}
+.pillIcon.pill14
+{
+ background-position: -96px -64px;
+}
+.pillIcon.pill15
+{
+ background-position: -128px -64px;
+}
+.pillIcon.pill16
+{
+ background-position: 0 -96px;
+}
+.pillIcon.pill17
+{
+ background-position: -32px -96px;
+}
+.pillIcon.pill18
+{
+ background-position: -64px -96px;
+}
+.pillIcon.pill19
+{
+ background-position: -96px -96px;
+}
+.pillIcon.pill20
+{
+ background-position: -128px -96px;
+}
+.pillIcon.bottle1
+{
+ background-position: 0 -128px;
+}
+.pillIcon.bottle2
+{
+ background-position: -32px -128px;
+}
+.pillIcon.bottle3
+{
+ background-position: -64px -128px;
+}
+.pillIcon.bottle4
+{
+ background-position: -96px -128px;
+}
ul
{
margin: 0;
diff --git a/nano/images/nanomap_z1.png b/nano/images/nanomap_z1.png
index e4a045bf82..474abc2206 100644
Binary files a/nano/images/nanomap_z1.png and b/nano/images/nanomap_z1.png differ
diff --git a/nano/images/nanomap_z3.png b/nano/images/nanomap_z3.png
new file mode 100644
index 0000000000..5d7db2a33c
Binary files /dev/null and b/nano/images/nanomap_z3.png differ
diff --git a/nano/images/nanomap_z4.png b/nano/images/nanomap_z4.png
new file mode 100644
index 0000000000..59e310132d
Binary files /dev/null and b/nano/images/nanomap_z4.png differ
diff --git a/nano/images/nanomap_z5.png b/nano/images/nanomap_z5.png
new file mode 100644
index 0000000000..3b709cca8c
Binary files /dev/null and b/nano/images/nanomap_z5.png differ
diff --git a/nano/styles/_icon.less b/nano/styles/_icon.less
index 7de6544e0e..5cd401dc8d 100644
--- a/nano/styles/_icon.less
+++ b/nano/styles/_icon.less
@@ -59,4 +59,44 @@
/* Security Positions */
.mapIcon16.rank-warden { background-position: -112px -128px; }
.mapIcon16.rank-detective { background-position: -112px -128px; }
-.mapIcon16.rank-securityofficer { background-position: -112px -128px; }
\ No newline at end of file
+.mapIcon16.rank-securityofficer { background-position: -112px -128px; }
+
+/*pill and bottle icons used in chem master*/
+.pillIcon32 {
+ float: left;
+ width: 32px;
+ height: 32px;
+}
+
+.pillIcon {
+ float: left;
+ width: 32px;
+ height: 32px;
+ margin: 2px 2px 0 2px;
+ background-image: url(pills32.png);
+}
+
+.pillIcon.pill1 { background-position: 0 0; }
+.pillIcon.pill2 { background-position: -32px 0; }
+.pillIcon.pill3 { background-position: -64px 0; }
+.pillIcon.pill4 { background-position: -96px 0; }
+.pillIcon.pill5 { background-position: -128px 0; }
+.pillIcon.pill6 { background-position: 0 -32px; }
+.pillIcon.pill7 { background-position: -32px -32px; }
+.pillIcon.pill8 { background-position: -64px -32px; }
+.pillIcon.pill9 { background-position: -96px -32px; }
+.pillIcon.pill10 { background-position: -128px -32px; }
+.pillIcon.pill11 { background-position: 0 -64px; }
+.pillIcon.pill12 { background-position: -32px -64px; }
+.pillIcon.pill13 { background-position: -64px -64px; }
+.pillIcon.pill14 { background-position: -96px -64px; }
+.pillIcon.pill15 { background-position: -128px -64px; }
+.pillIcon.pill16 { background-position: 0 -96px; }
+.pillIcon.pill17 { background-position: -32px -96px; }
+.pillIcon.pill18 { background-position: -64px -96px; }
+.pillIcon.pill19 { background-position: -96px -96px; }
+.pillIcon.pill20 { background-position: -128px -96px; }
+.pillIcon.bottle1 { background-position: 0 -128px; }
+.pillIcon.bottle2 { background-position: -32px -128px; }
+.pillIcon.bottle3 { background-position: -64px -128px; }
+.pillIcon.bottle4 { background-position: -96px -128px; }
\ No newline at end of file
diff --git a/nano/templates/chem_master.tmpl b/nano/templates/chem_master.tmpl
index 1b8315154a..7fb093efb9 100644
--- a/nano/templates/chem_master.tmpl
+++ b/nano/templates/chem_master.tmpl
@@ -69,8 +69,8 @@
{{:helper.link('Create bottle (60 units max)', null, {'createbottle' : 1})}}
- {{:helper.link('', 'pill pill' + data.pillSprite, {'tab_select' : 'pill'}, null, 'link32')}}
- {{:helper.link('', 'pill bottle' + data.bottleSprite, {'tab_select' : 'bottle'}, null, 'link32')}}
+ {{:helper.link(" ", null, {'tab_select' : 'pill'}, null, 'link pillIcon32')}}
+ {{:helper.link(" ", null, {'tab_select' : 'bottle'}, null, 'link pillIcon32')}}
{{/if}}
{{/if}}
@@ -104,13 +104,13 @@
{{else data.tab == 'pill'}}
{{for data.pillSpritesAmount}}
- {{:helper.link('', 'pill pill' + value, {'pill_sprite' : value}, null, data.pillSprite == value ? 'linkOn link32' : 'link32')}}
+ {{:helper.link("", null, {'pill_sprite' : value}, null, data.pillSprite == value ? 'linkOn pillIcon32' : 'link pillIcon32')}}
{{/for}}
{{:helper.link('Return', 'arrowreturn-1-w', {'tab_select' : 'home'})}}
{{else data.tab == 'bottle'}}
{{for data.bottleSpritesAmount}}
- {{:helper.link('', 'pill bottle' + value, {'bottle_sprite' : value}, null, data.bottleSprite == value ? 'linkOn link32' : 'link32')}}
+ {{:helper.link("", null, {'bottle_sprite' : value}, null, data.bottleSprite == value ? 'linkOn pillIcon32' : 'link pillIcon32')}}
{{/for}}
{{:helper.link('Return', 'arrowreturn-1-w', {'tab_select' : 'home'})}}
{{/if}}
\ No newline at end of file
diff --git a/nano/templates/crew_monitor.tmpl b/nano/templates/crew_monitor.tmpl
index 5c790ee0a3..eaffe24431 100644
--- a/nano/templates/crew_monitor.tmpl
+++ b/nano/templates/crew_monitor.tmpl
@@ -2,7 +2,7 @@
Title: Crew Monitoring Console (Main content)
Used In File(s): \code\game\machinery\computer\crew.dm
-->
- ",r.insertBefore(n.lastChild,r.firstChild)}function i(){var t=w.elements;return"string"==typeof t?t.split(" "):t}function o(t,e){var n=w.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof t&&(t=t.join(" ")),w.elements=n+" "+t,l(e)}function a(t){var e=b[t[g]];return e||(e={},y++,t[g]=y,b[y]=e),e}function s(t,e,r){if(e||(e=n),h)return e.createElement(t);r||(r=a(e));var i;return i=r.cache[t]?r.cache[t].cloneNode():m.test(t)?(r.cache[t]=r.createElem(t)).cloneNode():r.createElem(t),!i.canHaveChildren||v.test(t)||i.tagUrn?i:r.frag.appendChild(i)}function u(t,e){if(t||(t=n),h)return t.createDocumentFragment();e=e||a(t);for(var r=e.frag.cloneNode(),o=0,s=i(),u=s.length;u>o;o++)r.createElement(s[o]);return r}function c(t,e){e.cache||(e.cache={},e.createElem=t.createElement,e.createFrag=t.createDocumentFragment,e.frag=e.createFrag()),t.createElement=function(n){return w.shivMethods?s(n,t,e):e.createElem(n)},t.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+i().join().replace(/[\w\-:]+/g,function(t){return e.createElem(t),e.frag.createElement(t),'c("'+t+'")'})+");return n}")(w,e.frag)}function l(t){t||(t=n);var e=a(t);return!w.shivCSS||f||e.hasCSS||(e.hasCSS=!!r(t,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),h||c(t,e),t}var f,h,p="3.7.3-pre",d=t.html5||{},v=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,m=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,g="_html5shiv",y=0,b={};!function(){try{var t=n.createElement("a");t.innerHTML="",f="hidden"in t,h=1==t.childNodes.length||function(){n.createElement("a");var t=n.createDocumentFragment();return void 0===t.cloneNode||void 0===t.createDocumentFragment||void 0===t.createElement}()}catch(e){f=!0,h=!0}}();var w={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:p,shivCSS:d.shivCSS!==!1,supportsUnknownElements:h,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:l,createElement:s,createDocumentFragment:u,addElements:o};t.html5=w,l(n),"object"==typeof e&&e.exports&&(e.exports=w)}("undefined"!=typeof window?window:this,document)},{}],194:[function(t,e,n){(function(t){(function(t){!function(t){function e(t,e,n,r){for(var o,a=n.slice(),s=i(e,t),u=0,c=a.length;c>u&&(handler=a[u],"object"==typeof handler?"function"==typeof handler.handleEvent&&handler.handleEvent(s):handler.call(t,s),!s.stoppedImmediatePropagation);u++);return o=!s.stoppedPropagation,r&&o&&t.parentNode?t.parentNode.dispatchEvent(s):!s.defaultPrevented}function n(t,e){return{configurable:!0,get:t,set:e}}function r(t,e,r){var i=y(e||t,r);m(t,"textContent",n(function(){return i.get.call(this)},function(t){i.set.call(this,t)}))}function i(t,e){return t.currentTarget=e,t.eventPhase=t.target===t.currentTarget?2:3,t}function o(t,e){for(var n=t.length;n--&&t[n]!==e;);return n}function a(){if("BR"===this.tagName)return"\n";for(var t=this.firstChild,e=[];t;)8!==t.nodeType&&7!==t.nodeType&&e.push(t.textContent),t=t.nextSibling;return e.join("")}function s(t){var e=document.createEvent("Event");e.initEvent("input",!0,!0),(t.srcElement||t.fromElement||document).dispatchEvent(e)}function u(t){!h&&_.test(document.readyState)&&(h=!h,document.detachEvent(p,u),t=document.createEvent("Event"),t.initEvent(d,!0,!0),document.dispatchEvent(t))}function c(t){for(var e;e=this.lastChild;)this.removeChild(e);null!=t&&this.appendChild(document.createTextNode(t))}function l(e,n){return n||(n=t.event),n.target||(n.target=n.srcElement||n.fromElement||document),n.timeStamp||(n.timeStamp=(new Date).getTime()),n}if(!document.createEvent){var f=!0,h=!1,p="onreadystatechange",d="DOMContentLoaded",v="__IE8__"+Math.random(),m=Object.defineProperty||function(t,e,n){t[e]=n.value},g=Object.defineProperties||function(e,n){for(var r in n)if(b.call(n,r))try{m(e,r,n[r])}catch(i){t.console&&console.log(r+" failed on object:",e,i.message)}},y=Object.getOwnPropertyDescriptor,b=Object.prototype.hasOwnProperty,w=t.Element.prototype,x=t.Text.prototype,E=/^[a-z]+$/,_=/loaded|complete/,k={},S=document.createElement("div"),A=document.documentElement,O=A.removeAttribute,C=A.setAttribute;r(t.HTMLCommentElement.prototype,w,"nodeValue"),r(t.HTMLScriptElement.prototype,null,"text"),r(x,null,"nodeValue"),r(t.HTMLTitleElement.prototype,null,"text"),m(t.HTMLStyleElement.prototype,"textContent",function(t){return n(function(){return t.get.call(this.styleSheet)},function(e){t.set.call(this.styleSheet,e)})}(y(t.CSSStyleSheet.prototype,"cssText"))),g(w,{textContent:{get:a,set:c},firstElementChild:{get:function(){for(var t=this.childNodes||[],e=0,n=t.length;n>e;e++)if(1==t[e].nodeType)return t[e]}},lastElementChild:{get:function(){for(var t=this.childNodes||[],e=t.length;e--;)if(1==t[e].nodeType)return t[e]}},oninput:{get:function(){return this._oninput||null},set:function(t){this._oninput&&(this.removeEventListener("input",this._oninput),this._oninput=t,t&&this.addEventListener("input",t))}},previousElementSibling:{get:function(){for(var t=this.previousSibling;t&&1!=t.nodeType;)t=t.previousSibling;return t}},nextElementSibling:{get:function(){for(var t=this.nextSibling;t&&1!=t.nodeType;)t=t.nextSibling;return t}},childElementCount:{get:function(){for(var t=0,e=this.childNodes||[],n=e.length;n--;t+=1==e[n].nodeType);return t}},addEventListener:{value:function(t,n,r){if("function"==typeof n||"object"==typeof n){var i,a,u=this,c="on"+t,f=u[v]||m(u,v,{value:{}})[v],h=f[c]||(f[c]={}),p=h.h||(h.h=[]);if(!b.call(h,"w")){if(h.w=function(t){return t[v]||e(u,l(u,t),p,!1)},!b.call(k,c))if(E.test(t)){try{i=document.createEventObject(),i[v]=!0,9!=u.nodeType&&(null==u.parentNode&&S.appendChild(u),(a=u.getAttribute(c))&&O.call(u,c)),u.fireEvent(c,i),k[c]=!0}catch(i){for(k[c]=!1;S.hasChildNodes();)S.removeChild(S.firstChild)}null!=a&&C.call(u,c,a)}else k[c]=!1;(h.n=k[c])&&u.attachEvent(c,h.w)}o(p,n)<0&&p[r?"unshift":"push"](n),"input"===t&&u.attachEvent("onkeyup",s)}}},dispatchEvent:{value:function(t){var n,r=this,i="on"+t.type,o=r[v],a=o&&o[i],s=!!a;return t.target||(t.target=r),s?a.n?r.fireEvent(i,t):e(r,t,a.h,!0):(n=r.parentNode)?n.dispatchEvent(t):!0,!t.defaultPrevented}},removeEventListener:{value:function(t,e,n){if("function"==typeof e||"object"==typeof e){var r=this,i="on"+t,a=r[v],s=a&&a[i],u=s&&s.h,c=u?o(u,e):-1;c>-1&&u.splice(c,1)}}}}),g(x,{addEventListener:{value:w.addEventListener},dispatchEvent:{value:w.dispatchEvent},removeEventListener:{value:w.removeEventListener}}),g(t.XMLHttpRequest.prototype,{addEventListener:{value:function(t,e,n){var r=this,i="on"+t,a=r[v]||m(r,v,{value:{}})[v],s=a[i]||(a[i]={}),u=s.h||(s.h=[]);o(u,e)<0&&(r[i]||(r[i]=function(){var e=document.createEvent("Event");e.initEvent(t,!0,!0),r.dispatchEvent(e)}),u[n?"unshift":"push"](e))}},dispatchEvent:{value:function(t){var n=this,r="on"+t.type,i=n[v],o=i&&i[r],a=!!o;return a&&(o.n?n.fireEvent(r,t):e(n,t,o.h,!0))}},removeEventListener:{value:w.removeEventListener}}),g(t.Event.prototype,{bubbles:{value:!0,writable:!0},cancelable:{value:!0,writable:!0},preventDefault:{value:function(){this.cancelable&&(this.defaultPrevented=!0,this.returnValue=!1)}},stopPropagation:{value:function(){this.stoppedPropagation=!0,this.cancelBubble=!0}},stopImmediatePropagation:{value:function(){this.stoppedImmediatePropagation=!0,this.stopPropagation()}},initEvent:{value:function(t,e,n){this.type=t,this.bubbles=!!e,this.cancelable=!!n,this.bubbles||this.stopPropagation()}}}),g(t.HTMLDocument.prototype,{defaultView:{get:function(){return this.parentWindow}},textContent:{get:function(){return 11===this.nodeType?a.call(this):null},set:function(t){11===this.nodeType&&c.call(this,t)}},addEventListener:{value:function(e,n,r){var i=this;w.addEventListener.call(i,e,n,r),f&&e===d&&!_.test(i.readyState)&&(f=!1,i.attachEvent(p,u),t==top&&!function o(t){try{i.documentElement.doScroll("left"),u()}catch(e){setTimeout(o,50)}}())}},dispatchEvent:{value:w.dispatchEvent},removeEventListener:{value:w.removeEventListener},createEvent:{value:function(t){var e;if("Event"!==t)throw Error("unsupported "+t);return e=document.createEventObject(),e.timeStamp=(new Date).getTime(),e}}}),g(t.Window.prototype,{getComputedStyle:{value:function(){function t(t){this._=t}function e(){}var n=/^(?:[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|))(?!px)[a-z%]+$/,r=/^(top|right|bottom|left)$/,i=/\-([a-z])/g,o=function(t,e){return e.toUpperCase()};return t.prototype.getPropertyValue=function(t){var e,a,s,u=this._,c=u.style,l=u.currentStyle,f=u.runtimeStyle;return t=("float"===t?"style-float":t).replace(i,o),e=l?l[t]:c[t],n.test(e)&&!r.test(t)&&(a=c.left,s=f&&f.left,s&&(f.left=l.left),c.left="fontSize"===t?"1em":e,e=c.pixelLeft+"px",c.left=a,s&&(f.left=s)),null==e?e:e+""||"auto"},e.prototype.getPropertyValue=function(){return null},function(n,r){return r?new e(n):new t(n)}}()},addEventListener:{value:function(n,r,i){var a,s=t,u="on"+n;s[u]||(s[u]=function(t){return e(s,l(s,t),a,!1)}),a=s[u][v]||(s[u][v]=[]),o(a,r)<0&&a[i?"unshift":"push"](r)}},dispatchEvent:{value:function(e){var n=t["on"+e.type];return n?n.call(t,e)!==!1&&!e.defaultPrevented:!0}},removeEventListener:{value:function(e,n,r){var i="on"+e,a=(t[i]||Object)[v],s=a?o(a,n):-1;s>-1&&a.splice(s,1)}}}),function(t,e,n){for(n=0;n=s)return(0,u["default"])({points:n});for(var f=1;s-1>=f;f++)o.push((0,c.times)(r,(0,c.minus)(n[f],n[f-1])));for(var h=[(0,c.plus)(n[0],l(o[0],o[1]))],f=1;s-2>=f;f++)h.push((0,c.minus)(n[f],(0,c.average)([o[f],o[f-1]])));h.push((0,c.minus)(n[s-1],l(o[s-2],o[s-3])));var p=h[0],d=h[1],v=n[0],m=n[1],g=(e=(0,a["default"])()).moveto.apply(e,i(v)).curveto(p[0],p[1],d[0],d[1],m[0],m[1]);return{path:(0,c.range)(2,s).reduce(function(t,e){var r=h[e],i=n[e];return t.smoothcurveto(r[0],r[1],i[0],i[1])},g),centroid:(0,c.average)(n)}},e.exports=n["default"]},{198:198,199:199,200:200}],196:[function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(n,"__esModule",{value:!0});var i=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done)&&(n.push(a.value),!e||n.length!==e);r=!0);}catch(u){i=!0,o=u}finally{try{!r&&s["return"]&&s["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),o=t(197),a=r(o),s=t(198),u=1e-5,c=function(t,e){var n=t.map(e),r=n.sort(function(t,e){var n=i(t,2),r=n[0],o=(n[1],i(e,2)),a=o[0];o[1];return r-a}),o=r.length,a=r[0][0],c=r[o-1][0],l=(0,s.minBy)(r,function(t){return t[1]}),f=(0,s.maxBy)(r,function(t){return t[1]});return a==c&&(c+=u),l==f&&(f+=u),{points:r,xmin:a,xmax:c,ymin:l,ymax:f}};n["default"]=function(t){var e=t.data,n=t.xaccessor,r=t.yaccessor,o=t.width,u=t.height,l=t.closed,f=t.min,h=t.max;n||(n=function(t){var e=i(t,2),n=e[0];e[1];return n}),r||(r=function(t){var e=i(t,2),n=(e[0],e[1]);return n});var p=function(t){return[n(t),r(t)]},d=e.map(function(t){return c(t,p)}),v=(0,s.minBy)(d,function(t){return t.xmin}),m=(0,s.maxBy)(d,function(t){return t.xmax}),g=null==f?(0,s.minBy)(d,function(t){return t.ymin}):f,y=null==h?(0,s.maxBy)(d,function(t){return t.ymax}):h;l&&(g=Math.min(g,0),y=Math.max(y,0));var b=l?0:g,w=(0,a["default"])([v,m],[0,o]),x=(0,a["default"])([g,y],[u,0]),E=function(t){var e=i(t,2),n=e[0],r=e[1];return[w(n),x(r)]};return{arranged:d,scale:E,xscale:w,yscale:x,base:b}},e.exports=n["default"]},{197:197,198:198}],197:[function(t,e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done)&&(n.push(a.value),!e||n.length!==e);r=!0);}catch(u){i=!0,o=u}finally{try{!r&&s["return"]&&s["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),i=function o(t,e){var n=r(t,2),i=n[0],a=n[1],s=r(e,2),u=s[0],c=s[1],l=function(t){return u+(c-u)*(t-i)/(a-i)};return l.inverse=function(){return o([u,c],[i,a])},l};n["default"]=i,e.exports=n["default"]},{}],198:[function(t,e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done)&&(n.push(a.value),!e||n.length!==e);r=!0);}catch(u){i=!0,o=u}finally{try{!r&&s["return"]&&s["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),i=function(t){return t.reduce(function(t,e){return t+e},0)},o=function(t){return t.reduce(function(t,e){return Math.min(t,e)})},a=function(t){return t.reduce(function(t,e){return Math.max(t,e)})},s=function(t,e){return t.reduce(function(t,n){return t+e(n)},0)},u=function(t,e){return t.reduce(function(t,n){return Math.min(t,e(n))},1/0)},c=function(t,e){return t.reduce(function(t,n){return Math.max(t,e(n))},-(1/0))},l=function(t,e){var n=r(t,2),i=n[0],o=n[1],a=r(e,2),s=a[0],u=a[1];return[i+s,o+u]},f=function(t,e){var n=r(t,2),i=n[0],o=n[1],a=r(e,2),s=a[0],u=a[1];return[i-s,o-u]},h=function(t,e){var n=r(e,2),i=n[0],o=n[1];return[t*i,t*o]},p=function(t){var e=r(t,2),n=e[0],i=e[1];return Math.sqrt(n*n+i*i)},d=function(t){return t.reduce(l,[0,0])},v=function(t){return h(1/t.length,t.reduce(l))},m=function(t,e){return h(t,[Math.sin(e),-Math.cos(e)])},g=function(t,e){var n=t||{};for(var r in n){var i=n[r];e[r]=i(e.index,e.item,e.group)}return e},y=function(t,e,n){for(var r=[],i=t;e>i;i++)r.push(i);return n&&r.push(e),r},b=function(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var a,s=Object.keys(t)[Symbol.iterator]();!(r=(a=s.next()).done);r=!0){var u=a.value,c=t[u];n.push(e(u,c))}}catch(l){i=!0,o=l}finally{try{!r&&s["return"]&&s["return"]()}finally{if(i)throw o}}return n},w=function(t){return b(t,function(t,e){return[t,e]})},x=function(t){return t};n.sum=i,n.min=o,n.max=a,n.sumBy=s,n.minBy=u,n.maxBy=c,n.plus=l,n.minus=f,n.times=h,n.id=x,n.length=p,n.sumVectors=d,n.average=v,n.onCircle=m,n.enhance=g,n.range=y,n.mapObject=b,n.pairs=w,n["default"]={sum:i,min:o,max:a,sumBy:s,minBy:u,maxBy:c,plus:l,minus:f,times:h,id:x,length:p,sumVectors:d,average:v,onCircle:m,enhance:g,range:y,mapObject:b,pairs:w}},{}],199:[function(t,e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r=function(){function t(t,e){var n=[],r=!0,i=!1,o=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done)&&(n.push(a.value),!e||n.length!==e);r=!0);}catch(u){i=!0,o=u}finally{try{!r&&s["return"]&&s["return"]()}finally{if(i)throw o}}return n}return function(e,n){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return t(e,n);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),i=function o(t){var e=t||[],n=function(t,e){var n=t.slice(0,t.length);return n.push(e),n},i=function(t,e){var n=r(t,2),i=n[0],o=n[1],a=r(e,2),s=a[0],u=a[1];return i===s&&o===u},a=function(t,e){for(var n=t.length;"0"===t.charAt(n-1);)n-=1;return"."===t.charAt(n-1)&&(n-=1),t.substr(0,n)},s=function(t,e){var n=t.toFixed(e);return a(n)},u=function(t){var e=t.command,n=t.params,r=n.map(function(t){return s(t,6)});return e+" "+r.join(" ")},c=function(t,e){var n=t.command,i=t.params,o=r(e,2),a=o[0],s=o[1];switch(n){case"M":return[i[0],i[1]];case"L":return[i[0],i[1]];case"H":return[i[0],s];case"V":return[a,i[0]];case"Z":return null;case"C":return[i[4],i[5]];case"S":return[i[2],i[3]];case"Q":return[i[2],i[3]];case"T":return[i[0],i[1]];case"A":return[i[5],i[6]]}},l=function(t,e){return function(n){var r="object"==typeof n?t.map(function(t){return n[t]}):arguments;return e.apply(null,r)}},f=function(t){return o(n(e,t))};return{moveto:l(["x","y"],function(t,e){return f({command:"M",params:[t,e]})}),lineto:l(["x","y"],function(t,e){return f({command:"L",params:[t,e]})}),hlineto:l(["x"],function(t){return f({command:"H",params:[t]})}),vlineto:l(["y"],function(t){return f({command:"V",params:[t]})}),closepath:function(){return f({command:"Z",params:[]})},curveto:l(["x1","y1","x2","y2","x","y"],function(t,e,n,r,i,o){return f({command:"C",params:[t,e,n,r,i,o]})}),smoothcurveto:l(["x2","y2","x","y"],function(t,e,n,r){return f({command:"S",params:[t,e,n,r]})}),qcurveto:l(["x1","y1","x","y"],function(t,e,n,r){return f({command:"Q",params:[t,e,n,r]})}),smoothqcurveto:l(["x","y"],function(t,e){return f({command:"T",params:[t,e]})}),arc:l(["rx","ry","xrot","largeArcFlag","sweepFlag","x","y"],function(t,e,n,r,i,o,a){return f({command:"A",params:[t,e,n,r,i,o,a]})}),print:function(){return e.map(u).join(" ")},points:function(){var t=[],n=[0,0],r=!0,i=!1,o=void 0;try{for(var a,s=e[Symbol.iterator]();!(r=(a=s.next()).done);r=!0){var u=a.value,l=c(u,n);n=l,l&&t.push(l)}}catch(f){i=!0,o=f}finally{try{!r&&s["return"]&&s["return"]()}finally{if(i)throw o}}return t},instructions:function(){return e.slice(0,e.length)},connect:function(t){var e=this.points(),n=e[e.length-1],r=t.points()[0],a=t.instructions().slice(1);return i(n,r)||a.unshift({command:"L",params:r}),o(this.instructions().concat(a))}}};n["default"]=function(){return i()},e.exports=n["default"]},{}],200:[function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(Array.isArray(t)){for(var e=0,n=Array(t.length);e1)for(var n=1;n1?e-1:0),r=1;e>r;r++)n[r-1]=arguments[r];for(var i,o;o=n.shift();)for(i in o)Fa.call(o,i)&&(t[i]=o[i]);return t}function i(t){for(var e=arguments.length,n=Array(e>1?e-1:0),r=1;e>r;r++)n[r-1]=arguments[r];return n.forEach(function(e){for(var n in e)!e.hasOwnProperty(n)||n in t||(t[n]=e[n])}),t}function o(t){return"[object Array]"===Ma.call(t)}function a(t){return Na.test(Ma.call(t))}function s(t,e){return null===t&&null===e?!0:"object"==typeof t||"object"==typeof e?!1:t===e}function u(t){return!isNaN(parseFloat(t))&&isFinite(t)}function c(t){return t&&"[object Object]"===Ma.call(t)}function l(t,e){return t.replace(/%s/g,function(){return e.shift()})}function f(t){for(var e=arguments.length,n=Array(e>1?e-1:0),r=1;e>r;r++)n[r-1]=arguments[r];throw t=l(t,n),Error(t)}function h(){Fm.DEBUG&&Pa.apply(null,arguments)}function p(t){for(var e=arguments.length,n=Array(e>1?e-1:0),r=1;e>r;r++)n[r-1]=arguments[r];t=l(t,n),ja(t,n)}function d(t){for(var e=arguments.length,n=Array(e>1?e-1:0),r=1;e>r;r++)n[r-1]=arguments[r];t=l(t,n),La[t]||(La[t]=!0,ja(t,n))}function v(){Fm.DEBUG&&p.apply(null,arguments)}function m(){Fm.DEBUG&&d.apply(null,arguments)}function g(t,e,n){var r=y(t,e,n);return r?r[t][n]:null}function y(t,e,n){for(;e;){if(n in e[t])return e;if(e.isolated)return null;e=e.parent}}function b(t){return function(){return t}}function w(t){var e,n,r,i,o,a;for(e=t.split("."),(n=Wa[e.length])||(n=x(e.length)),o=[],r=function(t,n){return t?"*":e[n]},i=n.length;i--;)a=n[i].map(r).join("."),o.hasOwnProperty(a)||(o.push(a),o[a]=!0);return o}function x(t){var e,n,r,i,o,a,s,u,c="";if(!Wa[t]){for(r=[];c.length=o;o+=1){for(n=o.toString(2);n.lengtha;a++)u.push(i(n[a]));r[o]=u}Wa[t]=r}return Wa[t]}function E(t,e,n,r){var i=t[e];if(!i||!i.equalsOrStartsWith(r)&&i.equalsOrStartsWith(n))return t[e]=i?i.replace(n,r):r,!0}function _(t){var e=t.slice(2);return"i"===t[1]&&u(e)?+e:e}function k(t){return null==t?t:(Ka.hasOwnProperty(t)||(Ka[t]=new $a(t)),Ka[t])}function S(t,e){function n(e,n){var r,i,a;return n.isRoot?a=[].concat(Object.keys(t.viewmodel.data),Object.keys(t.viewmodel.mappings),Object.keys(t.viewmodel.computations)):(r=t.viewmodel.wrapped[n.str],i=r?r.get():t.viewmodel.get(n),a=i?Object.keys(i):null),a&&a.forEach(function(t){"_ractive"===t&&o(i)||e.push(n.join(t))}),e}var r,i,a;for(r=e.str.split("."),a=[Ya];i=r.shift();)"*"===i?a=a.reduce(n,[]):a[0]===Ya?a[0]=k(i):a=a.map(A(i));return a}function A(t){return function(e){return e.join(t)}}function O(t){return t?t.replace(Ha,".$1"):""}function C(t,e,n){if("string"!=typeof e||!u(n))throw Error("Bad arguments");var r=void 0,i=void 0;if(/\*/.test(e))return i={},S(t,k(O(e))).forEach(function(e){var r=t.viewmodel.get(e);if(!u(r))throw Error(Xa);i[e.str]=r+n}),t.set(i);if(r=t.get(e),!u(r))throw Error(Xa);return t.set(e,+r+n)}function P(t,e){return Ja(this,t,void 0===e?1:+e)}function j(t){this.event=t,this.method="on"+t,this.deprecate=rs[t]}function T(t,e){var n=t.indexOf(e);-1===n&&t.push(e)}function F(t,e){for(var n=0,r=t.length;r>n;n++)if(t[n]==e)return!0;return!1}function M(t,e){var n;if(!o(t)||!o(e))return!1;if(t.length!==e.length)return!1;for(n=t.length;n--;)if(t[n]!==e[n])return!1;return!0}function N(t){return"string"==typeof t?[t]:void 0===t?[]:t}function L(t){return t[t.length-1]}function R(t,e){var n=t.indexOf(e);-1!==n&&t.splice(n,1)}function I(t){for(var e=[],n=t.length;n--;)e[n]=t[n];return e}function D(t){setTimeout(t,0);
+}function V(t,e){return function(){for(var n;n=t.shift();)n(e)}}function U(t,e,n,r){var i;if(e===t)throw new TypeError("A promise's fulfillment handler cannot return the same promise");if(e instanceof is)e.then(n,r);else if(!e||"object"!=typeof e&&"function"!=typeof e)n(e);else{try{i=e.then}catch(o){return void r(o)}if("function"==typeof i){var a,s,u;s=function(e){a||(a=!0,U(t,e,n,r))},u=function(t){a||(a=!0,r(t))};try{i.call(e,s,u)}catch(o){if(!a)return r(o),void(a=!0)}}else n(e)}}function B(t,e,n){var r;return e=O(e),"~/"===e.substr(0,2)?(r=k(e.substring(2)),W(t,r.firstKey,n)):"."===e[0]?(r=q(ls(n),e),r&&W(t,r.firstKey,n)):r=z(t,k(e),n),r}function q(t,e){var n;if(void 0!=t&&"string"!=typeof t&&(t=t.str),"."===e)return k(t);if(n=t?t.split("."):[],"../"===e.substr(0,3)){for(;"../"===e.substr(0,3);){if(!n.length)throw Error('Could not resolve reference - too many "../" prefixes');n.pop(),e=e.substring(3)}return n.push(e),k(n.join("."))}return k(t?t+e.replace(/^\.\//,"."):e.replace(/^\.\/?/,""))}function z(t,e,n,r){var i,o,a,s,u;if(e.isRoot)return e;for(o=e.firstKey;n;)if(i=n.context,n=n.parent,i&&(s=!0,a=t.viewmodel.get(i),a&&("object"==typeof a||"function"==typeof a)&&o in a))return i.join(e.str);return H(t.viewmodel,o)?e:t.parent&&!t.isolated&&(s=!0,n=t.component.parentFragment,o=k(o),u=z(t.parent,o,n,!0))?(t.viewmodel.map(o,{origin:t.parent.viewmodel,keypath:u}),e):r||s?void 0:(t.viewmodel.set(e,void 0),e)}function W(t,e){var n;!t.parent||t.isolated||H(t.viewmodel,e)||(e=k(e),(n=z(t.parent,e,t.component.parentFragment,!0))&&t.viewmodel.map(e,{origin:t.parent.viewmodel,keypath:n}))}function H(t,e){return""===e||e in t.data||e in t.computations||e in t.mappings}function G(t){t.teardown()}function K(t){t.unbind()}function $(t){t.unrender()}function Q(t){t.cancel()}function Y(t){t.detach()}function J(t){t.detachNodes()}function X(t){!t.ready||t.outros.length||t.outroChildren||(t.outrosComplete||(t.parent?t.parent.decrementOutros(t):t.detachNodes(),t.outrosComplete=!0),t.intros.length||t.totalChildren||("function"==typeof t.callback&&t.callback(),t.parent&&t.parent.decrementTotal()))}function Z(){for(var t,e,n;ps.ractives.length;)e=ps.ractives.pop(),n=e.viewmodel.applyChanges(),n&&gs.fire(e,n);for(tt(),t=0;t=0;o--)i=t._subs[e[o]],i&&(s=gt(t,i,n,r)&&s);if(zs.dequeue(t),t.parent&&s){if(a&&t.component){var u=t.component.name+"."+e[e.length-1];e=k(u).wildcardMatches(),n&&(n.component=t)}mt(t.parent,e,n,r)}}function gt(t,e,n,r){var i=null,o=!1;n&&!n._noArg&&(r=[n].concat(r)),e=e.slice();for(var a=0,s=e.length;s>a;a+=1)e[a].apply(t,r)===!1&&(o=!0);return n&&!n._noArg&&o&&(i=n.original)&&(i.preventDefault&&i.preventDefault(),i.stopPropagation&&i.stopPropagation()),!o}function yt(t){var e={args:Array.prototype.slice.call(arguments,1)};Ws(this,t,e)}function bt(t){var e;return t=k(O(t)),e=this.viewmodel.get(t,Ks),void 0===e&&this.parent&&!this.isolated&&fs(this,t.str,this.component.parentFragment)&&(e=this.viewmodel.get(t)),e}function wt(e,n){if(!this.fragment.rendered)throw Error("The API has changed - you must call `ractive.render(target[, anchor])` to render your Ractive instance. Once rendered you can use `ractive.insert()`.");if(e=t(e),n=t(n)||null,!e)throw Error("You must specify a valid target to insert into");e.insertBefore(this.detach(),n),this.el=e,(e.__ractive_instances__||(e.__ractive_instances__=[])).push(this),this.detached=null,xt(this)}function xt(t){Qs.fire(t),t.findAllComponents("*").forEach(function(t){xt(t.instance)})}function Et(t,e,n){var r,i;return t=k(O(t)),r=this.viewmodel.get(t),o(r)&&o(e)?(i=ys.start(this,!0),this.viewmodel.merge(t,r,e,n),ys.end(),i):this.set(t,e,n&&n.complete)}function _t(t,e){var n,r;return n=S(t,e),r={},n.forEach(function(e){r[e.str]=t.get(e.str)}),r}function kt(t,e,n,r){var i,o,a;e=k(O(e)),r=r||lu,e.isPattern?(i=new uu(t,e,n,r),t.viewmodel.patternObservers.push(i),o=!0):i=new Zs(t,e,n,r),i.init(r.init),t.viewmodel.register(e,i,o?"patternObservers":"observers"),i.ready=!0;var s={cancel:function(){var n;a||(o?(n=t.viewmodel.patternObservers.indexOf(i),t.viewmodel.patternObservers.splice(n,1),t.viewmodel.unregister(e,i,"patternObservers")):t.viewmodel.unregister(e,i,"observers"),a=!0)}};return t._observers.push(s),s}function St(t,e,n){var r,i,o,a;if(c(t)){n=e,i=t,r=[];for(t in i)i.hasOwnProperty(t)&&(e=i[t],r.push(this.observe(t,e,n)));return{cancel:function(){for(;r.length;)r.pop().cancel()}}}if("function"==typeof t)return n=e,e=t,t="",cu(this,t,e,n);if(o=t.split(" "),1===o.length)return cu(this,t,e,n);for(r=[],a=o.length;a--;)t=o[a],t&&r.push(cu(this,t,e,n));return{cancel:function(){for(;r.length;)r.pop().cancel()}}}function At(t,e,n){var r=this.observe(t,function(){e.apply(this,arguments),r.cancel()},{init:!1,defer:n&&n.defer});return r}function Ot(t,e){var n,r=this;if(t)n=t.split(" ").map(pu).filter(du),n.forEach(function(t){var n,i;(n=r._subs[t])&&(e?(i=n.indexOf(e),-1!==i&&n.splice(i,1)):r._subs[t]=[])});else for(t in this._subs)delete this._subs[t];return this}function Ct(t,e){var n,r,i,o=this;if("object"==typeof t){n=[];for(r in t)t.hasOwnProperty(r)&&n.push(this.on(r,t[r]));return{cancel:function(){for(var t;t=n.pop();)t.cancel()}}}return i=t.split(" ").map(pu).filter(du),i.forEach(function(t){(o._subs[t]||(o._subs[t]=[])).push(e)}),{cancel:function(){return o.off(t,e)}}}function Pt(t,e){var n=this.on(t,function(){e.apply(this,arguments),n.cancel()});return n}function jt(t,e,n){var r,i,o,a,s,u,c=[];if(r=Tt(t,e,n),!r)return null;for(i=t.length,s=r.length-2-r[1],o=Math.min(i,r[0]),a=o+r[1],u=0;o>u;u+=1)c.push(u);for(;a>u;u+=1)c.push(-1);for(;i>u;u+=1)c.push(u+s);return 0!==s?c.touchedFrom=r[0]:c.touchedFrom=t.length,c}function Tt(t,e,n){switch(e){case"splice":for(void 0!==n[0]&&n[0]<0&&(n[0]=t.length+Math.max(n[0],-t.length));n.length<2;)n.push(0);return n[1]=Math.min(n[1],t.length-n[0]),n;case"sort":case"reverse":return null;case"pop":return t.length?[t.length-1,1]:[0,0];case"push":return[t.length,0].concat(n);case"shift":return[0,t.length?1:0];case"unshift":return[0,0].concat(n)}}function Ft(e,n){var r,i,o,a=this;if(o=this.transitionsEnabled,this.noIntro&&(this.transitionsEnabled=!1),r=ys.start(this,!0),ys.scheduleTask(function(){return Tu.fire(a)},!0),this.fragment.rendered)throw Error("You cannot call ractive.render() on an already rendered instance! Call ractive.unrender() first");if(e=t(e)||this.el,n=t(n)||this.anchor,this.el=e,this.anchor=n,!this.append&&e){var s=e.__ractive_instances__;s&&s.length&&Mt(s),e.innerHTML=""}return this.cssId&&Pu.apply(),e&&((i=e.__ractive_instances__)?i.push(this):e.__ractive_instances__=[this],n?e.insertBefore(this.fragment.render(),n):e.appendChild(this.fragment.render())),ys.end(),this.transitionsEnabled=o,r.then(function(){return Fu.fire(a)})}function Mt(t){t.splice(0,t.length).forEach(G)}function Nt(t,e){for(var n=t.slice(),r=e.length;r--;)~n.indexOf(e[r])||n.push(e[r]);return n}function Lt(t,e){var n,r,i;return r='[data-ractive-css~="{'+e+'}"]',i=function(t){var e,n,i,o,a,s,u,c=[];for(e=[];n=Du.exec(t);)e.push({str:n[0],base:n[1],modifiers:n[2]});for(o=e.map(It),u=e.length;u--;)s=o.slice(),i=e[u],s[u]=i.base+r+i.modifiers||"",a=o.slice(),a[u]=r+" "+a[u],c.push(s.join(" "),a.join(" "));return c.join(", ")},n=Uu.test(t)?t.replace(Uu,r):t.replace(Iu,"").replace(Ru,function(t,e){var n,r;return Vu.test(e)?t:(n=e.split(",").map(Rt),r=n.map(i).join(", ")+" ",t.replace(e,r))})}function Rt(t){return t.trim?t.trim():t.replace(/^\s+/,"").replace(/\s+$/,"")}function It(t){return t.str}function Dt(t){t&&t.constructor!==Object&&("function"==typeof t||("object"!=typeof t?f("data option must be an object or a function, `"+t+"` is not valid"):v("If supplied, options.data should be a plain JavaScript object - using a non-POJO as the root object may work, but is discouraged")))}function Vt(t,e){Dt(e);var n="function"==typeof t,r="function"==typeof e;return e||n||(e={}),n||r?function(){var i=r?Ut(e,this):e,o=n?Ut(t,this):t;return Bt(i,o)}:Bt(e,t)}function Ut(t,e){var n=t.call(e);if(n)return"object"!=typeof n&&f("Data function must return an object"),n.constructor!==Object&&m("Data function returned something other than a plain JavaScript object. This might work, but is strongly discouraged"),n}function Bt(t,e){if(t&&e){for(var n in e)n in t||(t[n]=e[n]);return t}return t||e}function qt(t){var e=ka($u);return e.parse=function(e,n){return zt(e,n||t)},e}function zt(t,e){if(!Gu)throw Error("Missing Ractive.parse - cannot parse template. Either preparse or use the version that includes the parser");return Gu(t,e||this.options)}function Wt(t,e){var n;if(!Zo){if(e&&e.noThrow)return;throw Error("Cannot retrieve template #"+t+" as Ractive is not running in a browser.")}if(Ht(t)&&(t=t.substring(1)),!(n=document.getElementById(t))){if(e&&e.noThrow)return;throw Error("Could not find template element with id #"+t)}if("SCRIPT"!==n.tagName.toUpperCase()){if(e&&e.noThrow)return;throw Error("Template element with id #"+t+", must be a
+
+
+ {{#if data.siliconUser}}
+
+ {{data.locked ? "Engaged" : "Disengaged"}}
+
+ {{else}}
+ Swipe an ID card to {{data.locked ? "unlock" : "lock"}} this interface.
+ {{/if}}
+
+
+
+ {{#if data.locked && !data.siliconUser}}
+ {{data.isOperating ? "On" : "Off"}}
+ {{else}}
+ {{data.isOperating ? "On" : "Off"}}
+ {{/if}}
+
+
+ {{data.externalPower == 2 ? "Good" : data.externalPower == 1 ? "Low" : "None"}}
+
+
+ {{#if data.powerCellStatus != null}}
+ {{Math.fixed(adata.powerCellStatus)}}%
+ {{else}}
+ Removed
+ {{/if}}
+
+ {{#if data.powerCellStatus != null}}
+
+ {{#if data.locked && !data.siliconUser}}
+ {{data.chargeMode ? "Auto" : "Off"}}
+ {{else}}
+ {{data.chargeMode ? "Auto" : "Off"}}
+ {{/if}}
+
+ [{{data.chargingStatus == 2 ? "Fully Charged" : data.chargingStatus == 1 ? "Charging" : "Not Charging"}}]
+
+ {{/if}}
+
+
+ {{#each data.powerChannels}}
+
+ {{Math.round(adata.powerChannels[@index].powerLoad)}} W
+ {{status >= 2 ? "On" : "Off"}}
+ [{{status == 1 || status == 3 ? "Auto" : "Manual"}}]
+
+ {{#if !data.locked || data.siliconUser}}
+ Auto
+ On
+ Off
+ {{/if}}
+
+
+ {{/each}}
+
+ {{Math.round(adata.totalLoad)}} W
+
+
+{{#if data.siliconUser}}
+
+ Overload
+ {{#if data.malfStatus}}
+ {{malfButton}}
+ {{/if}}
+
+{{/if}}
+
+
+ {{#if data.locked && !data.siliconUser}}
+ {{data.coverLocked ? "Engaged" : "Disengaged"}}
+ {{else}}
+ {{data.coverLocked ? "Engaged" : "Disengaged"}}
+ {{/if}}
+
+
diff --git a/tgui/src/Unimplemented/atmos_alert.ract b/tgui/src/Unimplemented/atmos_alert.ract
new file mode 100644
index 0000000000..05bf9b797d
--- /dev/null
+++ b/tgui/src/Unimplemented/atmos_alert.ract
@@ -0,0 +1,14 @@
+
+
+ {{#each data.priority}}
+ - {{.}}
+ {{else}}
+ - No Priority Alerts
+ {{/each}}
+ {{#each data.minor}}
+ - {{.}}
+ {{else}}
+ - No Minor Alerts
+ {{/each}}
+
+
diff --git a/tgui/src/Unimplemented/atmos_control.ract b/tgui/src/Unimplemented/atmos_control.ract
new file mode 100644
index 0000000000..36035285c8
--- /dev/null
+++ b/tgui/src/Unimplemented/atmos_control.ract
@@ -0,0 +1,40 @@
+
+ {{#each adata.sensors}}
+
+
+ {{Math.fixed(pressure, 2)}} kPa
+
+ {{#if temperature}}
+
+ {{Math.fixed(temperature, 2)}} K
+
+ {{/if}}
+ {{#each gases:id}}
+
+ {{Math.fixed(., 2)}}%
+
+ {{/each}}
+
+ {{/each}}
+
+{{#if data.tank}}
+
+ {{#partial button}}
+ Reconnect
+ {{/partial}}
+
+
+ {{data.inputting ? "Injecting": "Off"}}
+
+
+ {{Math.fixed(adata.inputRate)}} L/s
+
+
+
+ {{data.outputting ? "Open": "Closed"}}
+
+
+ {{Math.round(adata.outputPressure)}} kPa
+
+
+{{/if}}
diff --git a/tgui/src/Unimplemented/atmos_filter.ract b/tgui/src/Unimplemented/atmos_filter.ract
new file mode 100644
index 0000000000..59a00efc3c
--- /dev/null
+++ b/tgui/src/Unimplemented/atmos_filter.ract
@@ -0,0 +1,25 @@
+
+
+ {{data.on ? "On" : "Off"}}
+
+
+ Set
+ Max
+ {{Math.round(adata.pressure)}} kPa
+
+
+ Nothing
+ Plasma
+ O2
+ N2
+ CO2
+ N2O
+
+
diff --git a/tgui/src/Unimplemented/atmos_mixer.ract b/tgui/src/Unimplemented/atmos_mixer.ract
new file mode 100644
index 0000000000..4f9285dae6
--- /dev/null
+++ b/tgui/src/Unimplemented/atmos_mixer.ract
@@ -0,0 +1,33 @@
+
+
+ {{data.on ? "On" : "Off"}}
+
+
+ Set
+ Max
+ {{Math.round(adata.set_pressure)}} kPa
+
+
+
+
+
+
+ {{Math.round(adata.node1_concentration)}}%
+
+
+
+
+
+
+ {{Math.round(adata.node2_concentration)}}%
+
+
diff --git a/tgui/src/Unimplemented/atmos_pump.ract b/tgui/src/Unimplemented/atmos_pump.ract
new file mode 100644
index 0000000000..2f1f47aa4f
--- /dev/null
+++ b/tgui/src/Unimplemented/atmos_pump.ract
@@ -0,0 +1,19 @@
+
+
+ {{data.on ? "On" : "Off"}}
+
+ {{#if data.max_rate}}
+
+ Set
+ Max
+ {{Math.round(adata.rate)}} L/s
+
+ {{else}}
+
+ Set
+ Max
+ {{Math.round(adata.pressure)}} kPa
+
+ {{/if}}
+
diff --git a/tgui/src/Unimplemented/chem_dispenser.ract b/tgui/src/Unimplemented/chem_dispenser.ract
new file mode 100644
index 0000000000..1faddb2ff9
--- /dev/null
+++ b/tgui/src/Unimplemented/chem_dispenser.ract
@@ -0,0 +1,38 @@
+
+
+ {{Math.fixed(adata.energy)}} Units
+
+
+
+ {{#partial button}}
+ {{#each data.beakerTransferAmounts}}
+ {{.}}
+ {{/each}}
+ {{/partial}}
+
+ {{#each data.chemicals}}
+ {{title}}
+ {{/each}}
+
+
+
+ {{#partial button}}
+ {{#each data.beakerTransferAmounts}}
+ {{.}}
+ {{/each}}
+ Eject
+ {{/partial}}
+
+ {{#if data.isBeakerLoaded}}
+ {{Math.round(adata.beakerCurrentVolume)}}/{{data.beakerMaxVolume}} Units
+
+ {{#each adata.beakerContents}}
+ {{Math.fixed(volume, 2)}} units of {{name}}
+ {{else}}
+ Beaker Empty
+ {{/each}}
+ {{else}}
+ No Beaker
+ {{/if}}
+
+
diff --git a/tgui/src/Unimplemented/chem_heater.ract b/tgui/src/Unimplemented/chem_heater.ract
new file mode 100644
index 0000000000..9093804cb7
--- /dev/null
+++ b/tgui/src/Unimplemented/chem_heater.ract
@@ -0,0 +1,29 @@
+
+
+ {{data.isActive ? "On" : "Off"}}
+
+
+ {{Math.round(adata.targetTemp)}} K
+
+
+
+ {{#partial button}}
+ Eject
+ {{/partial}}
+
+ {{#if data.isBeakerLoaded}}
+ Temperature: {{Math.round(adata.currentTemp)}} K
+
+ {{#each adata.beakerContents}}
+ {{Math.fixed(volume, 2)}} units of {{name}}
+ {{else}}
+ Beaker Empty
+ {{/each}}
+ {{else}}
+ No Beaker
+ {{/if}}
+
+
diff --git a/tgui/src/Unimplemented/cryo.ract b/tgui/src/Unimplemented/cryo.ract
new file mode 100644
index 0000000000..f7ba23b5e2
--- /dev/null
+++ b/tgui/src/Unimplemented/cryo.ract
@@ -0,0 +1,74 @@
+
+
+
+
+ {{data.occupant.name ? data.occupant.name : "No Occupant"}}
+
+ {{#if data.hasOccupant}}
+
+ {{data.occupant.stat == 0 ? "Conscious" : data.occupant.stat == 1 ? "Unconcious" : "Dead"}}
+
+
+ {{Math.round(adata.occupant.bodyTemperature)}} K
+
+
+ {{Math.round(adata.occupant.health)}}
+
+ {{#each [{label: "Brute", type: "bruteLoss"}, {label: "Respiratory", type: "oxyLoss"}, {label: "Toxin", type: "toxLoss"}, {label: "Burn", type: "fireLoss"}]}}
+
+ {{Math.round(adata.occupant[type])}}
+
+ {{/each}}
+ {{/if}}
+
+
+
+ {{data.isOperating ? "On" : "Off"}}
+
+
+ {{Math.round(adata.cellTemperature)}} K
+
+
+ {{data.isOpen ? "Open" : "Closed"}}
+ {{data.autoEject ? "Auto" : "Manual"}}
+
+
+
+ {{#partial button}}
+ Eject
+ {{/partial}}
+
+ {{#if data.isBeakerLoaded}}
+ {{#each adata.beakerContents}}
+ {{Math.fixed(volume, 2)}} units of {{name}}
+ {{else}}
+ Beaker Empty
+ {{/each}}
+ {{else}}
+ No Beaker
+ {{/if}}
+
+
diff --git a/tgui/src/Unimplemented/firealarm.ract b/tgui/src/Unimplemented/firealarm.ract
new file mode 100644
index 0000000000..52f43d5b36
--- /dev/null
+++ b/tgui/src/Unimplemented/firealarm.ract
@@ -0,0 +1,29 @@
+
+
+
+
+ {{text.titleCase(data.seclevel)}}
+
+
+
+ {{data.alarm ? "Reset" : "Activate"}}
+
+ {{#if data.emagged}}
+
+ Safety measures offline. Device may exhibit abnormal behavior.
+
+ {{/if}}
+
diff --git a/tgui/src/Unimplemented/intellicard.ract b/tgui/src/Unimplemented/intellicard.ract
new file mode 100644
index 0000000000..a1941e48f8
--- /dev/null
+++ b/tgui/src/Unimplemented/intellicard.ract
@@ -0,0 +1,42 @@
+
+
+{{#if data.wiping}}
+
+ Wipe in progress!
+
+{{/if}}
+
+ {{#partial button}}
+ {{#if data.name}}
+ Wipe AI
+ {{/if}}
+ {{/partial}}
+ {{#if data.name}}
+
+ {{data.isDead || data.isBraindead ? "Offline" : "Operational"}}
+
+
+ {{Math.round(adata.health)}}%
+
+
+ {{#each data.laws}}
+ {{.}}
+ {{/each}}
+
+
+ Wireless Activity
+ Subspace Radio
+
+ {{/if}}
+
diff --git a/tgui/src/Unimplemented/keycard_auth.ract b/tgui/src/Unimplemented/keycard_auth.ract
new file mode 100644
index 0000000000..3a7479f578
--- /dev/null
+++ b/tgui/src/Unimplemented/keycard_auth.ract
@@ -0,0 +1,16 @@
+{{#if data.waiting}}
+
+ Waiting for another device to confirm your request...
+
+{{else}}
+
+
+ {{#if data.auth_required}}
+ Authorize {{data.auth_required}}
+ {{else}}
+ Red Alert
+ Emergency Maintenance Access
+ {{/if}}
+
+
+{{/if}}
diff --git a/tgui/src/Unimplemented/mulebot.ract b/tgui/src/Unimplemented/mulebot.ract
new file mode 100644
index 0000000000..7a98a5489b
--- /dev/null
+++ b/tgui/src/Unimplemented/mulebot.ract
@@ -0,0 +1,60 @@
+
+ {{#if data.siliconUser}}
+
+ {{data.locked ? "Engaged" : "Disengaged"}}
+
+ {{else}}
+ Swipe an ID card to {{data.locked ? "unlock" : "lock"}} this interface.
+ {{/if}}
+
+
+
+ {{#if !data.locked || data.siliconUser }}
+ {{data.on ? "On" : "Off"}}
+ {{else}}
+ {{data.on ? "On" : "Off"}}
+ {{/if}}
+
+
+ {{data.cell ? data.cellPercent + "%" : "No Cell"}}
+
+
+ {{data.mode}}
+
+
+ {{data.load ? data.load : "None"}}
+
+
+ {{data.destination ? data.destination : "None"}}
+
+
+{{#if !data.locked || data.siliconUser}}
+
+ {{#partial button}}
+ {{#if data.load}}
+ Unload
+ {{/if}}
+ {{#if data.haspai}}
+ Eject PAI
+ {{/if}}
+ Set ID
+ {{/partial}}
+
+ Set Destination
+ Stop
+ Go
+
+
+ Go Home
+ Set Home
+
+
+
+ Auto-Return Home
+
+ Auto-Pickup Crate
+
+ Report Deliveries
+
+
+{{/if}}
diff --git a/tgui/src/Unimplemented/portable_pump.ract b/tgui/src/Unimplemented/portable_pump.ract
new file mode 100644
index 0000000000..c9b780f231
--- /dev/null
+++ b/tgui/src/Unimplemented/portable_pump.ract
@@ -0,0 +1,54 @@
+
+ The regulator {{data.holding ? "is" : "is not"}} connected to a tank.
+
+
+
+ {{Math.round(adata.pressure)}} kPa
+
+
+ {{data.connected ? "Connected" : "Not Connected"}}
+
+
+
+
+ {{data.on ? "On" : "Off"}}
+
+
+ {{data.direction == "out" ? "Out" : "In"}}
+
+
+ {{Math.round(adata.target_pressure)}} kPa
+
+
+ Reset
+ Min
+ Set
+ Max
+
+
+
+ {{#partial button}}
+ {{#if data.holding}}
+ Eject
+ {{/if}}
+ {{/partial}}
+ {{#if data.holding}}
+
+ {{data.holding.name}}
+
+
+ {{Math.round(adata.holding.pressure)}} kPa
+
+ {{else}}
+
+ No Holding Tank
+
+ {{/if}}
+
diff --git a/tgui/src/Unimplemented/portable_scrubber.ract b/tgui/src/Unimplemented/portable_scrubber.ract
new file mode 100644
index 0000000000..ff8edff71c
--- /dev/null
+++ b/tgui/src/Unimplemented/portable_scrubber.ract
@@ -0,0 +1,37 @@
+
+ The regulator {{data.holding ? "is" : "is not"}} connected to a tank.
+
+
+
+ {{Math.round(adata.pressure)}} kPa
+
+
+ {{data.connected ? "Connected" : "Not Connected"}}
+
+
+
+
+ {{data.on ? "On" : "Off"}}
+
+
+
+ {{#partial button}}
+ {{#if data.holding}}
+ Eject
+ {{/if}}
+ {{/partial}}
+ {{#if data.holding}}
+
+ {{data.holding.name}}
+
+
+ {{Math.round(adata.holding.pressure)}} kPa
+
+ {{else}}
+
+ No Holding Tank
+
+ {{/if}}
+
diff --git a/tgui/src/Unimplemented/power_monitor.ract b/tgui/src/Unimplemented/power_monitor.ract
new file mode 100644
index 0000000000..2ebadeccf5
--- /dev/null
+++ b/tgui/src/Unimplemented/power_monitor.ract
@@ -0,0 +1,77 @@
+
+
+
+ {{#if config.fancy}}
+
+ {{else}}
+
+ {{data.supply}} W
+
+
+ {{data.demand}} W
+
+ {{/if}}
+
+
+
+ Area
+ Charge
+ Load
+ Status
+ Equipment
+ Lighting
+ Environment
+
+ {{#each data.areas}}
+
+ {{Math.round(adata.areas[@index].charge)}} %
+ {{Math.round(adata.areas[@index].load)}} W
+ {{chargingMode(charging)}}
+ {{channelPower(eqp)}} [{{channelMode(eqp)}}]
+ {{channelPower(lgt)}} [{{channelMode(lgt)}}]
+ {{channelPower(env)}} [{{channelMode(env)}}]
+
+ {{/each}}
+
diff --git a/tgui/src/Unimplemented/radio.ract b/tgui/src/Unimplemented/radio.ract
new file mode 100644
index 0000000000..a07347b940
--- /dev/null
+++ b/tgui/src/Unimplemented/radio.ract
@@ -0,0 +1,66 @@
+
+
+
+ {{#if data.headset}}
+
+
+ {{data.listening ? "On": "Off"}}
+
+ {{else}}
+
+
+ {{data.broadcasting ? "Engaged": "Disengaged"}}
+
+
+
+ {{data.listening ? "Engaged": "Disengaged"}}
+
+ {{/if}}
+ {{#if data.command}}
+
+
+ {{data.useCommand ? "On": "Off"}}
+
+ {{/if}}
+
+
+
+ {{#if data.freqlock}}
+ {{readableFrequency}}
+ {{else}}
+
+
+ {{readableFrequency}}
+
+
+ {{/if}}
+
+ {{#if data.subspaceSwitchable}}
+
+ {{data.subspace ? "Active" : "Inactive"}}
+
+ {{/if}}
+ {{#if data.subspace && data.channels}}
+
+ {{#each data.channels:channel}}
+
+ {{channel}}
+ {{/each}}
+
+ {{/if}}
+
diff --git a/tgui/src/Unimplemented/sleeper.ract b/tgui/src/Unimplemented/sleeper.ract
new file mode 100644
index 0000000000..1cda6f0daa
--- /dev/null
+++ b/tgui/src/Unimplemented/sleeper.ract
@@ -0,0 +1,56 @@
+
+
+
+
+ {{data.occupant.name ? data.occupant.name : "No Occupant"}}
+
+ {{#if data.occupied}}
+
+ {{data.occupant.stat == 0 ? "Conscious" : data.occupant.stat == 1 ? "Unconcious" : "Dead"}}
+
+
+ {{Math.round(adata.occupant.health)}}
+
+ {{#each [{label: "Brute", type: "bruteLoss"}, {label: "Respiratory", type: "oxyLoss"}, {label: "Toxin", type: "toxLoss"}, {label: "Burn", type: "fireLoss"}]}}
+
+ {{Math.round(adata.occupant[type])}}
+
+ {{/each}}
+
+ {{data.occupant.cloneLoss ? "Damaged" : "Healthy"}}
+
+
+ {{data.occupant.brainLoss ? "Abnormal" : "Healthy"}}
+
+
+ {{#each adata.occupant.reagents}}
+ {{Math.fixed(volume, 1)}} units of {{name}}
+ {{else}}
+ Pure
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{data.open ? "Open" : "Closed"}}
+
+
+ {{#each data.chems}}
+ {{name}}
+ {{/each}}
+
+
diff --git a/tgui/src/Unimplemented/smes.ract b/tgui/src/Unimplemented/smes.ract
new file mode 100644
index 0000000000..45982141f1
--- /dev/null
+++ b/tgui/src/Unimplemented/smes.ract
@@ -0,0 +1,70 @@
+
+
+
+
+ {{Math.fixed(adata.capacityPercent)}}%
+
+
+
+
+ {{data.inputAttempt ? "Auto" : "Off"}}
+
+ [{{data.capacityPercent >= 100 ? "Fully Charged" : data.inputting ? "Charging" : "Not Charging"}}]
+
+
+ {{Math.round(adata.inputLevel)}}W
+
+
+
+
+ Set
+
+
+
+
+ {{Math.round(adata.inputAvailable)}}W
+
+
+
+
+ {{data.outputAttempt ? "On" : "Off"}}
+
+ [{{data.outputting ? "Sending" : data.charge > 0 ? "Not Sending" : "No Charge"}}]
+
+
+ {{Math.round(adata.outputLevel)}}W
+
+
+
+
+ Set
+
+
+
+
+ {{Math.round(adata.outputUsed)}}W
+
+
diff --git a/tgui/src/Unimplemented/solar_control.ract b/tgui/src/Unimplemented/solar_control.ract
new file mode 100644
index 0000000000..45af5ba7b0
--- /dev/null
+++ b/tgui/src/Unimplemented/solar_control.ract
@@ -0,0 +1,46 @@
+
+
+ {{Math.round(adata.generated)}}W
+
+
+ {{Math.round(adata.angle)}}° ({{data.direction}})
+
+
+ 15°
+ 5°
+ 5°
+ 15°
+
+
+
+
+ Off
+ Timed
+ Auto
+
+
+ {{Math.round(adata.tracking_rate)}}°/h ({{data.rotating_way}})
+
+
+ 180°
+ 30°
+ 5°
+ 5°
+ 30°
+ 180°
+
+
+
+ {{#partial button}}
+ Refresh
+ {{/partial}}
+
+ {{data.connected_tracker ? "" : "Not "}}Found
+
+
+ {{Math.round(adata.connected_panels)}} Panels Connected
+
+
diff --git a/tgui/src/Unimplemented/space_heater.ract b/tgui/src/Unimplemented/space_heater.ract
new file mode 100644
index 0000000000..9f143d9562
--- /dev/null
+++ b/tgui/src/Unimplemented/space_heater.ract
@@ -0,0 +1,45 @@
+
+ {{#partial button}}
+ {{#if data.open}}
+ Eject
+ {{/if}}
+ {{/partial}}
+
+ {{data.on ? "On" : "Off"}}
+
+
+ {{#if data.hasPowercell}}
+ {{Math.fixed(adata.powerLevel)}}%
+ {{else}}
+ No Cell
+ {{/if}}
+
+
+
+
+ {{Math.round(adata.currentTemp)}}°C
+
+
+ {{Math.round(adata.targetTemp)}}°C
+
+ {{#if data.open}}
+
+
+
+ Set
+
+
+
+ {{/if}}
+
+ {{#if data.open}}
+ Heat
+ Cool
+ Auto
+ {{else}}
+ {{text.titleCase(data.mode)}}
+ {{/if}}
+
+
diff --git a/tgui/src/Unimplemented/station_alert.ract b/tgui/src/Unimplemented/station_alert.ract
new file mode 100644
index 0000000000..56c20e302f
--- /dev/null
+++ b/tgui/src/Unimplemented/station_alert.ract
@@ -0,0 +1,11 @@
+{{#each data.alarms:class}}
+
+
+ {{#each .}}
+ - {{.}}
+ {{else}}
+ - System Nominal
+ {{/each}}
+
+
+{{/each}}
diff --git a/tgui/src/Unimplemented/suit_storage_unit.ract b/tgui/src/Unimplemented/suit_storage_unit.ract
new file mode 100644
index 0000000000..d37a7d4b20
--- /dev/null
+++ b/tgui/src/Unimplemented/suit_storage_unit.ract
@@ -0,0 +1,41 @@
+{{#if data.occupied && data.safeties}}
+
+ Biological entity detected in contents. Please remove.
+
+{{/if}}
+{{#if data.uv_active}}
+
+ Contents are being disinfected. Please wait.
+
+{{else}}
+
+ {{#partial button}}
+ {{#if !data.open}}{{data.locked ? 'Unlock' : 'Lock'}}{{/if}}
+ {{#if !data.locked}}{{data.open ? 'Close' : 'Open'}}{{/if}}
+ {{/partial}}
+ {{#if data.locked}}
+
+ Unit Locked
+
+ {{elseif data.open}}
+
+ {{data.helmet || "Empty"}}
+
+
+ {{data.suit || "Empty"}}
+
+
+ {{data.mask || "Empty"}}
+
+
+ {{data.storage || "Empty"}}
+
+ {{else}}
+ Disinfect
+ {{/if}}
+
+{{/if}}
diff --git a/tgui/src/Unimplemented/tank_dispenser.ract b/tgui/src/Unimplemented/tank_dispenser.ract
new file mode 100644
index 0000000000..220bf40e2d
--- /dev/null
+++ b/tgui/src/Unimplemented/tank_dispenser.ract
@@ -0,0 +1,8 @@
+
+
+ Plasma ({{Math.round(adata.plasma)}})
+ Oxygen ({{Math.round(adata.oxygen)}})
+
+
diff --git a/tgui/src/Unimplemented/tanks.ract b/tgui/src/Unimplemented/tanks.ract
new file mode 100644
index 0000000000..e4d08c8441
--- /dev/null
+++ b/tgui/src/Unimplemented/tanks.ract
@@ -0,0 +1,35 @@
+
+
+
+ The regulator {{data.connected? "is" : "is not"}} connected to a mask.
+
+
+
+ {{Math.round(adata.tankPressure)}} kPa
+
+
+ {{Math.round(adata.releasePressure)}} kPa
+
+
+ Reset
+ Min
+ Set
+ Max
+
+
diff --git a/tgui/src/Unimplemented/thermomachine.ract b/tgui/src/Unimplemented/thermomachine.ract
new file mode 100644
index 0000000000..136284b145
--- /dev/null
+++ b/tgui/src/Unimplemented/thermomachine.ract
@@ -0,0 +1,25 @@
+
+
+ {{Math.fixed(adata.temperature, 2)}} K
+
+
+ {{Math.fixed(adata.pressure, 2)}} kPa
+
+
+
+
+ {{data.on ? "On": "Off"}}
+
+
+
+
+ {{Math.fixed(adata.target, 2)}}
+
+
+
+
diff --git a/tgui/src/Unimplemented/uplink.ract b/tgui/src/Unimplemented/uplink.ract
new file mode 100644
index 0000000000..6c38989574
--- /dev/null
+++ b/tgui/src/Unimplemented/uplink.ract
@@ -0,0 +1,50 @@
+
+
+
+ {{#partial button}}
+ {{#if config.fancy}}
+
+ {{/if}}
+ {{#if data.lockable}}
+ Lock
+ {{/if}}
+ {{/partial}}
+
+ {{data.telecrystals}} TC
+
+
+{{#each data.categories}}
+
+ {{#each items}}
+
+ {{cost}} TC
+
+ {{/each}}
+
+{{/each}}
diff --git a/tgui/src/Unimplemented/wires.ract b/tgui/src/Unimplemented/wires.ract
new file mode 100644
index 0000000000..a0ee953ebd
--- /dev/null
+++ b/tgui/src/Unimplemented/wires.ract
@@ -0,0 +1,16 @@
+
+ {{#each data.wires}}
+
+ {{cut ? "Mend" : "Cut"}}
+ Pulse
+ {{attached ? "Detach" : "Attach"}}
+
+ {{/each}}
+
+{{#if data.status}}
+
+ {{#each data.status}}
+ {{.}}
+ {{/each}}
+
+{{/if}}
diff --git a/tgui/src/components/bar.ract b/tgui/src/components/bar.ract
new file mode 100644
index 0000000000..f4d18125ef
--- /dev/null
+++ b/tgui/src/components/bar.ract
@@ -0,0 +1,16 @@
+
+
+
diff --git a/tgui/src/components/bar.styl b/tgui/src/components/bar.styl
new file mode 100644
index 0000000000..0e2b52cda8
--- /dev/null
+++ b/tgui/src/components/bar.styl
@@ -0,0 +1,32 @@
+context = selector()
+
+.bar
+ display: inline-block
+ position: relative
+ vertical-align: middle
+ width: 100%
+ height: 20px
+ line-height: @height - 3px
+ padding: 1px
+
+ border: 1px solid bar-color-border
+ background: bar-color-background
+
+ .barText
+ @extend {context} $fontReset
+ position: absolute
+ top: 0
+ right: 3px
+
+ .barFill
+ display: block
+ height: 100%
+
+ transition: background-color 1s
+ background-color: bar-color-normal
+ &.good
+ background-color: bar-color-good
+ &.average
+ background-color: bar-color-average
+ &.bad
+ background-color: bar-color-bad
diff --git a/tgui/src/components/button.ract b/tgui/src/components/button.ract
new file mode 100644
index 0000000000..e61164160a
--- /dev/null
+++ b/tgui/src/components/button.ract
@@ -0,0 +1,59 @@
+
+
+
+ {{#if icon}}
+
+ {{/if}}
+ {{yield}}
+
diff --git a/tgui/src/components/button.styl b/tgui/src/components/button.styl
new file mode 100644
index 0000000000..f02c779a81
--- /dev/null
+++ b/tgui/src/components/button.styl
@@ -0,0 +1,41 @@
+context = selector()
+
+buttoncolor(selector, color)
+ &.{selector}
+ transition: background-color 0.5s
+ background-color: color
+ &.{selector}.active:hover,
+ &.{selector}.active:focus
+ transition: background-color 0.25s
+ background-color: lighten(color, button-lighten-hover)
+ outline: 0
+
+span.button
+ @extend {context} $fontReset
+ display: inline-block
+ vertical-align: middle
+ height: 20px
+ line-height: @height - 3px
+ padding: 0 5px
+ white-space: nowrap
+
+ border: 1px solid button-color-border
+
+ .fa
+ padding-right: 2px
+
+ buttoncolor(normal, button-color-normal)
+ buttoncolor(disabled, button-color-disabled)
+ buttoncolor(selected, button-color-selected)
+ buttoncolor(caution, button-color-caution)
+ buttoncolor(danger, button-color-danger)
+
+ &.gridable
+ width: 125px
+ margin: 2px 0
+
+span:not(.button) + span.button
+ margin-left: 5px
+
+span.button + span:not(.button)
+ margin-left: 5px
diff --git a/tgui/src/components/display.ract b/tgui/src/components/display.ract
new file mode 100644
index 0000000000..2b37a0235d
--- /dev/null
+++ b/tgui/src/components/display.ract
@@ -0,0 +1,13 @@
+
+ {{#if title}}
+
+ {{title}}
+ {{#if button}}
+ {{yield button}}
+ {{/if}}
+
+ {{/if}}
+
+ {{yield}}
+
+
diff --git a/tgui/src/components/display.styl b/tgui/src/components/display.styl
new file mode 100644
index 0000000000..02b4d5d2a7
--- /dev/null
+++ b/tgui/src/components/display.styl
@@ -0,0 +1,29 @@
+div.display
+ width: 100%
+ padding: 4px
+ margin: 6px 0
+
+ background-color: display-color-background // Transparent background.
+ box-shadow: inset 0 0 5px display-color-shadow
+
+ header
+ display: block
+ position: relative
+ width: 100%
+ padding: 0 4px
+ margin-bottom: 6px
+
+ color: display-color-title
+
+ border-bottom: rule-size solid rule-color-normal
+
+ .buttonRight
+ position: absolute
+ bottom: 6px
+ right: 4px
+
+ article
+ display: table
+ width: 100%
+
+ border-collapse: collapse;
diff --git a/tgui/src/components/input.ract b/tgui/src/components/input.ract
new file mode 100644
index 0000000000..8114cead81
--- /dev/null
+++ b/tgui/src/components/input.ract
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/tgui/src/components/input.styl b/tgui/src/components/input.styl
new file mode 100644
index 0000000000..4669519939
--- /dev/null
+++ b/tgui/src/components/input.styl
@@ -0,0 +1,16 @@
+input
+ display: inline-block
+ vertical-align: middle
+ height: 20px
+ line-height: @height - 3px
+ padding: 0 5px
+ white-space: nowrap
+
+ color: input-color-text
+ background-color: input-color-background
+ border: 1px solid input-color-border
+
+ &::placeholder
+ color: input-color-placeholder
+ &::-ms-clear
+ display: none
diff --git a/tgui/src/components/linegraph.ract b/tgui/src/components/linegraph.ract
new file mode 100644
index 0000000000..a2c20a0c3b
--- /dev/null
+++ b/tgui/src/components/linegraph.ract
@@ -0,0 +1,88 @@
+
+
+
diff --git a/tgui/src/components/linegraph.styl b/tgui/src/components/linegraph.styl
new file mode 100644
index 0000000000..a3226b28b2
--- /dev/null
+++ b/tgui/src/components/linegraph.styl
@@ -0,0 +1,2 @@
+svg.linegraph
+ overflow: hidden
diff --git a/tgui/src/components/notice.ract b/tgui/src/components/notice.ract
new file mode 100644
index 0000000000..c23c27b122
--- /dev/null
+++ b/tgui/src/components/notice.ract
@@ -0,0 +1,3 @@
+
+ {{yield}}
+
diff --git a/tgui/src/components/notice.styl b/tgui/src/components/notice.styl
new file mode 100644
index 0000000000..10ea60a953
--- /dev/null
+++ b/tgui/src/components/notice.styl
@@ -0,0 +1,27 @@
+div.notice
+ margin: 8px 0
+ padding: 4px
+
+ box-shadow: none
+
+ color: text-color-inverse
+ font-weight: bold
+ font-style: italic
+
+ background-color: notice-color-first
+ background-image: repeating-linear-gradient(
+ -45deg,
+ notice-color-first,
+ notice-color-first 10px,
+ notice-color-second 10px,
+ notice-color-second 20px
+ )
+
+ .label
+ color: text-color-inverse
+
+ .content:only-of-type
+ padding: 0
+
+ hr
+ background-color: rule-color-dark
diff --git a/tgui/src/components/resize.ract b/tgui/src/components/resize.ract
new file mode 100644
index 0000000000..7065ee4572
--- /dev/null
+++ b/tgui/src/components/resize.ract
@@ -0,0 +1,29 @@
+
+
+{{#if config.fancy}}
+
+{{/if}}
diff --git a/tgui/src/components/resize.styl b/tgui/src/components/resize.styl
new file mode 100644
index 0000000000..f5f5fe37aa
--- /dev/null
+++ b/tgui/src/components/resize.styl
@@ -0,0 +1,12 @@
+div.resize
+ position: fixed
+ bottom: 0
+ right: 0
+ width: 0
+ height: 0
+
+ border-style: solid;
+ border-width: 0 0 45px 45px;
+ border-color: transparent transparent resize-color transparent
+
+ transform: rotate(360deg)
diff --git a/tgui/src/components/section.ract b/tgui/src/components/section.ract
new file mode 100644
index 0000000000..d229d2c454
--- /dev/null
+++ b/tgui/src/components/section.ract
@@ -0,0 +1,12 @@
+
+ {{#if label}}
+ {{label}}:
+ {{/if}}
+ {{#if nowrap}}
+ {{yield}}
+ {{else}}
+
+ {{yield}}
+
+ {{/if}}
+
diff --git a/tgui/src/components/section.styl b/tgui/src/components/section.styl
new file mode 100644
index 0000000000..d208433355
--- /dev/null
+++ b/tgui/src/components/section.styl
@@ -0,0 +1,33 @@
+/$cell
+ display: table-cell
+ margin: 0
+ text-align: left
+ vertical-align: middle
+ padding: 3px 2px
+
+section
+ display: table-row
+ width: 100%
+
+ &:not(:first-child)
+ padding-top: 4px
+
+ &.candystripe:nth-child(even)
+ background-color: section-color-candystripe
+
+ .label
+ @extend $cell
+ width: 1%
+ padding-right: 32px
+ white-space: nowrap
+
+ color: section-color-label
+
+ .content
+ @extend $cell
+ &:not(:last-child)
+ padding-right: 16px
+
+ .line
+ @extend $cell
+ width: 100%
diff --git a/tgui/src/components/subdisplay.ract b/tgui/src/components/subdisplay.ract
new file mode 100644
index 0000000000..8d6cd641f9
--- /dev/null
+++ b/tgui/src/components/subdisplay.ract
@@ -0,0 +1,11 @@
+
+ {{#if title}}
+
+ {{title}}
+ {{#if button}}{{yield button}}{{/if}}
+
+ {{/if}}
+
+ {{yield}}
+
+
diff --git a/tgui/src/components/subdisplay.styl b/tgui/src/components/subdisplay.styl
new file mode 100644
index 0000000000..11420ecba7
--- /dev/null
+++ b/tgui/src/components/subdisplay.styl
@@ -0,0 +1,11 @@
+context = selector()
+
+div.subdisplay
+ width: 100%
+ margin: 0
+
+ header
+ @extend {context} div.display header
+
+ article
+ @extend {context} div.display article
diff --git a/tgui/src/components/tabs.ract b/tgui/src/components/tabs.ract
new file mode 100644
index 0000000000..2a7db40a51
--- /dev/null
+++ b/tgui/src/components/tabs.ract
@@ -0,0 +1,27 @@
+
+
+
+
+
+ {{#each tabs}}
+ {{.}}
+ {{/each}}
+
+
+ {{>content}}
+
diff --git a/tgui/src/components/tabs/tab.ract b/tgui/src/components/tabs/tab.ract
new file mode 100644
index 0000000000..b02bba5c20
--- /dev/null
+++ b/tgui/src/components/tabs/tab.ract
@@ -0,0 +1,3 @@
+{{#if shown}}
+ {{yield}}
+{{/if}}
diff --git a/tgui/src/components/titlebar.ract b/tgui/src/components/titlebar.ract
new file mode 100644
index 0000000000..a67832cef8
--- /dev/null
+++ b/tgui/src/components/titlebar.ract
@@ -0,0 +1,56 @@
+
+
+
+
+ {{yield}}
+ {{#if config.fancy}}
+
+
+ {{/if}}
+
diff --git a/tgui/src/components/titlebar.styl b/tgui/src/components/titlebar.styl
new file mode 100644
index 0000000000..628fe75745
--- /dev/null
+++ b/tgui/src/components/titlebar.styl
@@ -0,0 +1,65 @@
+context = selector()
+
+$titleButton
+ display: inline-block
+ position: relative
+ padding: 7px // To make a bigger clickable area.
+ margin: -7px
+
+ color: titlebar-color-button;
+
+ &:hover
+ color: lighten(titlebar-color-button, button-lighten-hover)
+
+header.titlebar
+ position: fixed;
+ z-index: 1
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 32px;
+
+ background-color: titlebar-color-background
+ border-bottom: 1px solid titlebar-color-coreshadow
+ box-shadow: 0 3px 3px titlebar-color-shadow
+
+ .statusicon
+ position: absolute
+ top: 4px
+ left: 12px
+ transition: color 0.5s
+
+ .title
+ position: absolute
+ top: 6px
+ left: 46px
+
+ color: titlebar-color-text
+ font-size: 16px
+ white-space: nowrap
+
+ .minimize
+ @extend {context} $titleButton
+ position: absolute
+ top: 6px
+ right: 46px
+ .close
+ @extend {context} $titleButton
+ position: absolute
+ top: 4px
+ right: 12px
+
+/.no-icons header.titlebar
+ .statusicon
+ font-size: 20px
+ &::after
+ content: "O"
+ .minimize
+ top: -2px
+ font-size: 20px
+ &::after
+ content: "—"
+ .close
+ font-size: 20px
+ &::after
+ content: "X"
diff --git a/tgui/src/components/warnings.ract b/tgui/src/components/warnings.ract
new file mode 100644
index 0000000000..74e4819f4a
--- /dev/null
+++ b/tgui/src/components/warnings.ract
@@ -0,0 +1,47 @@
+
+
+
+{{#if config.fancy && ie && ie < 11}}
+
+ You have an old (IE{{ie}}), end-of-life (click 'EOL Info' for more information) version of Internet Explorer installed.
+ To upgrade, click 'Upgrade IE' to download IE11 from Microsoft.
+ If you are unable to upgrade directly, click 'IE VMs' to download a VM with IE11 or Edge from Microsoft.
+ Otherwise, click 'No Frills' below to disable potentially incompatible features (and this message).
+
+ No Frills
+
+ Upgrade IE
+
+ IE VMs
+
+ EOL Info
+ Debug Info
+ {{#if debug}}
+
+ Detected: IE{{ie}}
+ User Agent: {{userAgent}}
+ {{/if}}
+
+{{/if}}
diff --git a/tgui/src/images/nanotrasen.svg b/tgui/src/images/nanotrasen.svg
new file mode 100644
index 0000000000..d21b9f0a2a
--- /dev/null
+++ b/tgui/src/images/nanotrasen.svg
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/tgui/src/images/syndicate.svg b/tgui/src/images/syndicate.svg
new file mode 100644
index 0000000000..c2863b790d
--- /dev/null
+++ b/tgui/src/images/syndicate.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/tgui/src/interfaces/brig_timer.ract b/tgui/src/interfaces/brig_timer.ract
new file mode 100644
index 0000000000..28637da6b4
--- /dev/null
+++ b/tgui/src/interfaces/brig_timer.ract
@@ -0,0 +1,38 @@
+
+
+
+ {{#partial button}}
+ {{data.timing ? "Stop" : "Start"}}
+ {{#each data.flashes}}
+ {{status ? "Flash" : "Recharging"}}
+ {{/each}}
+ {{/partial}}
+
+
+
+ {{#if data.timing}}
+ {{text.zeroPad(getminute, 2)}}:{{text.zeroPad(getsecond, 2)}}
+ {{else}}
+ {{text.zeroPad(setminute, 2)}}:{{text.zeroPad(setsecond, 2)}}
+ {{/if}}
+
+
+
+
diff --git a/tgui/src/interfaces/canister.ract b/tgui/src/interfaces/canister.ract
new file mode 100644
index 0000000000..48ef646694
--- /dev/null
+++ b/tgui/src/interfaces/canister.ract
@@ -0,0 +1,62 @@
+
+ The regulator {{data.hasHoldingTank ? "is" : "is not"}} connected to a tank.
+
+
+ {{#partial button}}
+ Relabel
+ {{/partial}}
+
+ {{Math.round(adata.tankPressure)}} kPa
+
+
+ {{data.portConnected ? "Connected" : "Not Connected"}}
+
+
+
+
+ {{Math.round(adata.releasePressure)}} kPa
+
+
+
+
+
+
+
+
+
+
+
+
+ {{data.valveOpen ? "Open" : "Closed"}}
+
+
+
+ {{#partial button}}
+ {{#if data.hasHoldingTank}}
+ Eject
+ {{/if}}
+ {{/partial}}
+ {{#if data.hasHoldingTank}}
+
+ {{data.holdingTank.name}}
+
+
+ {{Math.round(adata.holdingTank.tankPressure)}} kPa
+
+ {{else}}
+
+ No Holding Tank
+
+ {{/if}}
+
diff --git a/tgui/src/interfaces/cargo.ract b/tgui/src/interfaces/cargo.ract
new file mode 100644
index 0000000000..5c7918c180
--- /dev/null
+++ b/tgui/src/interfaces/cargo.ract
@@ -0,0 +1,101 @@
+
+
+{{#if data.active}}
+
+
+ Cargo station logged in as register.
+ Log Out
+
+
+{{/if}}
+
+
+ Location: {{data.location}}
+ {{#if data.active}}
+ Undock Shuttle
+ {{/if}}
+
+
+ {{Math.floor(adata.points)}}
+
+
+{{#if data.active}}
+
+ {{#partial button}}
+ Clear
+ {{/partial}}
+ {{#each data.cart}}
+
+ #{{id}}
+ {{object}}
+ {{cost}} Points
+ By: {{orderer}}
+ Reason: {{reason}}
+
+ Remove
+
+
+
+ {{else}}
+ Nothing in Cart
+ {{/each}}
+
+{{/if}}
+
+ {{#partial button}}
+ {{#if data.active}}
+ Clear
+ {{/if}}
+ {{/partial}}
+ {{#each data.requests}}
+
+ #{{id}}
+ {{object}}
+ {{cost}} Points
+ By {{orderer}}
+ Reason: {{reason}}
+
+ {{#if data.active}}
+ Approve
+ Deny
+ {{/if}}
+ {{data.active ? "" :"Print Receipt"}}
+
+
+ {{else}}
+ No Requests
+ {{/each}}
+
+{{#if data.canorder}}
+
+ {{#each adata.supplies}}
+
+ {{#each pack}}
+
+ {{cost}} Credits
+
+ {{/each}}
+
+ {{/each}}
+
+{{else}}
+
+ Cargo Request services are disabled until a register is activated.
+
+{{/if}}
+{{#if !data.active}}
+
+
+ Log in to register mode (Will log out current register):
+ Log In
+
+
+{{/if}}
diff --git a/tgui/src/interfaces/error.ract b/tgui/src/interfaces/error.ract
new file mode 100644
index 0000000000..23d76c2248
--- /dev/null
+++ b/tgui/src/interfaces/error.ract
@@ -0,0 +1,3 @@
+
+ The requested interface ({{config.interface}}) was not found. Does it exist?
+
diff --git a/tgui/src/interfaces/jukebox.ract b/tgui/src/interfaces/jukebox.ract
new file mode 100644
index 0000000000..b02105fca9
--- /dev/null
+++ b/tgui/src/interfaces/jukebox.ract
@@ -0,0 +1,11 @@
+
+
+ Play
+ Stop
+
+
+
+ {{#each data.tracks}}
+ {{.}}
+ {{/each}}
+
diff --git a/tgui/src/interfaces/resleever.ract b/tgui/src/interfaces/resleever.ract
new file mode 100644
index 0000000000..29c3a21fc4
--- /dev/null
+++ b/tgui/src/interfaces/resleever.ract
@@ -0,0 +1,18 @@
+
+
+ {{data.name}}
+
+
+ {{data.lace}}
+
+
+
+
+
+ Start Procedure
+
+
+ Eject Occupant
+ Eject Neural Lace
+
diff --git a/tgui/src/interfaces/suit_sensor_jammer.ract b/tgui/src/interfaces/suit_sensor_jammer.ract
new file mode 100644
index 0000000000..13a0e47154
--- /dev/null
+++ b/tgui/src/interfaces/suit_sensor_jammer.ract
@@ -0,0 +1,22 @@
+
+
+ Enable
+ Disable
+
+
+
+
+ Decrease
+ Increase
+
+
+
+ {{#each data.methods}}
+ {{name}} - {{cost}}
+ {{/each}}
+
+
+
+ {{Math.fixed(data.current_charge*100/data.max_charge)}}%
+
+
diff --git a/tgui/src/styles/nanotrasen.styl b/tgui/src/styles/nanotrasen.styl
new file mode 100644
index 0000000000..14f6c7d574
--- /dev/null
+++ b/tgui/src/styles/nanotrasen.styl
@@ -0,0 +1,7 @@
+body.nanotrasen
+ background: data-url('images/nanotrasen.svg') no-repeat fixed center/70% 70%,
+ linear-gradient(to bottom,
+ background-color-start 0%,
+ background-color-end 100%)
+ @import "util/*"
+ @import "components/*"
diff --git a/tgui/src/styles/syndicate.styl b/tgui/src/styles/syndicate.styl
new file mode 100644
index 0000000000..e9d11c2e8a
--- /dev/null
+++ b/tgui/src/styles/syndicate.styl
@@ -0,0 +1,30 @@
+body.syndicate
+ color-good = pale-green
+ color-highlight = black
+ background-color-start = #750000
+ background-color-end = #340404
+ rule-color-normal = dark-gray
+ titlebar-color-text = pale-red
+ titlebar-color-button = pale-red
+ display-color-background = alpha(#000, 0.5)
+ display-color-shadow = alpha(#000, 0.75)
+ notice-color-first = #750000
+ notice-color-second = #910101
+ section-color-label = white
+ bar-color-normal = black
+ bar-color-good = pale-green
+ bar-color-border = black
+ button-color-normal = dark-green
+ button-color-disabled = gray
+ button-color-selected = dark-red
+ button-color-caution = yellow-orange
+ button-color-danger = yellow
+ input-color-text = white
+ input-color-background = dark-red
+
+ background: data-url('images/syndicate.svg') no-repeat fixed center/70% 70%,
+ linear-gradient(to bottom,
+ background-color-start 0%,
+ background-color-end 100%)
+ @import "util/*"
+ @import "components/*"
diff --git a/tgui/src/tgui.js b/tgui/src/tgui.js
new file mode 100644
index 0000000000..21adfb4859
--- /dev/null
+++ b/tgui/src/tgui.js
@@ -0,0 +1,53 @@
+// Temporarily import Ractive first to keep it from detecting ie8's object.defineProperty shim, which it misuses (ractivejs/ractive#2343).
+import Ractive from 'ractive'
+Ractive.DEBUG = /minified/.test(() => {/* minified */})
+
+import 'ie8'
+import 'babel-polyfill'
+import 'dom4'
+import 'html5shiv'
+
+// Extend the Math builtin with our own utilities.
+Object.assign(Math, require('util/math'))
+
+// Set up the initialize function. This is either called below if JSON is provided
+// inline, or called by the server if it was not.
+import TGUI from 'tgui.ract'
+window.initialize = (dataString) => {
+ if (window.tgui) return // Don't run twice.
+ window.tgui = new TGUI({
+ el: '#container',
+ data () {
+ const initial = JSON.parse(dataString)
+ return {
+ constants: require('util/constants'),
+ text: require('util/text'),
+ config: initial.config,
+ data: initial.data,
+ adata: initial.data
+ }
+ }
+ })
+}
+
+// Try to find data in the page. If the JSON was inlined, load it.
+const holder = document.getElementById('data')
+const data = holder.textContent
+const ref = holder.getAttribute('data-ref')
+if (data !== '{}') {
+ window.initialize(data)
+ holder.remove()
+}
+// Let the server know we're set up. This also sends data if it was not inlined.
+import { act } from 'util/byond'
+act(ref, 'tgui:initialize')
+
+// Load fonts.
+import { loadCSS } from 'fg-loadcss'
+loadCSS('https://cdn.jsdelivr.net/fontawesome/4.5.0/css/font-awesome.min.css')
+// Handle font loads.
+import FontFaceObserver from 'fontfaceobserver'
+const fontawesome = new FontFaceObserver('FontAwesome')
+fontawesome.check('\uf240')
+ .then(() => document.body.classList.add('icons'))
+ .catch(() => document.body.classList.add('no-icons'))
diff --git a/tgui/src/tgui.ract b/tgui/src/tgui.ract
new file mode 100644
index 0000000000..e5d93aebf8
--- /dev/null
+++ b/tgui/src/tgui.ract
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+{{{config.title}}}
+
+
+
+
+
diff --git a/tgui/src/tgui.styl b/tgui/src/tgui.styl
new file mode 100644
index 0000000000..3fc64c9045
--- /dev/null
+++ b/tgui/src/tgui.styl
@@ -0,0 +1,134 @@
+@charset "utf-8"
+
+white = white
+pale-red = #e74242
+red = #b00e0e
+dark-red = #9d0808
+yellow-orange = #be6209
+yellow = #9a9d00
+pale-green = #73E573
+green = #2f943c
+grass-green = #537d29
+dark-green = #397439
+royal-blue = #40628a
+pale-blue = #8ba5c4
+black = black
+black-gray = #161616
+dark-gray = #272727
+gray = #363636
+light-gray = #999999
+
+// Branding Colors
+color-normal = royal-blue
+color-good = grass-green
+color-average = yellow-orange
+color-bad = red
+color-highlight = pale-blue
+
+// Text
+text-color-normal = white
+text-color-inverse = black
+
+// Background
+background-color-start = #2a2a2a
+background-color-end = #202020
+
+// Rules ( etc)
+rule-color-normal = royal-blue
+rule-color-dark = dark-gray
+rule-size = 2px
+
+// Titlebar
+titlebar-color-text = pale-blue
+titlebar-color-button = pale-blue
+titlebar-color-background = gray
+titlebar-color-coreshadow = black-gray
+titlebar-color-shadow = alpha(#000, 0.1)
+
+// Resize
+resize-color = gray
+
+// Display
+display-color-title = white
+display-color-background = alpha(#000, 0.33)
+display-color-shadow = alpha(#000, 0.5)
+
+// Notice
+notice-color-first = #bb9b68
+notice-color-second = #b1905d
+notice-color-border = dark-gray
+
+// Section
+section-color-label = pale-blue
+section-color-candystripe = alpha(#000, 0.2)
+
+// Bar
+bar-color-normal = color-normal
+bar-color-good = color-good
+bar-color-average = color-average
+bar-color-bad = color-bad
+bar-color-border = royal-blue
+bar-color-background = dark-gray
+
+// Buttons
+button-color-normal = royal-blue
+button-color-disabled = light-gray
+button-color-selected = green
+button-color-caution = yellow
+button-color-danger = dark-red
+button-color-border = dark-gray
+button-lighten-hover = 15%
+button-alpha-disabled = .75
+
+// Input
+input-color-text = black
+input-color-placeholder = light-gray
+input-color-border = dark-gray
+input-color-background = white
+
+// Tooltips
+tooltip-color-border = dark-gray
+tooltip-color-background = gray
+
+html, body
+ box-sizing: border-box
+ height: 100%
+ margin: 0
+
+html
+ overflow: hidden
+ cursor: default // Reset the cursor.
+
+body
+ overflow: auto
+
+ font-family: Verdana, Geneva, sans-serif
+ font-size: 12px
+ color: text-color-normal
+
+ background-color: background-color-start
+ background-image: linear-gradient(to bottom,
+ background-color-start 0%,
+ background-color-end 100%)
+
+*, *:before, *:after
+ box-sizing: inherit
+
+$h
+ display: inline-block
+ margin: 0
+ padding: 6px 0
+h1
+ @extend $h
+ font-size: 18px
+h2
+ @extend $h
+ font-size: 16px
+h3
+ @extend $h
+ font-size: 14px
+h4
+ @extend $h
+ font-size: 12px
+
+@require "styles/*"
diff --git a/tgui/src/util/byond.js b/tgui/src/util/byond.js
new file mode 100644
index 0000000000..beb3157809
--- /dev/null
+++ b/tgui/src/util/byond.js
@@ -0,0 +1,16 @@
+const encode = encodeURIComponent
+
+// Helper to generate a BYOND href given 'params' as an object (with an optional 'url' for eg winset).
+export function href (params = {}, url = '') {
+ return `byond://${url}?` + Object.keys(params).map(key => `${encode(key)}=${encode(params[key])}`).join('&')
+}
+
+// Helper to make a BYOND ui_act() call on the UI 'src' given an 'action' and optional 'params'.
+export function act (src, action, params = {}) {
+ window.location.href = href(Object.assign({ src, action }, params))
+}
+
+// Helper to make a BYOND winset() call on 'window', setting 'key' to 'value'
+export function winset (win, key, value) {
+ window.location.href = href({[`${win}.${key}`]: value}, 'winset')
+}
diff --git a/tgui/src/util/colors.styl b/tgui/src/util/colors.styl
new file mode 100644
index 0000000000..d1f17a7768
--- /dev/null
+++ b/tgui/src/util/colors.styl
@@ -0,0 +1,10 @@
+.normal
+ color: color-normal
+.good
+ color: color-good
+.average
+ color: color-average
+.bad
+ color: color-bad
+.highlight
+ color: color-highlight
diff --git a/tgui/src/util/constants.js b/tgui/src/util/constants.js
new file mode 100644
index 0000000000..b7dac6eb67
--- /dev/null
+++ b/tgui/src/util/constants.js
@@ -0,0 +1,5 @@
+// Constants used in tgui; these are mirrored from the BYOND code.
+export const UI_INTERACTIVE = 2
+export const UI_UPDATE = 1
+export const UI_DISABLED = 0
+export const UI_CLOSE = -1
diff --git a/tgui/src/util/dragresize.js b/tgui/src/util/dragresize.js
new file mode 100644
index 0000000000..d31e32d641
--- /dev/null
+++ b/tgui/src/util/dragresize.js
@@ -0,0 +1,51 @@
+import {winset} from './byond'
+
+export function lock (x, y) {
+ if (x < 0) { // Left
+ x = 0
+ } else if (x + window.innerWidth > window.screen.availWidth) { // Right
+ x = window.screen.availWidth - window.innerWidth
+ }
+
+ if (y < 0) { // Top
+ y = 0
+ } else if (y + window.innerHeight > window.screen.availHeight) { // Bottom
+ y = window.screen.availHeight - window.innerHeight
+ }
+
+ return {x, y}
+}
+
+export function drag (event) {
+ event.preventDefault()
+
+ if (!this.get('drag')) return
+
+ if (this.get('x')) {
+ let x = (event.screenX - this.get('x')) + window.screenLeft
+ let y = (event.screenY - this.get('y')) + window.screenTop
+ if (this.get('config.locked')) ({x, y} = lock(x, y)) // Lock to primary monitor.
+ winset(this.get('config.window'), 'pos', `${x},${y}`)
+ }
+ this.set({ x: event.screenX, y: event.screenY })
+}
+
+export function sane (x, y) {
+ x = Math.clamp(100, window.screen.width, x)
+ y = Math.clamp(100, window.screen.height, y)
+ return {x, y}
+}
+
+export function resize (event) {
+ event.preventDefault()
+
+ if (!this.get('resize')) return
+
+ if (this.get('x')) {
+ let x = (event.screenX - this.get('x')) + window.innerWidth
+ let y = (event.screenY - this.get('y')) + window.innerHeight
+ ;({x, y} = sane(x, y))
+ winset(this.get('config.window'), 'size', `${x},${y}`)
+ }
+ this.set({ x: event.screenX, y: event.screenY })
+}
diff --git a/tgui/src/util/filter.js b/tgui/src/util/filter.js
new file mode 100644
index 0000000000..66e9360052
--- /dev/null
+++ b/tgui/src/util/filter.js
@@ -0,0 +1,22 @@
+export function filterMulti (displays, string) {
+ for (let display of displays) { // First check if the display includes the search term in the first place.
+ if (display.textContent.toLowerCase().includes(string)) {
+ display.style.display = ''
+ filter(display, string)
+ } else {
+ display.style.display = 'none'
+ }
+ }
+}
+
+export function filter (display, string) {
+ const items = display.queryAll('section')
+ const titleMatch = display.query('header').textContent.toLowerCase().includes(string)
+ for (let item of items) { // Check if the item or its displays title contains the search term.
+ if (titleMatch || item.textContent.toLowerCase().includes(string)) {
+ item.style.display = ''
+ } else {
+ item.style.display = 'none'
+ }
+ }
+}
diff --git a/tgui/src/util/math.js b/tgui/src/util/math.js
new file mode 100644
index 0000000000..39a4b0c424
--- /dev/null
+++ b/tgui/src/util/math.js
@@ -0,0 +1,9 @@
+// Helper to limit a number to be inside 'min' and 'max'.
+export function clamp (min, max, number) {
+ return Math.max(min, Math.min(number, max))
+}
+
+// Helper to round a number to 'decimals' decimals.
+export function fixed (number, decimals = 1) {
+ return Number(Math.round(number + 'e' + decimals) + 'e-' + decimals)
+}
diff --git a/tgui/src/util/misc.styl b/tgui/src/util/misc.styl
new file mode 100644
index 0000000000..08dd093547
--- /dev/null
+++ b/tgui/src/util/misc.styl
@@ -0,0 +1,12 @@
+main
+ display: block
+ margin-top: 32px
+ padding: 2px 6px 0
+
+hr
+ height: rule-size
+ background-color: rule-color-normal
+ border: none
+
+.hidden
+ display: none
diff --git a/tgui/src/util/text.js b/tgui/src/util/text.js
new file mode 100644
index 0000000000..657160b673
--- /dev/null
+++ b/tgui/src/util/text.js
@@ -0,0 +1,14 @@
+export function upperCaseFirst (str) {
+ return str[0].toUpperCase() + str.slice(1).toLowerCase()
+}
+
+export function titleCase (str) {
+ return str.replace(/\w\S*/g, upperCaseFirst)
+}
+
+export function zeroPad (str, pad_size) {
+ str = str.toString()
+ while(str.length < pad_size)
+ str = '0' + str
+ return str
+}
diff --git a/tgui/src/util/text.styl b/tgui/src/util/text.styl
new file mode 100644
index 0000000000..752bb120cc
--- /dev/null
+++ b/tgui/src/util/text.styl
@@ -0,0 +1,17 @@
+// Helper to reset font options to default.
+$fontReset
+ color: text-color-normal
+ font-size: 12px
+ font-weight: normal
+ font-style: normal
+ text-decoration: none
+
+// Utility classes to set font styles.
+.bold
+ font-weight: bold
+.italic
+ font-style: italic
+
+// Make 'unselectable' text unselectable on all browsers.
+[unselectable=on]
+ user-select: none
diff --git a/tgui/src/util/tooltip.styl b/tgui/src/util/tooltip.styl
new file mode 100644
index 0000000000..a951b82876
--- /dev/null
+++ b/tgui/src/util/tooltip.styl
@@ -0,0 +1,53 @@
+div[data-tooltip], span[data-tooltip]
+ position: relative
+
+ &::after
+ position: absolute
+ display: block
+ z-index: 2
+ width: 250px
+ padding: 10px
+ transform: translateX(-50%)
+
+ visibility: hidden
+ opacity: 0
+
+ white-space: normal
+ text-align: left
+ content: attr(data-tooltip)
+
+ transition: all .5s
+ border: 1px solid tooltip-color-border
+ background-color: tooltip-color-background
+
+ &:hover::after
+ visibility: visible
+ opacity: 1
+
+ &.tooltip-top::after
+ bottom: 100%
+ left: 50%
+ transform: translateX(-50%) translateY(8px)
+ &.tooltip-top:hover::after
+ transform: translateX(-50%) translateY(-8px)
+
+ &.tooltip-bottom::after
+ top: 100%
+ left: 50%
+ transform: translateX(-50%) translateY(-8px)
+ &.tooltip-bottom:hover::after
+ transform: translateX(-50%) translateY(8px)
+
+ &.tooltip-left::after
+ top: 50%
+ right: 100%
+ transform: translateX(8px) translateY(-50%)
+ &.tooltip-left:hover::after
+ transform: translateX(-8px) translateY(-50%)
+
+ &.tooltip-right::after
+ top: 50%
+ left: 100%
+ transform: translateX(-8px) translateY(-50%)
+ &.tooltip-right:hover::after
+ transform: translateX(8px) translateY(-50%)
diff --git a/tgui/tgui.html b/tgui/tgui.html
new file mode 100644
index 0000000000..adcb337b94
--- /dev/null
+++ b/tgui/tgui.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
|