code, still not enabled

This commit is contained in:
Letter N
2020-07-22 10:27:37 +08:00
parent 827a1ad523
commit 49941ea2e6
23 changed files with 936 additions and 602 deletions
+330 -16
View File
@@ -1,3 +1,12 @@
/**
* tgui subsystem
*
* Contains all tgui state and subsystem code.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
SUBSYSTEM_DEF(tgui) SUBSYSTEM_DEF(tgui)
name = "tgui" name = "tgui"
wait = 9 wait = 9
@@ -5,33 +14,338 @@ SUBSYSTEM_DEF(tgui)
priority = FIRE_PRIORITY_TGUI priority = FIRE_PRIORITY_TGUI
runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
var/list/currentrun = list() /// A list of UIs scheduled to process
var/list/open_uis = list() // A list of open UIs, grouped by src_object and ui_key. var/list/current_run = list()
var/list/processing_uis = list() // A list of processing UIs, ungrouped. /// A list of open UIs
var/basehtml // The HTML base used for all UIs. var/list/open_uis = list()
/// A list of open UIs, grouped by src_object.
var/list/open_uis_by_src = list()
/// The HTML base used for all UIs.
var/basehtml
/datum/controller/subsystem/tgui/PreInit() /datum/controller/subsystem/tgui/PreInit()
basehtml = file2text('tgui/packages/tgui/public/tgui.html') basehtml = file2text('tgui/packages/tgui/public/tgui.html')
/datum/controller/subsystem/tgui/Shutdown() /datum/controller/subsystem/tgui/Shutdown()
close_all_uis() close_all_uis()
/datum/controller/subsystem/tgui/stat_entry() /datum/controller/subsystem/tgui/stat_entry()
..("P:[processing_uis.len]") ..("P:[open_uis.len]")
/datum/controller/subsystem/tgui/fire(resumed = 0) /datum/controller/subsystem/tgui/fire(resumed = 0)
if (!resumed) if(!resumed)
src.currentrun = processing_uis.Copy() src.current_run = open_uis.Copy()
//cache for sanic speed (lists are references anyways) // Cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun var/list/current_run = src.current_run
while(current_run.len)
while(currentrun.len) var/datum/tgui/ui = current_run[current_run.len]
var/datum/tgui/ui = currentrun[currentrun.len] current_run.len--
currentrun.len-- // TODO: Move user/src_object check to process()
if(ui && ui.user && ui.src_object) if(ui && ui.user && ui.src_object)
ui.process() ui.process()
else else
processing_uis.Remove(ui) open_uis.Remove(ui)
if (MC_TICK_CHECK) if(MC_TICK_CHECK)
return return
/**
* public
*
* Requests a usable tgui window from the pool.
* Returns null if pool was exhausted.
*
* required user mob
* return datum/tgui
*/
/datum/controller/subsystem/tgui/proc/request_pooled_window(mob/user)
if(!user.client)
return null
var/list/windows = user.client.tgui_windows
var/window_id
var/datum/tgui_window/window
var/window_found = FALSE
// Find a usable window
for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT)
window_id = TGUI_WINDOW_ID(i)
window = windows[window_id]
// As we are looping, create missing window datums
if(!window)
window = new(user.client, window_id, pooled = TRUE)
// Skip windows with acquired locks
if(window.locked)
continue
if(window.status == TGUI_WINDOW_READY)
return window
if(window.status == TGUI_WINDOW_CLOSED)
window.status = TGUI_WINDOW_LOADING
window_found = TRUE
break
if(!window_found)
log_tgui(user, "Error: Pool exhausted")
return null
return window
/**
* public
*
* Force closes all tgui windows.
*
* required user mob
*/
/datum/controller/subsystem/tgui/proc/force_close_all_windows(mob/user)
log_tgui(user, "force_close_all_windows")
if(user.client)
user.client.tgui_windows = list()
for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT)
var/window_id = TGUI_WINDOW_ID(i)
user << browse(null, "window=[window_id]")
/**
* public
*
* Force closes the tgui window by window_id.
*
* required user mob
* required window_id string
*/
/datum/controller/subsystem/tgui/proc/force_close_window(mob/user, window_id)
log_tgui(user, "force_close_window")
// Close all tgui datums based on window_id.
for(var/datum/tgui/ui in user.tgui_open_uis)
if(ui.window && ui.window.id == window_id)
ui.close(can_be_suspended = FALSE)
// Unset machine just to be sure.
user.unset_machine()
// Close window directly just to be sure.
user << browse(null, "window=[window_id]")
/**
* public
*
* Try to find an instance of a UI, and push an update to it.
*
* required user mob The mob who opened/is using the UI.
* required src_object datum The object/datum which owns the UI.
* optional ui datum/tgui The UI to be updated, if it exists.
* optional force_open bool If the UI should be re-opened instead of updated.
*
* return datum/tgui The found UI.
*/
/datum/controller/subsystem/tgui/proc/try_update_ui(
mob/user,
datum/src_object,
datum/tgui/ui)
// Look up a UI if it wasn't passed
if(isnull(ui))
ui = get_open_ui(user, src_object)
// Couldn't find a UI.
if(isnull(ui))
return null
ui.process_status()
// UI ended up with the closed status
// or is actively trying to close itself.
// FIXME: Doesn't actually fix the paper bug.
if(ui.status <= UI_CLOSE)
ui.close()
return null
ui.send_update()
return ui
/**
* public
*
* Get a open UI given a user and src_object.
*
* required user mob The mob who opened/is using the UI.
* required src_object datum The object/datum which owns the UI.
*
* return datum/tgui The found UI.
*/
/datum/controller/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object)
var/key = "[REF(src_object)]"
// No UIs opened for this src_object
if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list))
return null
for(var/datum/tgui/ui in open_uis_by_src[key])
// Make sure we have the right user
if(ui.user == user)
return ui
return null
/**
* public
*
* Update all UIs attached to src_object.
*
* required src_object datum The object/datum which owns the UIs.
*
* return int The number of UIs updated.
*/
/datum/controller/subsystem/tgui/proc/update_uis(datum/src_object)
var/count = 0
var/key = "[REF(src_object)]"
// No UIs opened for this src_object
if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list))
return count
for(var/datum/tgui/ui in open_uis_by_src[key])
// Check if UI is valid.
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user))
ui.process(force = 1)
count++
return count
/**
* public
*
* Close all UIs attached to src_object.
*
* required src_object datum The object/datum which owns the UIs.
*
* return int The number of UIs closed.
*/
/datum/controller/subsystem/tgui/proc/close_uis(datum/src_object)
var/count = 0
var/key = "[REF(src_object)]"
// No UIs opened for this src_object
if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list))
return count
for(var/datum/tgui/ui in open_uis_by_src[key])
// Check if UI is valid.
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user))
ui.close()
count++
return count
/**
* public
*
* Close all UIs regardless of their attachment to src_object.
*
* return int The number of UIs closed.
*/
/datum/controller/subsystem/tgui/proc/close_all_uis()
var/count = 0
for(var/key in open_uis_by_src)
for(var/datum/tgui/ui in open_uis_by_src[key])
// Check if UI is valid.
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user))
ui.close()
count++
return count
/**
* public
*
* 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.
*
* return int The number of UIs updated.
*/
/datum/controller/subsystem/tgui/proc/update_user_uis(mob/user, datum/src_object)
var/count = 0
if(length(user?.tgui_open_uis) == 0)
return count
for(var/datum/tgui/ui in user.tgui_open_uis)
if(isnull(src_object) || ui.src_object == src_object)
ui.process(force = 1)
count++
return count
/**
* public
*
* 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.
*
* return int The number of UIs closed.
*/
/datum/controller/subsystem/tgui/proc/close_user_uis(mob/user, datum/src_object)
var/count = 0
if(length(user?.tgui_open_uis) == 0)
return count
for(var/datum/tgui/ui in user.tgui_open_uis)
if(isnull(src_object) || ui.src_object == src_object)
ui.close()
count++
return count
/**
* private
*
* Add a UI to the list of open UIs.
*
* required ui datum/tgui The UI to be added.
*/
/datum/controller/subsystem/tgui/proc/on_open(datum/tgui/ui)
var/key = "[REF(ui.src_object)]"
if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list))
open_uis_by_src[key] = list()
ui.user.tgui_open_uis |= ui
var/list/uis = open_uis_by_src[key]
uis |= ui
open_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/subsystem/tgui/proc/on_close(datum/tgui/ui)
var/key = "[REF(ui.src_object)]"
if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list))
return FALSE
// Remove it from the list of processing UIs.
open_uis.Remove(ui)
// If the user exists, remove it from them too.
if(ui.user)
ui.user.tgui_open_uis.Remove(ui)
var/list/uis = open_uis_by_src[key]
uis.Remove(ui)
if(length(uis) == 0)
open_uis_by_src.Remove(key)
return TRUE
/**
* 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/subsystem/tgui/proc/on_logout(mob/user)
close_user_uis(user)
/**
* private
*
* Handle clients switching mobs, by transferring 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/subsystem/tgui/proc/on_transfer(mob/source, mob/target)
// The old mob had no open UIs.
if(length(source?.tgui_open_uis) == 0)
return FALSE
if(isnull(target.tgui_open_uis) || !istype(target.tgui_open_uis, /list))
target.tgui_open_uis = list()
// Transfer all the UIs.
for(var/datum/tgui/ui in source.tgui_open_uis)
// Inform the UIs of their new owner.
ui.user = target
target.tgui_open_uis.Add(ui)
// Clear the old list.
source.tgui_open_uis.Cut()
return TRUE
+75 -40
View File
@@ -1,7 +1,8 @@
/** /**
* tgui external * External tgui definitions, such as src_object APIs.
* *
* Contains all external tgui declarations. * Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
/** /**
@@ -11,13 +12,9 @@
* If this proc is not implemented properly, the UI will not update correctly. * If this proc is not implemented properly, the UI will not update correctly.
* *
* required user mob The mob who opened/is using the UI. * 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 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/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) /datum/proc/ui_interact(mob/user, datum/tgui/ui)
return FALSE // Not implemented. return FALSE // Not implemented.
/** /**
@@ -37,10 +34,11 @@
* public * public
* *
* Static Data to be sent to the UI. * Static Data to be sent to the UI.
* Static data differs from normal data in that it's large data that should be sent infrequently *
* This is implemented optionally for heavy uis that would be sending a lot of redundant data * Static data differs from normal data in that it's large data that should be
* frequently. * sent infrequently. This is implemented optionally for heavy uis that would
* Gets squished into one object on the frontend side, but the static part is cached. * be sending a lot of redundant data frequently. Gets squished into one
* object on the frontend side, but the static part is cached.
* *
* required user mob The mob interacting with the UI. * required user mob The mob interacting with the UI.
* *
@@ -52,18 +50,17 @@
/** /**
* public * public
* *
* Forces an update on static data. Should be done manually whenever something happens to change static data. * Forces an update on static data. Should be done manually whenever something
* happens to change static data.
* *
* required user the mob currently interacting with the ui * required user the mob currently interacting with the ui
* optional ui ui to be updated * optional ui ui to be updated
* optional ui_key ui key of ui to be updated
*/ */
/datum/proc/update_static_data(mob/user, datum/tgui/ui, ui_key = "main") /datum/proc/update_static_data(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui_key, ui)
// If there was no ui to update, there's no static data to update either.
if(!ui) if(!ui)
return ui = SStgui.get_open_ui(user, src)
ui.push_data(null, ui_static_data(), TRUE) if(ui)
ui.send_full_update()
/** /**
* public * public
@@ -85,17 +82,12 @@
* public * public
* *
* Called on an object when a tgui object is being created, allowing you to * Called on an object when a tgui object is being created, allowing you to
* customise the html * push various assets to tgui, for examples spritesheets.
* For example: inserting a custom stylesheet that you need in the head
* *
* For this purpose, some tags are available in the html, to be parsed out * return list List of asset datums or file paths.
^ with replacetext
* (customheadhtml) - Additions to the head tag
*
* required html the html base text
*/ */
/datum/proc/ui_base_html(html) /datum/proc/ui_assets(mob/user)
return html return list()
/** /**
* private * private
@@ -107,6 +99,15 @@
/datum/proc/ui_host(mob/user) /datum/proc/ui_host(mob/user)
return src // Default src. return src // Default src.
/**
* private
*
* The UI's state controller to be used for created uis
* This is a proc over a var for memory reasons
*/
/datum/proc/ui_state(mob/user)
return GLOB.default_state
/** /**
* global * global
* *
@@ -118,9 +119,17 @@
/** /**
* global * global
* *
* Used to track UIs for a mob. * Tracks open UIs for a user.
*/ */
/mob/var/list/open_uis = list() /mob/var/list/tgui_open_uis = list()
/**
* global
*
* Tracks open windows for a user.
*/
/client/var/list/tgui_windows = list()
/** /**
* public * public
* *
@@ -137,17 +146,43 @@
* *
* required uiref ref The UI that was closed. * required uiref ref The UI that was closed.
*/ */
/client/verb/uiclose(ref as text) /client/verb/uiclose(window_id as text)
// Name the verb, and hide it from the user panel. // Name the verb, and hide it from the user panel.
set name = "uiclose" set name = "uiclose"
set hidden = 1 set hidden = TRUE
var/mob/user = src && src.mob
if(!user)
return
// Close all tgui datums based on window_id.
SStgui.force_close_window(user, window_id)
// Get the UI based on the ref. /**
var/datum/tgui/ui = locate(ref) * Middleware for /client/Topic.
*
// If we found the UI, close it. * return bool Whether the topic is passed (TRUE), or cancelled (FALSE).
if(istype(ui)) */
ui.close() /proc/tgui_Topic(href_list)
// Unset machine just to be sure. // Skip non-tgui topics
if(src && src.mob) if(!href_list["tgui"])
src.mob.unset_machine() return TRUE
var/type = href_list["type"]
// Unconditionally collect tgui logs
if(type == "log")
log_tgui(usr, href_list["message"])
// Locate window
var/window_id = href_list["window_id"]
var/datum/tgui_window/window
if(window_id)
window = usr.client.tgui_windows[window_id]
if(!window)
log_tgui(usr, "Error: Couldn't find the window datum, force closing.")
SStgui.force_close_window(usr, window_id)
return FALSE
// Decode payload
var/payload
if(href_list["payload"])
payload = json_decode(href_list["payload"])
// Pass message to window
if(window)
window.on_message(type, payload, href_list)
return FALSE
+38 -19
View File
@@ -1,7 +1,9 @@
/** /**
* tgui states * Base state and helpers for states. Just does some sanity checks,
* implement a proper state for in-depth checks.
* *
* Base state and helpers for states. Just does some sanity checks, implement a state for in-depth checks. * Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
/** /**
@@ -22,13 +24,14 @@
if(isobserver(user)) if(isobserver(user))
// If they turn on ghost AI control, admins can always interact. // If they turn on ghost AI control, admins can always interact.
if(IsAdminGhost(user)) if(isAdminGhostAI(user)) //IsAdminGhost
. = max(., UI_INTERACTIVE) . = max(., UI_INTERACTIVE)
// Regular ghosts can always at least view if in range. // Regular ghosts can always at least view if in range.
var/clientviewlist = getviewsize(user.client.view) if(user.client)
if(get_dist(src_object, user) < max(clientviewlist[1],clientviewlist[2])) var/clientviewlist = getviewsize(user.client.view)
. = max(., UI_UPDATE) if(get_dist(src_object, user) < max(clientviewlist[1], clientviewlist[2]))
. = max(., UI_UPDATE)
// Check if the state allows interaction // Check if the state allows interaction
var/result = state.can_use_topic(src_object, user) var/result = state.can_use_topic(src_object, user)
@@ -46,7 +49,8 @@
* return UI_state The state of the UI. * return UI_state The state of the UI.
*/ */
/datum/ui_state/proc/can_use_topic(src_object, mob/user) /datum/ui_state/proc/can_use_topic(src_object, mob/user)
return UI_CLOSE // Don't allow interaction by default. // Don't allow interaction by default.
return UI_CLOSE
/** /**
* public * public
@@ -56,21 +60,31 @@
* return UI_state The state of the UI. * return UI_state The state of the UI.
*/ */
/mob/proc/shared_ui_interaction(src_object) /mob/proc/shared_ui_interaction(src_object)
if(!client) // Close UIs if mindless. // Close UIs if mindless.
if(!client)
return UI_CLOSE return UI_CLOSE
else if(stat) // Disable UIs if unconcious. // Disable UIs if unconcious.
else if(stat)
return UI_DISABLED return UI_DISABLED
else if(incapacitated() || lying) // Update UIs if incapicitated but concious. // Update UIs if incapicitated but concious.
else if(incapacitated())
return UI_UPDATE return UI_UPDATE
return UI_INTERACTIVE return UI_INTERACTIVE
/mob/living/shared_ui_interaction(src_object)
. = ..()
if(!(mobility_flags & MOBILITY_UI) && . == UI_INTERACTIVE)
return UI_UPDATE
/mob/living/silicon/ai/shared_ui_interaction(src_object) /mob/living/silicon/ai/shared_ui_interaction(src_object)
if(lacks_power()) // Disable UIs if the AI is unpowered. // Disable UIs if the AI is unpowered.
if(lacks_power())
return UI_DISABLED return UI_DISABLED
return ..() return ..()
/mob/living/silicon/robot/shared_ui_interaction(src_object) /mob/living/silicon/robot/shared_ui_interaction(src_object)
if(!cell || cell.charge <= 0 || locked_down) // Disable UIs if the Borg is unpowered or locked. // Disable UIs if the Borg is unpowered or locked.
if(!cell || cell.charge <= 0 || lockcharge)
return UI_DISABLED return UI_DISABLED
return ..() return ..()
@@ -87,7 +101,8 @@
* return UI_state The state of the UI. * return UI_state The state of the UI.
*/ */
/atom/proc/contents_ui_distance(src_object, mob/living/user) /atom/proc/contents_ui_distance(src_object, mob/living/user)
return user.shared_living_ui_distance(src_object) // Just call this mob's check. // Just call this mob's check.
return user.shared_living_ui_distance(src_object)
/** /**
* public * public
@@ -99,17 +114,21 @@
* return UI_state The state of the UI. * return UI_state The state of the UI.
*/ */
/mob/living/proc/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE) /mob/living/proc/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE)
if(viewcheck && !(src_object in view(src))) // If the object is obscured, close it. // If the object is obscured, close it.
if(viewcheck && !(src_object in view(src)))
return UI_CLOSE return UI_CLOSE
var/dist = get_dist(src_object, src) var/dist = get_dist(src_object, src)
if(dist <= 1 || src_object.hasSiliconAccessInArea(src)) // Open and interact if 1-0 tiles away. // Open and interact if 1-0 tiles away.
if(dist <= 1)
return UI_INTERACTIVE return UI_INTERACTIVE
else if(dist <= 2) // View only if 2-3 tiles away. // View only if 2-3 tiles away.
else if(dist <= 2)
return UI_UPDATE return UI_UPDATE
else if(dist <= 5) // Disable if 5 tiles away. // Disable if 5 tiles away.
else if(dist <= 5)
return UI_DISABLED return UI_DISABLED
return UI_CLOSE // Otherwise, we got nothing. // Otherwise, we got nothing.
return UI_CLOSE
/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE) /mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE)
if(dna.check_mutation(TK) && tkMaxRangeCheck(src, src_object)) if(dna.check_mutation(TK) && tkMaxRangeCheck(src, src_object))
+3
View File
@@ -2,6 +2,9 @@
* tgui state: admin_state * tgui state: admin_state
* *
* Checks that the user is an admin, end-of-story. * Checks that the user is an admin, end-of-story.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new) GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new)
+3
View File
@@ -2,6 +2,9 @@
* tgui state: always_state * tgui state: always_state
* *
* Always grants the user UI_INTERACTIVE. Period. * Always grants the user UI_INTERACTIVE. Period.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(always_state, /datum/ui_state/always_state, new) GLOBAL_DATUM_INIT(always_state, /datum/ui_state/always_state, new)
+3
View File
@@ -2,6 +2,9 @@
* tgui state: conscious_state * tgui state: conscious_state
* *
* Only checks if the user is conscious. * Only checks if the user is conscious.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(conscious_state, /datum/ui_state/conscious_state, new) GLOBAL_DATUM_INIT(conscious_state, /datum/ui_state/conscious_state, new)
+3
View File
@@ -2,6 +2,9 @@
* tgui state: contained_state * tgui state: contained_state
* *
* Checks that the user is inside the src_object. * Checks that the user is inside the src_object.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(contained_state, /datum/ui_state/contained_state, new) GLOBAL_DATUM_INIT(contained_state, /datum/ui_state/contained_state, new)
+5 -1
View File
@@ -1,7 +1,11 @@
/** /**
* tgui state: deep_inventory_state * tgui state: deep_inventory_state
* *
* Checks that the src_object is in the user's deep (backpack, box, toolbox, etc) inventory. * Checks that the src_object is in the user's deep
* (backpack, box, toolbox, etc) inventory.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(deep_inventory_state, /datum/ui_state/deep_inventory_state, new) GLOBAL_DATUM_INIT(deep_inventory_state, /datum/ui_state/deep_inventory_state, new)
+5 -1
View File
@@ -1,7 +1,11 @@
/** /**
* tgui state: default_state * tgui state: default_state
* *
* Checks a number of things -- mostly physical distance for humans and view for robots. * Checks a number of things -- mostly physical distance for humans
* and view for robots.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new) GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new)
@@ -1,13 +0,0 @@
/**
* tgui state: default_contained
*
* Basically default and contained combined, allowing for both
*/
GLOBAL_DATUM_INIT(default_contained_state, /datum/ui_state/default/contained, new)
/datum/ui_state/default/contained/can_use_topic(atom/src_object, mob/user)
if(src_object.contains(user))
return UI_INTERACTIVE
return ..()
+5 -2
View File
@@ -2,6 +2,9 @@
* tgui state: hands_state * tgui state: hands_state
* *
* Checks that the src_object is in the user's hands. * Checks that the src_object is in the user's hands.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new) GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new)
@@ -19,7 +22,7 @@ GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new)
return UI_INTERACTIVE return UI_INTERACTIVE
return UI_CLOSE return UI_CLOSE
/mob/living/silicon/robot/hands_can_use_topic(obj/src_object) /mob/living/silicon/robot/hands_can_use_topic(src_object)
if(activated(src_object) || istype(src_object.loc, /obj/item/weapon/gripper)) if(activated(src_object))
return UI_INTERACTIVE return UI_INTERACTIVE
return UI_CLOSE return UI_CLOSE
@@ -3,6 +3,9 @@
* *
* In addition to default checks, only allows interaction for a * In addition to default checks, only allows interaction for a
* human adjacent user. * human adjacent user.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(human_adjacent_state, /datum/ui_state/human_adjacent_state, new) GLOBAL_DATUM_INIT(human_adjacent_state, /datum/ui_state/human_adjacent_state, new)
+5 -1
View File
@@ -1,7 +1,11 @@
/** /**
* tgui state: inventory_state * tgui state: inventory_state
* *
* Checks that the src_object is in the user's top-level (hand, ear, pocket, belt, etc) inventory. * Checks that the src_object is in the user's top-level
* (hand, ear, pocket, belt, etc) inventory.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(inventory_state, /datum/ui_state/inventory_state, new) GLOBAL_DATUM_INIT(inventory_state, /datum/ui_state/inventory_state, new)
@@ -1,5 +1,8 @@
/** /**
* tgui state: language_menu_state * tgui state: language_menu_state
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(language_menu_state, /datum/ui_state/language_menu, new) GLOBAL_DATUM_INIT(language_menu_state, /datum/ui_state/language_menu, new)
@@ -2,6 +2,9 @@
* tgui state: not_incapacitated_state * tgui state: not_incapacitated_state
* *
* Checks that the user isn't incapacitated * Checks that the user isn't incapacitated
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(not_incapacitated_state, /datum/ui_state/not_incapacitated_state, new) GLOBAL_DATUM_INIT(not_incapacitated_state, /datum/ui_state/not_incapacitated_state, new)
@@ -24,6 +27,10 @@ GLOBAL_DATUM_INIT(not_incapacitated_turf_state, /datum/ui_state/not_incapacitate
/datum/ui_state/not_incapacitated_state/can_use_topic(src_object, mob/user) /datum/ui_state/not_incapacitated_state/can_use_topic(src_object, mob/user)
if(user.stat) if(user.stat)
return UI_CLOSE return UI_CLOSE
if(user.incapacitated() || user.lying || (turf_check && !isturf(user.loc))) if(user.incapacitated() || (turf_check && !isturf(user.loc)))
return UI_DISABLED return UI_DISABLED
return UI_INTERACTIVE if(isliving(user))
var/mob/living/L = user
if(!(L.mobility_flags & MOBILITY_STAND))
return UI_DISABLED
return UI_INTERACTIVE
+5 -1
View File
@@ -1,7 +1,11 @@
/** /**
* tgui state: notcontained_state * tgui state: notcontained_state
* *
* Checks that the user is not inside src_object, and then makes the default checks. * Checks that the user is not inside src_object, and then makes the
* default checks.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(notcontained_state, /datum/ui_state/notcontained_state, new) GLOBAL_DATUM_INIT(notcontained_state, /datum/ui_state/notcontained_state, new)
+3
View File
@@ -2,6 +2,9 @@
* tgui state: observer_state * tgui state: observer_state
* *
* Checks that the user is an observer/ghost. * Checks that the user is an observer/ghost.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(observer_state, /datum/ui_state/observer_state, new) GLOBAL_DATUM_INIT(observer_state, /datum/ui_state/observer_state, new)
+3
View File
@@ -2,6 +2,9 @@
* tgui state: physical_state * tgui state: physical_state
* *
* Short-circuits the default state to only check physical distance. * Short-circuits the default state to only check physical distance.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(physical_state, /datum/ui_state/physical, new) GLOBAL_DATUM_INIT(physical_state, /datum/ui_state/physical, new)
+3
View File
@@ -2,6 +2,9 @@
* tgui state: self_state * tgui state: self_state
* *
* Only checks that the user and src_object are the same. * Only checks that the user and src_object are the same.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(self_state, /datum/ui_state/self_state, new) GLOBAL_DATUM_INIT(self_state, /datum/ui_state/self_state, new)
+3
View File
@@ -2,6 +2,9 @@
* tgui state: z_state * tgui state: z_state
* *
* Only checks that the Z-level of the user and src_object are the same. * Only checks that the Z-level of the user and src_object are the same.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/ */
GLOBAL_DATUM_INIT(z_state, /datum/ui_state/z_state, new) GLOBAL_DATUM_INIT(z_state, /datum/ui_state/z_state, new)
-247
View File
@@ -1,247 +0,0 @@
/**
* tgui subsystem
*
* Contains all tgui state and subsystem 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/subsystem/tgui/proc/try_update_ui(mob/user, datum/src_object, ui_key, datum/tgui/ui, force_open = FALSE)
if(isnull(ui)) // No UI was passed, so look for one.
ui = get_open_ui(user, src_object, ui_key)
if(!isnull(ui))
var/data = src_object.ui_data(user) // Get data from the src_object.
if(!force_open) // UI is already open; update it.
ui.push_data(data)
else // Re-open it anyways.
ui.reinitialize(null, data)
return ui // We found the UI, return it.
else
return null // We couldn't find a UI.
/**
* 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/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object, ui_key)
var/src_object_key = "[REF(src_object)]"
if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list))
return null // No UIs open.
else if(isnull(open_uis[src_object_key][ui_key]) || !istype(open_uis[src_object_key][ui_key], /list))
return null // No UIs open for this object.
for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) // Find UIs for this object.
if(ui.user == user) // Make sure we have the right user
return ui
return null // Couldn't find a UI!
/**
* 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/subsystem/tgui/proc/update_uis(datum/src_object)
var/src_object_key = "[REF(src_object)]"
if(isnull(open_uis[src_object_key]) || !istype(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 open_uis[src_object_key])
for(var/datum/tgui/ui in open_uis[src_object_key][ui_key])
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid.
ui.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/subsystem/tgui/proc/close_uis(datum/src_object)
var/src_object_key = "[REF(src_object)]"
if(isnull(open_uis[src_object_key]) || !istype(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 open_uis[src_object_key])
for(var/datum/tgui/ui in open_uis[src_object_key][ui_key])
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid.
ui.close() // Close the UI.
close_count++ // Count each UI we close.
return close_count
/**
* private
*
* Close *ALL* UIs
*
* return int The number of UIs closed.
*/
/datum/controller/subsystem/tgui/proc/close_all_uis()
var/close_count = 0
for(var/src_object_key in open_uis)
for(var/ui_key in open_uis[src_object_key])
for(var/datum/tgui/ui in open_uis[src_object_key][ui_key])
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid.
ui.close() // Close the UI.
close_count++ // Count each UI we close.
return close_count
/**
* 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/subsystem/tgui/proc/update_user_uis(mob/user, datum/src_object = null, ui_key = null)
if(isnull(user.open_uis) || !istype(user.open_uis, /list) || open_uis.len == 0)
return 0 // Couldn't find any UIs for this user.
var/update_count = 0
for(var/datum/tgui/ui in user.open_uis)
if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key))
ui.process(force = 1) // Update the UI.
update_count++ // Count each UI we upadte.
return update_count
/**
* 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/subsystem/tgui/proc/close_user_uis(mob/user, datum/src_object = null, ui_key = null)
if(isnull(user.open_uis) || !istype(user.open_uis, /list) || open_uis.len == 0)
return 0 // Couldn't find any UIs for this user.
var/close_count = 0
for(var/datum/tgui/ui in user.open_uis)
if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key))
ui.close() // Close the UI.
close_count++ // Count each UI we close.
return close_count
/**
* private
*
* Add a UI to the list of open UIs.
*
* required ui datum/tgui The UI to be added.
*/
/datum/controller/subsystem/tgui/proc/on_open(datum/tgui/ui)
var/src_object_key = "[REF(ui.src_object)]"
if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list))
open_uis[src_object_key] = list(ui.ui_key = list()) // Make a list for the ui_key and src_object.
else if(isnull(open_uis[src_object_key][ui.ui_key]) || !istype(open_uis[src_object_key][ui.ui_key], /list))
open_uis[src_object_key][ui.ui_key] = list() // Make a list for the ui_key.
// Append the UI to all the lists.
ui.user.open_uis |= ui
var/list/uis = open_uis[src_object_key][ui.ui_key]
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/subsystem/tgui/proc/on_close(datum/tgui/ui)
var/src_object_key = "[REF(ui.src_object)]"
if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list))
return 0 // It wasn't open.
else if(isnull(open_uis[src_object_key][ui.ui_key]) || !istype(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.open_uis.Remove(ui)
var/Ukey = ui.ui_key
var/list/uis = open_uis[src_object_key][Ukey] // Remove it from the list of open UIs.
uis.Remove(ui)
if(!uis.len)
var/list/uiobj = open_uis[src_object_key]
uiobj.Remove(Ukey)
if(!uiobj.len)
open_uis.Remove(src_object_key)
return 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/subsystem/tgui/proc/on_logout(mob/user)
return close_user_uis(user)
/**
* private
*
* Handle clients switching mobs, by transferring 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/subsystem/tgui/proc/on_transfer(mob/source, mob/target)
if(!source || isnull(source.open_uis) || !istype(source.open_uis, /list) || open_uis.len == 0)
return 0 // The old mob had no open UIs.
if(isnull(target.open_uis) || !istype(target.open_uis, /list))
target.open_uis = list() // Create a list for the new mob if needed.
for(var/datum/tgui/ui in source.open_uis)
ui.user = target // Inform the UIs of their new owner.
target.open_uis.Add(ui) // Transfer all the UIs.
source.open_uis.Cut() // Clear the old list.
return 1 // Let the caller know we did it.
+191 -259
View File
@@ -1,7 +1,6 @@
/** /**
* tgui * Copyright (c) 2020 Aleksej Komarov
* * SPDX-License-Identifier: MIT
* /tg/station user interface library
*/ */
/** /**
@@ -14,34 +13,26 @@
var/datum/src_object var/datum/src_object
/// The title of te UI. /// The title of te UI.
var/title var/title
/// The ui_key of the UI. This allows multiple UIs for one src_object.
var/ui_key
/// The window_id for browse() and onclose(). /// The window_id for browse() and onclose().
var/window_id var/datum/tgui_window/window
/// The window width. /// Key that is used for remembering the window geometry.
var/width = 0 var/window_key
/// The window height /// Deprecated: Window size.
var/height = 0 var/window_size
/// The interface (template) to be used for this UI. /// The interface (template) to be used for this UI.
var/interface var/interface
/// Update the UI every MC tick. /// Update the UI every MC tick.
var/autoupdate = TRUE var/autoupdate = TRUE
/// If the UI has been initialized yet. /// If the UI has been initialized yet.
var/initialized = FALSE var/initialized = FALSE
/// The data (and datastructure) used to initialize the UI. /// Time of opening the window.
var/list/initial_data var/opened_at
/// The static data used to initialize the UI. /// Stops further updates when close() was called.
var/list/initial_static_data var/closing = FALSE
/// Holder for the json string, that is sent during the initial update
var/_initial_update
/// The status/visibility of the UI. /// The status/visibility of the UI.
var/status = UI_INTERACTIVE var/status = UI_INTERACTIVE
/// Topic state used to determine status/interactability. /// Topic state used to determine status/interactability.
var/datum/ui_state/state = null var/datum/ui_state/state = null
/// The parent UI.
var/datum/tgui/master_ui
/// Children of this UI.
var/list/datum/tgui/children = list()
/** /**
* public * public
@@ -50,38 +41,25 @@
* *
* required user mob The mob who opened/is using the 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 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. * required interface string The interface used to render the UI.
* optional title string The title of the UI. * optional title string The title of the UI.
* optional width int The window width. * optional ui_x int Deprecated: Window width.
* optional height int The window height. * optional ui_y int Deprecated: 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. * 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 = GLOB.default_state) /datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y)
log_tgui(user, "new [interface] fancy [user.client.prefs.tgui_fancy]")
src.user = user src.user = user
src.src_object = src_object src.src_object = src_object
src.ui_key = ui_key src.window_key = "[REF(src_object)]-main"
// DO NOT replace with \ref here. src_object could potentially be tagged
src.window_id = "[REF(src_object)]-[ui_key]"
src.interface = interface src.interface = interface
if(title) if(title)
src.title = sanitize(title) src.title = title
if(width) src.state = src_object.ui_state()
src.width = width // Deprecated
if(height) if(ui_x && ui_y)
src.height = height src.window_size = list(ui_x, ui_y)
src.master_ui = master_ui
if(master_ui)
master_ui.children += src
src.state = state
var/datum/asset/assets = get_asset_datum(/datum/asset/group/tgui)
assets.send(user)
/** /**
* public * public
@@ -90,85 +68,53 @@
*/ */
/datum/tgui/proc/open() /datum/tgui/proc/open()
if(!user.client) if(!user.client)
return // Bail if there is no client. return null
if(window)
update_status(push = FALSE) // Update the window status. return null
process_status()
if(status < UI_UPDATE) if(status < UI_UPDATE)
return // Bail if we're not supposed to open. return null
window = SStgui.request_pooled_window(user)
// Build window options if(!window)
var/window_options = "can_minimize=0;auto_format=0;" return null
// If we have a width and height, use them. opened_at = world.time
if(width && height) window.acquire_lock(src)
window_options += "size=[width]x[height];" if(!window.is_ready())
// Remove titlebar and resize handles for a fancy window window.initialize(inline_assets = list(
if(user.client.prefs.tgui_fancy) get_asset_datum(/datum/asset/simple/tgui),
window_options += "titlebar=0;can_resize=0;" ))
else else
window_options += "titlebar=1;can_resize=1;" window.send_message("ping")
window.send_asset(get_asset_datum(/datum/asset/simple/fontawesome))
// Generate page html for(var/datum/asset/asset in src_object.ui_assets(user))
var/html window.send_asset(asset)
html = SStgui.basehtml window.send_message("update", get_payload(
// Allow the src object to override the html if needed with_data = TRUE,
html = src_object.ui_base_html(html) with_static_data = TRUE))
// Replace template tokens with important UI data
// NOTE: Intentional \ref usage; tgui datums can't/shouldn't
// be tagged, so this is an effective unwrap
html = replacetextEx(html, "\[tgui:ref]", "\ref[src]")
// Open the window.
user << browse(html, "window=[window_id];[window_options]")
// Instruct the client to signal UI when the window is closed.
// NOTE: Intentional \ref usage; tgui datums can't/shouldn't
// be tagged, so this is an effective unwrap
winset(user, window_id, "on-close=\"uiclose \ref[src]\"")
// Pre-fetch initial state while browser is still loading in
// another thread
if(!initial_data) {
initial_data = src_object.ui_data(user)
}
if(!initial_static_data) {
initial_static_data = src_object.ui_static_data(user)
}
_initial_update = url_encode(get_json(initial_data, initial_static_data))
SStgui.on_open(src) SStgui.on_open(src)
/** /**
* public * public
* *
* Reinitialize the UI. * Close the UI.
* (Possibly with a new interface and/or data).
* *
* optional template string The name of the new interface. * optional can_be_suspended bool
* optional data list The new initial data.
*/ */
/datum/tgui/proc/reinitialize(interface, list/data, list/static_data) /datum/tgui/proc/close(can_be_suspended = TRUE)
if(interface) if(closing)
src.interface = interface return
if(data) closing = TRUE
initial_data = data // If we don't have window_id, open proc did not have the opportunity
if(static_data) // to finish, therefore it's safe to skip this whole block.
initial_static_data = static_data if(window)
open() // Windows you want to keep are usually blue screens of death
// and we want to keep them around, to allow user to read
/** // the error message properly.
* public window.release_lock()
* window.close(can_be_suspended)
* Close the UI, and all its children. src_object.ui_close(user)
*/ SStgui.on_close(src)
/datum/tgui/proc/close()
user << browse(null, "window=[window_id]") // Close the window.
src_object.ui_close(user)
SStgui.on_close(src)
for(var/datum/tgui/child in children) // Loop through and close all children.
child.close()
children.Cut()
state = null state = null
master_ui = null
qdel(src) qdel(src)
/** /**
@@ -176,187 +122,173 @@
* *
* Enable/disable auto-updating of the UI. * Enable/disable auto-updating of the UI.
* *
* required state bool Enable/disable auto-updating. * required value bool Enable/disable auto-updating.
*/ */
/datum/tgui/proc/set_autoupdate(state = TRUE) /datum/tgui/proc/set_autoupdate(autoupdate)
autoupdate = state src.autoupdate = autoupdate
/**
* public
*
* Replace current ui.state with a new one.
*
* required state datum/ui_state/state Next state
*/
/datum/tgui/proc/set_state(datum/ui_state/state)
src.state = state
/**
* public
*
* Makes an asset available to use in tgui.
*
* required asset datum/asset
*/
/datum/tgui/proc/send_asset(datum/asset/asset)
if(!window)
CRASH("send_asset() can only be called after open().")
window.send_asset(asset)
/**
* public
*
* Send a full update to the client (includes static data).
*
* optional custom_data list Custom data to send instead of ui_data.
* optional force bool Send an update even if UI is not interactive.
*/
/datum/tgui/proc/send_full_update(custom_data, force)
if(!user.client || !initialized || closing)
return
var/should_update_data = force || status >= UI_UPDATE
window.send_message("update", get_payload(
custom_data,
with_data = should_update_data,
with_static_data = TRUE))
/**
* public
*
* Send a partial update to the client (excludes static data).
*
* optional custom_data list Custom data to send instead of ui_data.
* optional force bool Send an update even if UI is not interactive.
*/
/datum/tgui/proc/send_update(custom_data, force)
if(!user.client || !initialized || closing)
return
var/should_update_data = force || status >= UI_UPDATE
window.send_message("update", get_payload(
custom_data,
with_data = should_update_data))
/** /**
* private * private
* *
* Package the data to send to the UI, as JSON. * Package the data to send to the UI, as JSON.
* This includes the UI data and config_data.
* *
* return string The packaged JSON. * return list
*/ */
/datum/tgui/proc/get_json(list/data, list/static_data) /datum/tgui/proc/get_payload(custom_data, with_data, with_static_data)
var/list/json_data = list() var/list/json_data = list()
json_data["config"] = list( json_data["config"] = list(
"title" = title, "title" = title,
"status" = status, "status" = status,
"interface" = interface, "interface" = interface,
"fancy" = user.client.prefs.tgui_fancy, "window" = list(
"locked" = user.client.prefs.tgui_lock, "key" = window_key,
"observer" = isobserver(user), "size" = window_size,
"window" = window_id, "fancy" = user.client.prefs.tgui_fancy,
// NOTE: Intentional \ref usage; tgui datums can't/shouldn't "locked" = user.client.prefs.tgui_lock,
// be tagged, so this is an effective unwrap ),
"ref" = "\ref[src]" "user" = list(
"name" = "[user]",
"ckey" = "[user.ckey]",
"observer" = isobserver(user),
),
) )
var/data = custom_data || with_data && src_object.ui_data(user)
if(!isnull(data)) if(data)
json_data["data"] = data json_data["data"] = data
if(!isnull(static_data)) var/static_data = with_static_data && src_object.ui_static_data(user)
if(static_data)
json_data["static_data"] = static_data json_data["static_data"] = static_data
// Send shared states
if(src_object.tgui_shared_states) if(src_object.tgui_shared_states)
json_data["shared"] = src_object.tgui_shared_states json_data["shared"] = src_object.tgui_shared_states
return json_data
// Generate the JSON.
var/json = json_encode(json_data)
// Strip #255/improper.
json = replacetext(json, "\proper", "")
json = replacetext(json, "\improper", "")
return json
/** /**
* private * private
* *
* Handle clicks from the UI. * Run an update cycle for this UI. Called internally by SStgui
* Call the src_object's ui_act() if status is UI_INTERACTIVE. * every second or so.
* 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(_initial_update, "[window_id].browser:update")
initialized = TRUE
if("tgui:setSharedState")
// Update the window state.
update_status(push = FALSE)
// Bail if UI is not interactive or usr calling Topic
// is not the UI user.
if(status != UI_INTERACTIVE)
return
var/key = params["key"]
var/value = params["value"]
if(!src_object.tgui_shared_states)
src_object.tgui_shared_states = list()
src_object.tgui_shared_states[key] = value
SStgui.update_uis(src_object)
if("tgui:setFancy")
var/value = text2num(params["value"])
user.client.prefs.tgui_fancy = value
if("tgui:log")
// Force window to show frills on fatal errors
if(params["fatal"])
winset(user, window_id, "titlebar=1;can-resize=1;size=600x600")
log_message(params["log"])
if("tgui:link")
user << link(params["url"])
else
// Update the window state.
update_status(push = FALSE)
// Call ui_act() on the src_object.
if(src_object.ui_act(action, params, src, state))
// Update if the object requested it.
SStgui.update_uis(src_object)
/**
* 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/process(force = FALSE) /datum/tgui/process(force = FALSE)
if(closing)
return
var/datum/host = src_object.ui_host(user) var/datum/host = src_object.ui_host(user)
if(!src_object || !host || !user) // If the object or user died (or something else), abort. // If the object or user died (or something else), abort.
if(!src_object || !host || !user || !window)
close(can_be_suspended = FALSE)
return
// Validate ping
if(!initialized && world.time - opened_at > TGUI_PING_TIMEOUT)
log_tgui(user, \
"Error: Zombie window detected, killing it with fire.\n" \
+ "window_id: [window.id]\n" \
+ "opened_at: [opened_at]\n" \
+ "world.time: [world.time]")
close(can_be_suspended = FALSE)
return
// Update through a normal call to ui_interact
if(status != UI_DISABLED && (autoupdate || force))
src_object.ui_interact(user, src)
return
// Update status only
var/needs_update = process_status()
if(status <= UI_CLOSE)
close() close()
return return
if(needs_update)
if(status && (force || autoupdate)) window.send_message("update", get_payload())
update() // Update the UI if the status and update settings allow it.
else
update_status(push = TRUE) // Otherwise only update status.
/** /**
* private * private
* *
* Push data to an already open UI. * Updates the status, and returns TRUE if status has changed.
*
* 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, static_data, force = FALSE) /datum/tgui/proc/process_status()
// Update the window state. var/prev_status = status
update_status(push = FALSE) status = src_object.ui_status(user, state)
// Cannot update UI if it is not set up yet. return prev_status != status
if(!initialized)
return
// Cannot update UI, we have no visibility.
if(status <= UI_DISABLED && !force)
return
// Send the new JSON to the update() Javascript function.
user << output(
url_encode(get_json(data, static_data)),
"[window_id].browser:update")
/** /**
* private * private
* *
* Updates the UI by interacting with the src_object again, which will hopefully * Callback for handling incoming tgui messages.
* 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 = FALSE) /datum/tgui/proc/on_message(type, list/payload, list/href_list)
src_object.ui_interact(user, ui_key, src, force_open, master_ui, state) // Pass act type messages to ui_act
if(type && copytext(type, 1, 5) == "act/")
/** process_status()
* private if(src_object.ui_act(copytext(type, 5), payload, src, state))
* SStgui.update_uis(src_object)
* Update the status/visibility of the UI for its user. return FALSE
* switch(type)
* optional push bool Push an update to the UI (an update is always sent for UI_DISABLED). if("ready")
*/ initialized = TRUE
/datum/tgui/proc/update_status(push = FALSE) if("pingReply")
var/status = src_object.ui_status(user, state) initialized = TRUE
if(master_ui) if("suspend")
status = min(status, master_ui.status) close(can_be_suspended = TRUE)
set_status(status, push) if("close")
if(status == UI_CLOSE) close(can_be_suspended = FALSE)
close() if("log")
if(href_list["fatal"])
/** close(can_be_suspended = FALSE)
* private if("setSharedState")
* if(status != UI_INTERACTIVE)
* Set the status/visibility of the UI. return
* LAZYINITLIST(src_object.tgui_shared_states)
* required status int The status to set (UI_CLOSE/UI_DISABLED/UI_UPDATE/UI_INTERACTIVE). src_object.tgui_shared_states[href_list["key"]] = href_list["value"]
* optional push bool Push an update to the UI (an update is always sent for UI_DISABLED). SStgui.update_uis(src_object)
*/
/datum/tgui/proc/set_status(status, push = FALSE)
// Only update if status has changed.
if(src.status != status)
if(src.status == UI_DISABLED)
src.status = status
if(push)
update()
else
src.status = status
// Update if the UI just because disabled, or a push is requested.
if(status == UI_DISABLED || push)
push_data(null, force = TRUE)
/datum/tgui/proc/log_message(message)
log_tgui("[user] ([user.ckey]) using \"[title]\":\n[message]")
+238
View File
@@ -0,0 +1,238 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/datum/tgui_window
var/id
var/client/client
var/pooled
var/pool_index
var/status = TGUI_WINDOW_CLOSED
var/locked = FALSE
var/datum/tgui/locked_by
var/fatally_errored = FALSE
var/message_queue
var/sent_assets = list()
/**
* public
*
* Create a new tgui window.
*
* required client /client
* required id string A unique window identifier.
*/
/datum/tgui_window/New(client/client, id, pooled = FALSE)
src.id = id
src.client = client
src.pooled = pooled
if(pooled)
client.tgui_windows[id] = src
src.pool_index = TGUI_WINDOW_INDEX(id)
/**
* public
*
* Initializes the window with a fresh page. Puts window into the "loading"
* state. You can begin sending messages right after initializing. Messages
* will be put into the queue until the window finishes loading.
*
* optional inline_assets list List of assets to inline into the html.
*/
/datum/tgui_window/proc/initialize(inline_assets = list())
log_tgui(client, "[id]/initialize")
if(!client)
return
status = TGUI_WINDOW_LOADING
fatally_errored = FALSE
message_queue = null
// Build window options
var/options = "file=[id].html;can_minimize=0;auto_format=0;"
// Remove titlebar and resize handles for a fancy window
if(client.prefs.tgui_fancy)
options += "titlebar=0;can_resize=0;"
else
options += "titlebar=1;can_resize=1;"
// Generate page html
var/html = SStgui.basehtml
html = replacetextEx(html, "\[tgui:windowId]", id)
// Process inline assets
var/inline_styles = ""
var/inline_scripts = ""
for(var/datum/asset/asset in inline_assets)
var/mappings = asset.get_url_mappings()
for(var/name in mappings)
var/url = mappings[name]
// Not urlencoding since asset strings are considered safe
if(copytext(name, -4) == ".css")
inline_styles += "<link rel=\"stylesheet\" type=\"text/css\" href=\"[url]\">\n"
else if(copytext(name, -3) == ".js")
inline_scripts += "<script type=\"text/javascript\" defer src=\"[url]\"></script>\n"
asset.send()
html = replacetextEx(html, "<!-- tgui:styles -->\n", inline_styles)
html = replacetextEx(html, "<!-- tgui:scripts -->\n", inline_scripts)
// Open the window
client << browse(html, "window=[id];[options]")
// Instruct the client to signal UI when the window is closed.
winset(client, id, "on-close=\"uiclose [id]\"")
/**
* public
*
* Checks if the window is ready to receive data.
*
* return bool
*/
/datum/tgui_window/proc/is_ready()
return status == TGUI_WINDOW_READY
/**
* public
*
* Checks if the window can be sanely suspended.
*
* return bool
*/
/datum/tgui_window/proc/can_be_suspended()
return !fatally_errored \
&& pooled \
&& pool_index > 0 \
&& pool_index <= TGUI_WINDOW_SOFT_LIMIT \
&& status == TGUI_WINDOW_READY
/**
* public
*
* Acquire the window lock. Pool will not be able to provide this window
* to other UIs for the duration of the lock.
*
* Can be given an optional tgui datum, which will hook its on_message
* callback into the message stream.
*
* optional ui /datum/tgui
*/
/datum/tgui_window/proc/acquire_lock(datum/tgui/ui)
locked = TRUE
locked_by = ui
/**
* Release the window lock.
*/
/datum/tgui_window/proc/release_lock()
// Clean up assets sent by tgui datum which requested the lock
if(locked)
sent_assets = list()
locked = FALSE
locked_by = null
/**
* public
*
* Close the UI.
*
* optional can_be_suspended bool
*/
/datum/tgui_window/proc/close(can_be_suspended = TRUE)
if(!client)
return
if(can_be_suspended && can_be_suspended())
log_tgui(client, "[id]/close: suspending")
status = TGUI_WINDOW_READY
send_message("suspend")
return
log_tgui(client, "[id]/close")
release_lock()
status = TGUI_WINDOW_CLOSED
message_queue = null
// Do not close the window to give user some time
// to read the error message.
if(!fatally_errored)
client << browse(null, "window=[id]")
/**
* public
*
* Sends a message to tgui window.
*
* required type string Message type
* required payload list Message payload
* optional force bool Send regardless of the ready status.
*/
/datum/tgui_window/proc/send_message(type, list/payload, force)
if(!client)
return
var/message = json_encode(list(
"type" = type,
"payload" = payload,
))
// Strip #255/improper.
message = replacetext(message, "\proper", "")
message = replacetext(message, "\improper", "")
// Pack for sending via output()
message = url_encode(message)
// Place into queue if window is still loading
if(!force && status != TGUI_WINDOW_READY)
if(!message_queue)
message_queue = list()
message_queue += list(message)
return
client << output(message, "[id].browser:update")
/**
* public
*
* Makes an asset available to use in tgui.
*
* required asset datum/asset
*/
/datum/tgui_window/proc/send_asset(datum/asset/asset)
if(!client || !asset)
return
if(istype(asset, /datum/asset/spritesheet))
var/datum/asset/spritesheet/spritesheet = asset
send_message("asset/stylesheet", spritesheet.css_filename())
send_message("asset/mappings", asset.get_url_mappings())
sent_assets += list(asset)
asset.send(client)
/**
* private
*
* Sends queued messages if the queue wasn't empty.
*/
/datum/tgui_window/proc/flush_message_queue()
if(!client || !message_queue)
return
for(var/message in message_queue)
client << output(message, "[id].browser:update")
message_queue = null
/**
* private
*
* Callback for handling incoming tgui messages.
*/
/datum/tgui_window/proc/on_message(type, list/payload, list/href_list)
switch(type)
if("ready")
// Status can be READY if user has refreshed the window.
if(status == TGUI_WINDOW_READY)
// Resend the assets
for(var/asset in sent_assets)
send_asset(asset)
status = TGUI_WINDOW_READY
if("log")
if(href_list["fatal"])
fatally_errored = TRUE
// Pass message to UI that requested the lock
if(locked && locked_by)
locked_by.on_message(type, payload, href_list)
flush_message_queue()
return
// If not locked, handle these message types
switch(type)
if("suspend")
close(can_be_suspended = TRUE)
if("close")
close(can_be_suspended = FALSE)