TGUIv4 - Core backend updates, TGchat, Asset cache updates (#23726)

* TGUI upgrade project: Port initial TGUI 4.0 changes from TG (#23440)

* tgui4.0

* bugfix for un-interaction

* fix for input not resetting on close

* NTOS restore

* fix all interfaces having scrollbars, fix colours

* bundle update

* dep bumps

* bumps the deps as much as possible

* button regression fix

* TGUI test map rev 1

* fix theme and some component regression

* fix login screen regression

* fixes regression with uplink cart

* bundle

* fix regressions

* fix the input issue, again

* regression fixes, stylesheet edition, hash restore

* fixes GPS BSOD

* draggable control regression

* dev server dep regression

* byondUI regression fix

* section regression fix

* fix secure storage weirdness

* Fixed mining vendor scrolling

* Arthri review pass 1

* mining vendor double-scroll

* fix for RPD, AirAlarm, and Radio

* arthri review pass 2

* arthri review pass 3

* sanitize var setting, fix colours, delete unused file

* adds CI to branch

* god damn AI making random changes

* I should remember to compile when changes are made

* fixes scrollbar issues

* fix camera console, fix flexGrow for sections

* CI fix

* nanomap fix

* Update code/modules/atmospherics/machinery/airalarm.dm

* restore margin

* style semicolon

* TGUIv4 - Moves TGUI Window Sizing from DM to JS (#23524)

* Initial commit

* Transferring the remaining windows to JS

* Resizing part 1

* Resizing part 2

* Some reverts and polish

* Forgot that

* undeployed nuke window size

---------

Co-authored-by: Aylong <alexanderkitsa@gmail.com>

* TGUIv4: Remove force_open (#23537)

* First state fixes

* Removes "force_open"

* TGUIv4 - TGUI Version 4.1 (#23547)

* Reduced Logging Changes

* setSharedState early return for non-interactive UI

* TGUI version bump to 4.1

* Fix sending updates for non-interactive UIs

* Scalablue UI Stylesheet Changes

* CSS Tgui Bundle

* JavaScript TGUI Style Changes

* Update tgui/packages/tgui/styles/base.scss

Co-authored-by: Aylong <69762909+Aylong220@users.noreply.github.com>

* Additional changes to Input and Button scalability

* TGUI logging proc update

* Indentation fix for logging proc

* Update code/__HELPERS/_logging.dm

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* Additional log_tgui changes

---------

Co-authored-by: Aylong <69762909+Aylong220@users.noreply.github.com>
Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* dev server hotfix (#23584)

* ui_interact object constructor argument check (#23594)

* feat: add smart asset cache, add CDN support for assets, properly group assets (#23585)

* feat: add smart asset cache, add CDN support for assets, properly group assets

* fix: make tgui actually work

* fix: keeping local name for `tgui.bundle.js` and `tgui.bundle.css` for debug and dev server functioning

* fix: make `fontawesome` assets finally work

* fix: make proper identation for `if`

* fix: add `resolveAsset` to `NanoMap.js`

* refactor: update `claw_game` and `chess` to new asset framework

* refactor/tweak: don't use string concatenation for browser `content` and `head_content`, use list instead. Don't use `common.css` for paper UI, keep local name for stamp image assets, replace hard ref of browser to `atom` (rare case) to `UID`

* refactor: remove redundant debug logs

* refactor: remove space betwee `if` and `(`

* refactor: remove one more redundant log, properly reload UI resources

* rafactor: change names of asset files

* fix: adjust existing UIs to properly use assets

* fix: properly pass args to to `onclose` proc

* Update code/modules/asset_cache/transports/asset_transport.dm

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* Update code/modules/asset_cache/transports/asset_transport.dm

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* TGUI 4.2: TGchat + Refactor Asset Delivery + Yarn Berry (#23643)

* tgchat

* Little cleanup

* Refactor Asset Delivery (Part 2)

* Little tweaks

* fix code styling issues

* fix file name duplications

* Browser window options fix @gaxeer

* transfer valve fixes

* yarn berry initial

* vsc tasks

* yarn berry working

* node 20

* fix dev server (lol wrong yarn)

* bloody regressions

* fixes that damn scrolltracking bug

* Some tweaks and flexGrow deletions

It still doesn't work as it should, which makes me nervous

* remove unneeded deps

* 514 regression fix

* change stuff to not conflict with other servers

* name it as requested

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* revert test_TGUI map addition

* TGUI 4.3: Stack, Webpack 5 & UI Tweaks, Yarn 3.6.4, TS support, Jest (#23677)

* Stack & UI's Refactor

Almost final, polish required

Stack & UI's Refactor (Part 1)

Stack & UI's Refactor (Part 2)

Stack & UI's Refactor (Part 3)

* Prettier

Maybe bad

* ClearChat & some tweaks

* Adds a Chat Reliability Layer https://github.com/tgstation/tgstation/pull/79479

* Fix chat BSOD

https://github.com/tgstation/tgstation/pull/79821

* WebPack 5 (Didn't work)

I hate this shit

* Yarn 3.6.4

* make it all work

* revert snowflake fix

* Stories

* adds TS support

* re-enable test and prettier

* update yarn sdk's

* Fix some box regression

* ping/reply

* Fixes regressions and some things

* Zebra and Fix chat button transfer

+rebuild

* make VSC use the proper local typescript lib

* Popper Tooltips https://github.com/tgstation/tgstation/pull/58980

* Popper Tooltips performancy fixes

* Dropdown v2 https://github.com/tgstation/tgstation/pull/75164

Without Icon.tsx

* BB test map

* run build

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* we moved these

* change test values

* ignore scss files

* change hash function

* TGUIv4: TGchat fix traitor codeword highlighting (#23720)

* Codeword highlighting for TGUIv4

* null safeties

* Apply suggestions from code review

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

---------

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* TGUIv4 - updates standalone build tooling (#23721)

* swap `xxhash64` for `md4`

* remove hash function

* remove old file

* TGUI 4.3+4.4: Newscaster fix, `inline-block` replacements and other fixes (#23722)

* TGUI 4.3 fixes

Newscaster fix + inline-blocks

* Be gone Box

* Mod Chat -> Mentor Chat

* Some warning filters

* Rebuild

* Delete tgui-common.bundle.js

* please work

* try it like this

* 4.3.1

* 4.3.2

* fix

* i forgot to build

* 4.4.0

* give me more diff details please so I can fix you

* its not binary pls git

* test without svgs

* removes an un-needed SVG file

* inlined SVGs test

* fix code styling

* comment these

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* TGUIv4 - Removes resizable and improves drag code (#23719)

* Initial commit

* bundle rebuild

* Fixes Radio Sizing

* Updates Radio sizing to fix wrapping issues

* fix dev server

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* fix hotkeys not being passed to BYOND

* Apply suggestions from code review

* bump deps

* forgot these deps

* ci fix

* ChemDispenser fixes and HoloControl cooldown

* Station Traits TGUI

+Rebuild

* CI

* fix CI for real this time

* I have no idea

* fix station trait panel

* Update code/controllers/subsystem/SSping.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* Update code/controllers/subsystem/SSticker.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* Update code/controllers/subsystem/SSping.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>

* deterministic module IDs

* TGUI fixes. Again... (#23762)

* TGUI Fixes: Part 1

ORM icon for reinforced glass now showing correctly

Mining vendor moved to Stack

Exofab UI buttons descended from the heavens to the earth

* TGUI Fixes: Part 2

DestTagger now works, forgot import LabeledList

PowerMonitor graph displays correct

Request Console updated to 2.0, BUT, problem with message priority, it was there before the changes, maybe even before the project, I'll have to check it out

* TGUI Fixes: Part 2

Fixed `onEnter` input

Fixed Cloning Console storage (Im dumbass)

Some windows resizing

Fixed bad picture in security and medical records console

Fixed scrollable dropdown

Added translucent color for buttons

* Fucking uplink DONE, and Button.Input fix

* NT Recruiter and Uplink polish

* Del LabeledList from dest. tagger

* ChemMaster and OreRedemption log deletion

* Update test_tiny.dmm

* Translucent button story

* Filter for SQL error

* del: `log_debug` in `hear_say.dm`

* Some little fixes and SpecMenu Stack

* Panel.js: mx -> mr

* fix ghost spawners

* null check

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* fix material name in protolathe

* fix various UI interacts

* card machine runtime fix

* remove legacy folders

* TGUIv4 - TGchat Theme Adjustments (#23772)

* Initial commit

* Changes rule grouping

* Robot class style changes and webpack config

* Reverts webpack/font changes and subs robot font

* revert a state changed in error

* fix uninteractable UIs due to chunky fingers

* TGUIv4: More fixes, tweaks and fixes for tweaks (#23781)

* I fuckin hate PDA

* Vending

* Add `|` to highlight splitters and some light mode tweaks

* Teleporter rewrite

* APC and AiAirlock (God forgive me)

* clarify highlight syntax

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* TGUIv4 - Abductor Experiment Machine TGUI (#23776)

* Experiment Console TGUI

* Update for TGUIv4

* UI Size adjustment

* ui state and ghost attack update

* remove this

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

* fix

* TGUIv4 - Adjustments, Fixes, and Tweaks (#23785)

* Photocopier layout adjustments

* Autolathe Text and Button spacing adjustment

* Hotkey fix for moving bug

* Removes rule sets for visited links

* Fixes ordering multiples on cargo console

* fix possible wonky json payloads

* fix admin log input list stuff

* TGUIv4: Reconnect fix and some other fixes + tweaks (#23790)

* Fix reconnecting and transfer it to TSX

* RPD polish

* RCD, AccessList, GuestPass

* Focus reset fix when camera switched. God forgive me again...

* Purple box and identation fixes

For votes and health scanner

* ChemMaster and ChemDispenser

ChemMaster - translucent pills buttons and no grow produce section
ChemDuspenser - dynamic height

* ShuttleManipulator Tabs

* RCD buttons

* make input bar mode more prominent

* fix chem master icons

* tab fix

* Properly sanitizes loaded messages in tgui chat

* TGUIv4 - Adjustments, Fixes, and Tweaks v2 (#23795)

* Fixes icon spacing for nanobank tabs

* Tweaks Dropdown styling

* Fixes sending stuff to old chat

* OpenDream TGUI fix

* Resizes Station Traits window

* Adjusts health analyzer messages for chat tabs

* Revert "Properly sanitizes loaded messages in tgui chat"

This reverts commit 4c32a7094a.

* Revert "Revert "Properly sanitizes loaded messages in tgui chat""

This reverts commit 24afa55922.

* Changes how tgui handles static data

* Reenable no-undef rule. Enable Format On Save for VSCode (#23803)

* Enable Format On Save for VSCode

* Re-enable and fix no-undef

---------

Co-authored-by: Arthri <41360489+a@users.noreply.github.com>

* pda scanner message improvements

* fix mentor/admin PMs being sent to the "unsorted" category

* fix garbled custom vote messages

* moves cyborg analyser to use chatboxes

* fixes regression

* Prevent F5 reload

* TGUIv4: Themes polish and some new ones + misc fixes (#23814)

* Little themes tweaks

* NTOS chat theme

* Update tgui-panel.bundle.js

* Capitalize themes

* Paradise theme v1.0

* Little StationTraitsPanel cleanup

* Update member_content.dmi

* NoCapitalize

* Rebuild

* Delete Paradise.scss

* Create paradise.scss

* Forgot

* Rewritten Security Records Console

For Jesus fucking christ...

* Vending and Wires sizes tweaks

* ThermoMachine

* Syndicate Theme

* Little darker

* Rewritten Medical Records Console

* PDA Nanobank fix

* Syndicate Theme Darker

* Rebuild TGUI

* Wires section grow fix

* fixes the enshittified paradise icon

* Sirryan+Warrior reviews

* bloody define comments

* comment correction

* unprivate these

* errant . begone (how did this not cause a compiler error)

* TGUIv4 - Adjustments, Fixes, and Tweaks v3 (#23807)

* Fixes ERT Manager silenced message function

* Adjusts progress bar on ExosuitFabricator

* Sets Dropdown selected to align left

* Removes the use of self_state select interfaces

* Exosuit Fabricator style adjustments

* fix sextractor UI

* remove empty tochat string

* fix human air alarm interactions

* fix modals

* add F12/IEChooser

---------

Co-authored-by: S34N <12197162+S34NW@users.noreply.github.com>

---------

Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
Co-authored-by: Aylong <alexanderkitsa@gmail.com>
Co-authored-by: Aylong <69762909+Aylong220@users.noreply.github.com>
Co-authored-by: Gaxeer <44334376+Gaxeer@users.noreply.github.com>
Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
Co-authored-by: Arthri <41360489+Arthri@users.noreply.github.com>
Co-authored-by: Arthri <41360489+a@users.noreply.github.com>
This commit is contained in:
S34N
2024-01-21 19:20:35 +01:00
committed by GitHub
co-authored by Arthri Burzah Aylong Aylong Gaxeer DGamerL Arthri
parent 98638f1d38
commit 22fbd9aced
792 changed files with 39742 additions and 26804 deletions
+118 -48
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
*/
/**
@@ -10,14 +11,10 @@
* Used to open and update UIs.
* If this proc is not implemented properly, the UI will not update correctly.
*
* * mob/user - The mob who opened/is using the UI. (REQUIRED)
* * ui_key - The ui_key of the UI. (OPTIONAL)
* * datum/tgui/ui - The UI to be updated, if it exists. (OPTIONAL)
* * force_open - If the UI should be re-opened instead of updated. (OPTIONAL)
* * datum/tgui/master_ui - The parent UI. (OPTIONAL)
* * datum/ui_state/state - The state used to determine status. (OPTIONAL)
* required user mob The mob who opened/is using the UI.
* optional ui datum/tgui The UI to be updated, if it exists.
*/
/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.
/**
@@ -26,10 +23,11 @@
* Data to be sent to the UI.
* This must be implemented for a UI to work.
*
* * mob/user - The mob interacting with the UI.
* required user mob The mob interacting with the UI.
*
* return list Data to be sent to the UI.
*/
/datum/proc/ui_data(mob/user)
// Two good reasons why it shouldn't:
// 1) this is polled several times a second, so sleeping means more running threads, needlessly tanking performance
// 2) if you try to sleep, you get fun bugs ranging from BSOD to uninteractable white windows, to windows straight up vanishing.
// Just don't.
@@ -40,12 +38,15 @@
* public
*
* 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
* frequently.
* Gets squished into one object on the frontend side, but the static part is cached.
*
* * mob/user - The mob interacting with 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 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.
*
* return list Statuic Data to be sent to the UI.
*/
/datum/proc/ui_static_data(mob/user)
SHOULD_NOT_SLEEP(TRUE)
@@ -54,49 +55,44 @@
/**
* 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.
*
* * mob/user - The mob currently interacting with the UI. (REQUIRED)
* * datum/tgui/ui - UI to be updated (OPTIONAL)
* * ui_key - Key of the UI to be updated. (OPTIONAL)
* required user the mob currently interacting with the ui
* optional ui ui to be updated
*/
/datum/proc/update_static_data(mob/user, datum/tgui/ui, ui_key = "main")
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.
/datum/proc/update_static_data(mob/user, datum/tgui/ui)
if(!ui)
return
ui.push_data(null, ui_static_data(user), TRUE)
ui = SStgui.get_open_ui(user, src)
if(ui)
ui.send_full_update()
/**
* public
*
* Called on a UI when the UI receieves a href.
* Think of this as Topic().
* Returns TRUE if the UI should be updated, and FALSE if not.
*
* * action - The action/button that has been invoked by the user.
* * list/params - A list of parameters attached to the button.
* 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 is not interactive or usr calling Topic is not the UI user, bail.
if(!ui || ui.status != STATUS_INTERACTIVE)
if(!ui || ui.status != UI_INTERACTIVE)
return TRUE
/**
* public
*
* Called on an object when a tgui object is being created, allowing you to
* customise the html
* For example: inserting a custom stylesheet that you need in the head
* push various assets to tgui, for examples spritesheets.
*
* For this purpose, some tags are available in the html, to be parsed out
^ with replacetext
* (customheadhtml) - Additions to the head tag
*
* * html - The html base text.
* return list List of asset datums or file paths.
*/
/datum/proc/ui_base_html(html)
return html
/datum/proc/ui_assets(mob/user)
return list()
/**
* private
@@ -108,6 +104,15 @@
/datum/proc/ui_host(mob/user)
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
*
@@ -116,6 +121,27 @@
*/
/datum/var/list/tgui_shared_states
/**
* global
*
* Tracks open UIs for a user.
*/
/mob/var/list/tgui_open_uis = list()
/**
* global
*
* Tracks open windows for a user.
*/
/client/var/list/tgui_windows = list()
/**
* global
*
* TRUE if cache was reloaded by tgui dev server at least once.
*/
/client/var/tgui_cache_reloaded = FALSE
/**
* public
*
@@ -130,19 +156,63 @@
* Called by UIs when they are closed.
* Must be a verb so winset() can call it.
*
* * uid - The UI that was closed.
* required uiref ref The UI that was closed.
*/
/client/verb/uiclose(uid as text)
/client/verb/uiclose(window_id as text)
// Name the verb, and hide it from the user panel.
set name = "uiclose"
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 UID.
var/datum/tgui/ui = locateUID(uid)
/**
* Middleware for /client/Topic.
*
* return bool If TRUE, prevents propagation of the topic call.
*/
/proc/tgui_Topic(href_list)
// Skip non-tgui topics
if(!href_list["tgui"])
return FALSE
var/type = href_list["type"]
// Unconditionally collect tgui logs
if(type == "log")
log_tgui(usr, href_list["message"])
// Reload all tgui windows
if(type == "cacheReloaded")
if(!check_rights(R_ADMIN) || usr.client.tgui_cache_reloaded)
return TRUE
// Mark as reloaded
usr.client.tgui_cache_reloaded = TRUE
// Notify windows
var/list/windows = usr.client.tgui_windows
for(var/window_id in windows)
var/datum/tgui_window/window = windows[window_id]
if(window.status == TGUI_WINDOW_READY)
window.on_message(type, null, href_list)
return TRUE
// 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 TRUE
// Decode payload
var/payload
if(href_list["payload"])
var/payload_text = href_list["payload"]
if(!rustg_json_is_valid(payload_text))
log_tgui(usr, "Error: Invalid JSON")
return TRUE
// 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()
payload = json_decode(payload_text)
// Pass message to window
if(window)
window.on_message(type, payload, href_list)
return TRUE
@@ -199,10 +199,13 @@
cut_and_generate_data()
/datum/ui_module/appearance_changer/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)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/appearance_changer/ui_state(mob/user)
return GLOB.default_state
/datum/ui_module/appearance_changer/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "AppearanceChanger", name, 800, 450, master_ui, state)
ui = new(user, src, "AppearanceChanger", name)
ui.open()
/datum/ui_module/appearance_changer/ui_data(mob/user)
+14 -9
View File
@@ -9,19 +9,24 @@
if("open_alarm")
var/obj/machinery/alarm/alarm = locate(params["aref"]) in GLOB.air_alarms
if(alarm)
alarm.ui_interact(usr, master_ui = ui, state = GLOB.always_state) // ALWAYS is intentional here, as the master_ui pass will prevent fuckery
alarm.ui_interact(usr)
/datum/ui_module/atmos_control/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)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/atmos_control/ui_state(mob/user)
if(isliving(usr))
return GLOB.human_adjacent_state
return GLOB.default_state
/datum/ui_module/atmos_control/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "AtmosControl", name, 800, 600, master_ui, state)
// Send nanomaps
var/datum/asset/nanomaps = get_asset_datum(/datum/asset/simple/nanomaps)
nanomaps.send(user)
ui = new(user, src, "AtmosControl", name)
ui.open()
/datum/ui_module/atmos_control/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/simple/nanomaps)
)
/datum/ui_module/atmos_control/ui_data(mob/user)
var/list/data = list()
data["alarms"] = GLOB.air_alarm_repository.air_alarm_data(GLOB.air_alarms, target_z=level_name_to_num(MAIN_STATION))
@@ -19,10 +19,13 @@
target_datum = target
/datum/ui_module/colour_matrix_tester/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.admin_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/colour_matrix_tester/ui_state(mob/user)
return GLOB.admin_state
/datum/ui_module/colour_matrix_tester/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "ColourMatrixTester", name, 350, 170, master_ui, state)
ui = new(user, src, "ColourMatrixTester", name)
ui.autoupdate = TRUE
ui.open()
+9 -9
View File
@@ -27,19 +27,19 @@
return
viewing_current_z_level = text2num(params["new_level"])
/datum/ui_module/crew_monitor/ui_state(mob/user)
return GLOB.default_state
/datum/ui_module/crew_monitor/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)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/crew_monitor/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "CrewMonitor", name, 800, 600, master_ui, state)
// Send nanomaps
var/datum/asset/nanomaps = get_asset_datum(/datum/asset/simple/nanomaps)
nanomaps.send(user)
ui = new(user, src, "CrewMonitor", name)
ui.open()
/datum/ui_module/crew_monitor/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/simple/nanomaps)
)
/datum/ui_module/crew_monitor/ui_data(mob/user)
var/list/data = list()
@@ -2,10 +2,13 @@
name = "Destination Tagger"
var/my_tag = 1
/datum/ui_module/destination_tagger/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = TRUE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/destination_tagger/ui_state(mob/user)
return GLOB.default_state
/datum/ui_module/destination_tagger/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "DestinationTagger", name, 400, 350, master_ui, state)
ui = new(user, src, "DestinationTagger", name)
ui.open()
ui.set_autoupdate(FALSE)
+7 -4
View File
@@ -11,10 +11,13 @@
/// The below is a toggle for if sec cyborgs are enabled or not
var/cyborg_security = FALSE
/datum/ui_module/ert_manager/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.admin_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/ert_manager/ui_state(mob/user)
return GLOB.admin_state
/datum/ui_module/ert_manager/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "ERTManager", name, 350, 540, master_ui, state)
ui = new(user, src, "ERTManager", name)
ui.autoupdate = TRUE
ui.open()
@@ -96,7 +99,7 @@
if(cyborg_slots > 0)
slots_list += "cyborg: [cyborg_slots]"
var/silenced = text2bool(params["silent"])
var/silenced = (params["silent"])
D.silent = silenced
var/slot_text = english_list(slots_list)
@@ -3,10 +3,18 @@ GLOBAL_DATUM_INIT(generic_crew_manifest, /datum/ui_module/generic_crew_manifest,
/datum/ui_module/generic_crew_manifest
name = "Crew Manifest"
/datum/ui_module/generic_crew_manifest/ui_interact(user, ui_key = "GenericCrewManifest", datum/tgui/ui = null, datum/tgui/master_ui = null, state)
ui = SStgui.try_update_ui(user, src, ui_key, ui)
/datum/ui_module/generic_crew_manifest/ui_state(mob/user)
if(isnewplayer(user))
return GLOB.always_state
if(isobserver(user))
return GLOB.observer_state
if(issilicon(user))
return GLOB.not_incapacitated_state
/datum/ui_module/generic_crew_manifest/ui_interact(user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "GenericCrewManifest", name, 588, 510, master_ui, state)
ui = new(user, src, "GenericCrewManifest", name)
ui.set_autoupdate(FALSE)
ui.open()
+6 -3
View File
@@ -16,10 +16,13 @@ GLOBAL_DATUM_INIT(ghost_hud_panel, /datum/ui_module/ghost_hud_panel, new)
"diagnostic" = DATA_HUD_DIAGNOSTIC_ADVANCED
)
/datum/ui_module/ghost_hud_panel/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.observer_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui)
/datum/ui_module/ghost_hud_panel/ui_state(mob/user)
return GLOB.observer_state
/datum/ui_module/ghost_hud_panel/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "GhostHudPanel", name, 250, 207, master_ui, state)
ui = new(user, src, "GhostHudPanel", name)
ui.set_autoupdate(FALSE)
ui.open()
+10 -3
View File
@@ -149,10 +149,17 @@
to_chat(usr, "<span class='notice'>Laws displayed.</span>")
/datum/ui_module/law_manager/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)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/law_manager/ui_state(mob/user)
if(check_rights(R_ADMIN, FALSE))
return GLOB.admin_state
if(issilicon(user))
return GLOB.conscious_state
return GLOB.default_state
/datum/ui_module/law_manager/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "LawManager", sanitize("[src] - [owner.name]"), 800, is_malf(user) ? 600 : 400, master_ui, state)
ui = new(user, src, "LawManager", sanitize("[src] - [owner.name]"))
ui.open()
/datum/ui_module/law_manager/ui_data(mob/user)
+6 -3
View File
@@ -11,10 +11,13 @@
if(!select_monitor)
powermonitor = ui_host()
/datum/ui_module/power_monitor/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)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/power_monitor/ui_state(mob/user)
return GLOB.default_state
/datum/ui_module/power_monitor/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "PowerMonitor", name, 600, 650, master_ui, state)
ui = new(user, src, "PowerMonitor", name)
ui.open()
/datum/ui_module/power_monitor/ui_data(mob/user)
@@ -7,10 +7,13 @@
CRASH("A [S.type] was passed to /datum/ui_module/robot_self_diagnosis/New().")
owner = S
/datum/ui_module/robot_self_diagnosis/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.always_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/robot_self_diagnosis/ui_state(mob/user)
return GLOB.always_state
/datum/ui_module/robot_self_diagnosis/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "RobotSelfDiagnosis", "Component Self Diagnosis", 280, 480, master_ui, state)
ui = new(user, src, "RobotSelfDiagnosis", "Component Self Diagnosis")
ui.set_autoupdate(FALSE)
ui.open()
+6 -3
View File
@@ -114,10 +114,13 @@
while(!choice && !closed)
stoplag(1)
/datum/tgui_list_input/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.always_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/tgui_list_input/ui_state(mob/user)
return GLOB.always_state
/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "ListInput", title, 325, 355, master_ui, state)
ui = new(user, src, "ListInput", title)
ui.set_autoupdate(FALSE)
ui.open()
+6 -3
View File
@@ -1,10 +1,13 @@
/datum/ui_module/volume_mixer
name = "Volume Mixer"
/datum/ui_module/volume_mixer/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.always_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
/datum/ui_module/volume_mixer/ui_state(mob/user)
return GLOB.always_state
/datum/ui_module/volume_mixer/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, ui_key, "VolumeMixer", name, 400, clamp(80 + 50 * length(user.client.prefs.volume_mixer), 300, 600), master_ui, state)
ui = new(user, src, "VolumeMixer", name)
ui.set_autoupdate(FALSE)
ui.open()
+47 -34
View File
@@ -1,10 +1,11 @@
/**
* 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
*/
/**
* public
*
@@ -17,19 +18,20 @@
*/
/datum/proc/ui_status(mob/user, datum/ui_state/state)
var/src_object = ui_host(user)
. = STATUS_CLOSE
. = UI_CLOSE
if(!state)
return
if(isobserver(user))
// If they turn on ghost AI control, admins can always interact.
if(user.client.advanced_admin_interaction)
. = max(., STATUS_INTERACTIVE)
if(user.can_admin_interact())
. = max(., UI_INTERACTIVE)
// Regular ghosts can always at least view if in range.
var/clientviewlist = getviewsize(user.client.view)
if(get_dist(src_object, user) < max(clientviewlist[1],clientviewlist[2]))
. = max(., STATUS_UPDATE)
if(user.client)
var/clientviewlist = getviewsize(user.client.view)
if(get_dist(src_object, user) < max(clientviewlist[1], clientviewlist[2]))
. = max(., UI_UPDATE)
// Check if the state allows interaction
var/result = state.can_use_topic(src_object, user)
@@ -47,7 +49,8 @@
* return UI_state The state of the UI.
*/
/datum/ui_state/proc/can_use_topic(src_object, mob/user)
return STATUS_CLOSE // Don't allow interaction by default.
// Don't allow interaction by default.
return UI_CLOSE
/**
* public
@@ -57,22 +60,27 @@
* return UI_state The state of the UI.
*/
/mob/proc/shared_ui_interaction(src_object)
if(!client) // Close UIs if mindless.
return STATUS_CLOSE
else if(stat) // Disable UIs if unconcious.
return STATUS_DISABLED
else if(incapacitated()) // Update UIs if incapicitated but concious.
return STATUS_UPDATE
return STATUS_INTERACTIVE
// Close UIs if mindless.
if(!client)
return UI_CLOSE
// Disable UIs if unconcious.
else if(stat)
return UI_DISABLED
// Update UIs if incapicitated but concious.
else if(incapacitated())
return UI_UPDATE
return UI_INTERACTIVE
/mob/living/silicon/ai/shared_ui_interaction(src_object)
if(lacks_power() && !apc_override) // Disable UIs if the AI is unpowered.
return STATUS_DISABLED
// Disable UIs if the AI is unpowered.
if(lacks_power() && !apc_override)
return UI_DISABLED
return ..()
/mob/living/silicon/robot/shared_ui_interaction(src_object)
if(!cell || cell.charge <= 0 || lockcharge) // Disable UIs if the Borg is unpowered or locked.
return STATUS_DISABLED
// Disable UIs if the Borg is unpowered or locked.
if(!cell || cell.charge <= 0 || lockcharge)
return UI_DISABLED
return ..()
/**
@@ -88,7 +96,8 @@
* 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.
// Just call this mob's check.
return user.shared_living_ui_distance(src_object)
/**
* public
@@ -100,19 +109,23 @@
* return UI_state The state of the UI.
*/
/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.
return STATUS_CLOSE
// If the object is obscured, close it.
if(viewcheck && !(src_object in view(src)))
return UI_CLOSE
var/dist = get_dist(src_object, src)
if(dist <= 1) // Open and interact if 1-0 tiles away.
return STATUS_INTERACTIVE
else if(dist <= 2) // View only if 2-3 tiles away.
return STATUS_UPDATE
else if(dist <= 5) // Disable if 5 tiles away.
return STATUS_DISABLED
return STATUS_CLOSE // Otherwise, we got nothing.
// Open and interact if 1-0 tiles away.
if(dist <= 1)
return UI_INTERACTIVE
// View only if 2-3 tiles away.
else if(dist <= 2)
return UI_UPDATE
// Disable if 5 tiles away.
else if(dist <= 5)
return UI_DISABLED
// Otherwise, we got nothing.
return UI_CLOSE
/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object)
/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck)
if(HAS_TRAIT(src, TRAIT_TELEKINESIS) && (get_dist(src, src_object) <= 2))
return STATUS_INTERACTIVE
return UI_INTERACTIVE
return ..()
+2 -2
View File
@@ -8,5 +8,5 @@ GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new)
/datum/ui_state/admin_state/can_use_topic(src_object, mob/user)
if(check_rights_for(user.client, R_ADMIN))
return STATUS_INTERACTIVE
return STATUS_CLOSE
return UI_INTERACTIVE
return UI_CLOSE
+7 -2
View File
@@ -1,10 +1,15 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: always_state
*
* Always grants the user STATUS_INTERACTIVE. Period.
* Always grants the user UI_INTERACTIVE. Period.
*/
GLOBAL_DATUM_INIT(always_state, /datum/ui_state/always_state, new)
/datum/ui_state/always_state/can_use_topic(src_object, mob/user)
return STATUS_INTERACTIVE
return UI_INTERACTIVE
+7 -2
View File
@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: conscious_state
*
@@ -8,5 +13,5 @@ GLOBAL_DATUM_INIT(conscious_state, /datum/ui_state/conscious_state, new)
/datum/ui_state/conscious_state/can_use_topic(src_object, mob/user)
if(user.stat == CONSCIOUS)
return STATUS_INTERACTIVE
return STATUS_CLOSE
return UI_INTERACTIVE
return UI_CLOSE
+6 -1
View File
@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: contained_state
*
@@ -9,6 +14,6 @@ GLOBAL_DATUM_INIT(contained_state, /datum/ui_state/contained_state, new)
/datum/ui_state/contained_state/can_use_topic(atom/src_object, mob/user)
if(!src_object.contains(user))
return STATUS_CLOSE
return UI_CLOSE
return user.shared_ui_interaction(src_object)
*/
+8 -2
View File
@@ -1,12 +1,18 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* 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.
*/
GLOBAL_DATUM_INIT(deep_inventory_state, /datum/ui_state/deep_inventory_state, new)
/datum/ui_state/deep_inventory_state/can_use_topic(src_object, mob/user)
if(!user.contains(src_object))
return STATUS_CLOSE
return UI_CLOSE
return user.shared_ui_interaction(src_object)
+26 -22
View File
@@ -1,7 +1,11 @@
/**
* 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)
@@ -10,67 +14,67 @@ GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new)
return user.default_can_use_topic(src_object) // Call the individual mob-overridden procs.
/mob/proc/default_can_use_topic(src_object)
return STATUS_CLOSE // Don't allow interaction by default.
return UI_CLOSE // Don't allow interaction by default.
/mob/living/default_can_use_topic(src_object)
. = shared_ui_interaction(src_object)
if(. > STATUS_CLOSE && loc)
if(. > UI_CLOSE && loc) //must not be in nullspace.
. = min(., loc.contents_ui_distance(src_object, src)) // Check the distance...
if(. == STATUS_INTERACTIVE) // Non-human living mobs can only look, not touch.
return STATUS_UPDATE
if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) && . == UI_INTERACTIVE) // Non-human living mobs and mobs with blocked hands can only look, not touch.
return UI_UPDATE
/mob/living/carbon/human/default_can_use_topic(src_object)
. = shared_ui_interaction(src_object)
if(. > STATUS_CLOSE)
if(. > UI_CLOSE)
. = min(., shared_living_ui_distance(src_object)) // Check the distance...
/mob/living/silicon/robot/default_can_use_topic(src_object)
. = shared_ui_interaction(src_object)
if(. <= STATUS_DISABLED)
if(. <= UI_DISABLED)
return
// Robots can interact with anything they can see.
var/list/clientviewlist = getviewsize(client.view)
if((src_object in view(src)) && (get_dist(src, src_object) <= max(clientviewlist[1], clientviewlist[2])))
return STATUS_INTERACTIVE
return STATUS_DISABLED // Otherwise they can keep the UI open.
if((src_object in view(src)) && get_dist(src, src_object) <= max(clientviewlist[1], clientviewlist[2]))
return UI_INTERACTIVE
return UI_DISABLED // Otherwise they can keep the UI open.
/mob/living/silicon/ai/default_can_use_topic(src_object)
. = shared_ui_interaction(src_object)
if(. < STATUS_INTERACTIVE)
if(. < UI_INTERACTIVE)
return
// The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled.
if(!control_disabled && can_see(src_object))
return STATUS_INTERACTIVE
return STATUS_CLOSE
return UI_INTERACTIVE
return UI_CLOSE
/mob/living/simple_animal/revenant/default_can_use_topic(src_object)
return STATUS_UPDATE
return UI_UPDATE
/mob/living/simple_animal/demon/pulse_demon/default_can_use_topic(src_object)
. = shared_ui_interaction(src_object)
if(. < STATUS_INTERACTIVE)
if(. < UI_INTERACTIVE)
return
// anything in its APC's area
if(get_area(src_object) == controlling_area)
return STATUS_INTERACTIVE
return STATUS_CLOSE
return UI_INTERACTIVE
return UI_CLOSE
/mob/living/simple_animal/default_can_use_topic(src_object)
. = shared_ui_interaction(src_object)
if(. > STATUS_CLOSE)
if(. > UI_CLOSE)
. = min(., shared_living_ui_distance(src_object)) //simple animals can only use things they're near.
/mob/living/silicon/pai/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 STATUS_INTERACTIVE
return UI_INTERACTIVE
else
return ..()
/mob/dead/observer/default_can_use_topic()
/mob/dead/observer/default_can_use_topic(src_object)
if(can_admin_interact())
return STATUS_INTERACTIVE // Admins are more equal
return STATUS_UPDATE // Ghosts can view updates
return UI_INTERACTIVE // Admins are more equal
return UI_UPDATE // Ghosts can view updates
+15 -10
View File
@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: hands_state
*
@@ -8,21 +13,21 @@ GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new)
/datum/ui_state/hands_state/can_use_topic(src_object, mob/user)
. = user.shared_ui_interaction(src_object)
if(. > STATUS_CLOSE)
if(. > UI_CLOSE)
return min(., user.hands_can_use_topic(src_object))
/mob/proc/hands_can_use_topic(src_object)
return STATUS_CLOSE
return UI_CLOSE
/mob/living/hands_can_use_topic(src_object)
if(is_in_active_hand(src_object) || is_in_inactive_hand(src_object))
return STATUS_INTERACTIVE
return STATUS_CLOSE
/mob/living/simple_animal/revenant/hands_can_use_topic(src_object)
return STATUS_UPDATE
if(is_holding(src_object))
return UI_INTERACTIVE
return UI_CLOSE
/mob/living/silicon/robot/hands_can_use_topic(src_object)
if(activated(src_object))
return STATUS_INTERACTIVE
return STATUS_CLOSE
return UI_INTERACTIVE
return UI_CLOSE
/mob/living/simple_animal/revenant/hands_can_use_topic(src_object)
return UI_UPDATE
+6 -1
View File
@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: human_adjacent_state
*
@@ -13,4 +18,4 @@ GLOBAL_DATUM_INIT(human_adjacent_state, /datum/ui_state/human_adjacent_state, ne
var/dist = get_dist(src_object, user)
if((dist > 1) || (!ishuman(user)))
// Can't be used unless adjacent and human, even with TK
. = min(., STATUS_UPDATE)
. = min(., UI_UPDATE)
+1 -1
View File
@@ -12,5 +12,5 @@ GLOBAL_DATUM_INIT(inventory_state, /datum/ui_state/inventory_state, new)
var/mob/living/silicon/robot/R = user
if(src_object in R.module_active) // Magnetic grippers
return user.shared_ui_interaction(src_object)
return STATUS_CLOSE
return UI_CLOSE
return user.shared_ui_interaction(src_object)
+10 -5
View File
@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: not_incapacitated_state
*
@@ -22,8 +27,8 @@ GLOBAL_DATUM_INIT(not_incapacitated_turf_state, /datum/ui_state/not_incapacitate
turf_check = no_turfs
/datum/ui_state/not_incapacitated_state/can_use_topic(src_object, mob/user)
if(user.stat)
return STATUS_CLOSE
if(user.incapacitated() || (turf_check && !isturf(user.loc)))
return STATUS_DISABLED
return STATUS_INTERACTIVE
if(user.stat != CONSCIOUS)
return UI_CLOSE
if(HAS_TRAIT(src, TRAIT_UI_BLOCKED) || user.incapacitated() || (turf_check && !isturf(user.loc)))
return UI_DISABLED
return UI_INTERACTIVE
+15 -7
View File
@@ -1,25 +1,33 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* 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.
*/
/*
GLOBAL_DATUM_INIT(notcontained_state, /datum/ui_state/notcontained_state, new)
/datum/ui_state/notcontained_state/can_use_topic(atom/src_object, mob/user)
. = user.shared_ui_interaction(src_object)
if(. > STATUS_CLOSE)
if(. > UI_CLOSE)
return min(., user.notcontained_can_use_topic(src_object))
/mob/proc/notcontained_can_use_topic(src_object)
return STATUS_CLOSE
return UI_CLOSE
/mob/living/notcontained_can_use_topic(atom/src_object)
if(src_object.contains(src))
return STATUS_CLOSE // Close if we're inside it.
return UI_CLOSE // Close if we're inside it.
return default_can_use_topic(src_object)
/mob/living/silicon/notcontained_can_use_topic(src_object)
return default_can_use_topic(src_object) // Silicons use default bevhavior.
*/
return default_can_use_topic(src_object) // Silicons use default behavior.
/mob/living/simple_animal/drone/notcontained_can_use_topic(src_object)
return default_can_use_topic(src_object) // Drones use default behavior.
+3 -3
View File
@@ -8,8 +8,8 @@ GLOBAL_DATUM_INIT(observer_state, /datum/ui_state/observer_state, new)
/datum/ui_state/observer_state/can_use_topic(src_object, mob/user)
if(isobserver(user))
return STATUS_INTERACTIVE
return UI_INTERACTIVE
if(check_rights(R_ADMIN, 0, src))
return STATUS_INTERACTIVE
return STATUS_CLOSE
return UI_INTERACTIVE
return UI_CLOSE
+39 -8
View File
@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: physical_state
*
@@ -8,23 +13,49 @@ GLOBAL_DATUM_INIT(physical_state, /datum/ui_state/physical, new)
/datum/ui_state/physical/can_use_topic(src_object, mob/user)
. = user.shared_ui_interaction(src_object)
if(. > STATUS_CLOSE)
if(. > UI_CLOSE)
return min(., user.physical_can_use_topic(src_object))
/mob/proc/physical_can_use_topic(src_object)
return STATUS_CLOSE
/mob/living/simple_animal/revenant/physical_can_use_topic(src_object)
return STATUS_UPDATE
return UI_CLOSE
/mob/living/simple_animal/demon/pulse_demon/physical_can_use_topic(src_object)
return STATUS_UPDATE
return UI_UPDATE
/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(STATUS_UPDATE, shared_living_ui_distance(src_object)) // Silicons can always see.
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 STATUS_UPDATE // AIs are not physical.
return UI_UPDATE // AIs are not physical.
/mob/living/simple_animal/revenant/physical_can_use_topic(src_object)
return UI_UPDATE
/**
* tgui state: physical_obscured_state
*
* Short-circuits the default state to only check physical distance, being in view doesn't matter
*/
GLOBAL_DATUM_INIT(physical_obscured_state, /datum/ui_state/physical_obscured_state, new)
/datum/ui_state/physical_obscured_state/can_use_topic(src_object, mob/user)
. = user.shared_ui_interaction(src_object)
if(. > UI_CLOSE)
return min(., user.physical_obscured_can_use_topic(src_object))
/mob/proc/physical_obscured_can_use_topic(src_object)
return UI_CLOSE
/mob/living/physical_obscured_can_use_topic(src_object)
return shared_living_ui_distance(src_object, viewcheck = FALSE)
/mob/living/silicon/physical_obscured_can_use_topic(src_object)
return max(UI_UPDATE, shared_living_ui_distance(src_object, viewcheck = FALSE)) // Silicons can always see.
/mob/living/silicon/ai/physical_obscured_can_use_topic(src_object)
return UI_UPDATE // AIs are not physical.
+6 -1
View File
@@ -1,3 +1,8 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: self_state
*
@@ -8,5 +13,5 @@ GLOBAL_DATUM_INIT(self_state, /datum/ui_state/self_state, new)
/datum/ui_state/self_state/can_use_topic(src_object, mob/user)
if(src_object != user)
return STATUS_CLOSE
return UI_CLOSE
return user.shared_ui_interaction(src_object)
+1 -1
View File
@@ -7,4 +7,4 @@
GLOBAL_DATUM_INIT(viewer_state, /datum/ui_state/viewer_state, new)
/datum/ui_state/viewer_state/can_use_topic(src_object, mob/user)
return STATUS_UPDATE
return UI_UPDATE
-16
View File
@@ -1,16 +0,0 @@
/**
* tgui state: z_state
*
* Only checks that the Z-level of the user and src_object are the same.
*/
/*
GLOBAL_DATUM_INIT(z_state, /datum/ui_state/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 STATUS_INTERACTIVE
return STATUS_CLOSE
*/
+221 -254
View File
@@ -1,7 +1,6 @@
/**
* tgui
*
* /tg/station user interface library
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
@@ -14,34 +13,32 @@
var/datum/src_object
/// The title of te UI.
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().
var/window_id
/// The window width.
var/width = 0
/// The window height
var/height = 0
var/datum/tgui_window/window
/// Key that is used for remembering the window geometry.
var/window_key
/// Deprecated: Window size.
var/window_size
/// The interface (template) to be used for this UI.
var/interface
/// Update the UI every MC tick.
var/autoupdate = TRUE
/// If the UI has been initialized yet.
var/initialized = FALSE
/// The data (and datastructure) used to initialize the UI.
var/list/initial_data
/// The static data used to initialize the UI.
var/list/initial_static_data
/// Holder for the json string, that is sent during the initial update
var/_initial_update
/// Time of opening the window.
var/opened_at
/// Stops further updates when close() was called.
var/closing = FALSE
/// The status/visibility of the UI.
var/status = STATUS_INTERACTIVE
var/status = UI_INTERACTIVE
/// Timed refreshing state
var/refreshing = FALSE
/// Topic state used to determine status/interactability.
var/datum/ui_state/state = null
/// The parent UI.
var/datum/tgui/master_ui
/// Children of this UI.
var/list/datum/tgui/children = list()
/// Asset data to be sent with every update
var/list/asset_data
/// Rate limit client refreshes to prevent DoS.
COOLDOWN_DECLARE(refresh_cooldown)
/**
* public
@@ -50,39 +47,25 @@
*
* 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.
* optional ui_x int Deprecated: Window width.
* optional ui_y int Deprecated: Window height.
*
* 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]")
src.user = user
src.src_object = src_object
src.ui_key = ui_key
src.window_id = "[src_object.UID()]-[ui_key]"
src.window_key = "[src_object.UID()]-main"
src.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/tgui_assets = get_asset_datum(/datum/asset/simple/tgui)
var/datum/asset/fa = get_asset_datum(/datum/asset/simple/fontawesome)
tgui_assets.send(user)
fa.send(user)
src.title = title
src.state = src_object.ui_state(user)
// Deprecated
if(ui_x && ui_y)
src.window_size = list(ui_x, ui_y)
/**
* public
@@ -91,82 +74,61 @@
*/
/datum/tgui/proc/open()
if(!user.client)
return // Bail if there is no client.
update_status(push = FALSE) // Update the window status.
if(status < STATUS_UPDATE)
return // Bail if we're not supposed to open.
// Build window options
var/window_options = "can_minimize=0;auto_format=0;"
// If we have a width and height, use them.
if(width && height)
window_options += "size=[width]x[height];"
// Remove titlebar and resize handles for a fancy window
if(user.client.prefs.toggles2 & PREFTOGGLE_2_FANCYUI)
window_options += "titlebar=0;can_resize=0;"
return null
if(window)
return null
process_status()
if(status < UI_UPDATE)
return null
window = SStgui.request_pooled_window(user)
if(!window)
return null
opened_at = world.time
window.acquire_lock(src)
if(!window.is_ready())
window.initialize(
fancy = (user.client.prefs.toggles2 & PREFTOGGLE_2_FANCYUI),
assets = list(
get_asset_datum(/datum/asset/simple/tgui),
))
else
window_options += "titlebar=1;can_resize=1;"
// Generate page html
var/html
html = SStgui.basehtml
// Allow the src object to override the html if needed
html = src_object.ui_base_html(html)
// Replace template tokens with important UI data
html = replacetextEx(html, "\[tgui:ref]", "[src.UID()]")
// Open the window.
user << browse(html, "window=[window_id];[window_options]")
// Instruct the client to signal UI when the window is closed.
winset(user, window_id, "on-close=\"uiclose [src.UID()]\"")
// 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))
window.send_message("ping")
send_assets()
window.send_message("update", get_payload(
with_data = TRUE,
with_static_data = TRUE))
SStgui.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, list/static_data)
if(interface)
src.interface = interface
if(data)
initial_data = data
if(static_data)
initial_static_data = static_data
open()
/datum/tgui/proc/send_assets()
var/flushqueue = window.send_asset(get_asset_datum(
/datum/asset/simple/namespaced/fontawesome))
for(var/datum/asset/asset in src_object.ui_assets(user))
flushqueue |= window.send_asset(asset)
if(flushqueue)
user.client.browse_queue_flush()
/**
* public
*
* Close the UI, and all its children.
* Close the UI.
*
* optional can_be_suspended bool
*/
/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()
/datum/tgui/proc/close(can_be_suspended = TRUE)
if(closing)
return
closing = TRUE
// If we don't have window_id, open proc did not have the opportunity
// to finish, therefore it's safe to skip this whole block.
if(window)
// 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.
window.release_lock()
window.close(can_be_suspended)
src_object.ui_close(user)
SStgui.on_close(src)
state = null
master_ui = null
qdel(src)
/**
@@ -174,184 +136,189 @@
*
* 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)
autoupdate = state
/datum/tgui/proc/set_autoupdate(autoupdate)
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().")
return 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
if(!COOLDOWN_FINISHED(src, refresh_cooldown))
refreshing = TRUE
addtimer(CALLBACK(src, PROC_REF(send_full_update)), TGUI_REFRESH_FULL_UPDATE_COOLDOWN, TIMER_UNIQUE)
return
refreshing = FALSE
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))
COOLDOWN_START(src, refresh_cooldown, TGUI_REFRESH_FULL_UPDATE_COOLDOWN)
/**
* 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
*
* 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()
json_data["config"] = list(
"title" = title,
"status" = status,
"interface" = interface,
"fancy" = user.client.prefs.toggles2 & PREFTOGGLE_2_FANCYUI,
"observer" = isobserver(user),
"window" = window_id,
"refreshing" = refreshing,
"window" = list(
"key" = window_key,
"size" = window_size,
"fancy" = (user.client?.prefs?.toggles2 & PREFTOGGLE_2_FANCYUI),
"locked" = FALSE,
),
"client" = list(
"ckey" = user.client.ckey,
"address" = user.client.address,
"computer_id" = user.client.computer_id,
),
"user" = list(
"name" = "[user]",
"observer" = isobserver(user),
),
"map" = SSmapping.map_datum.technical_name,
"ref" = "[src.UID()]"
)
if(!isnull(data))
var/data = custom_data || with_data && src_object.ui_data(user)
if(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
// Send shared states
if(src_object.tgui_shared_states)
json_data["shared"] = src_object.tgui_shared_states
// Generate the JSON.
var/json = json_encode(json_data)
// Strip #255/improper.
json = replacetext(json, "\proper", "")
json = replacetext(json, "\improper", "")
return json
return json_data
/**
* 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(_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 != STATUS_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")
user.client.prefs.toggles2 ^= PREFTOGGLE_2_FANCYUI
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.
* Run an update cycle for this UI. Called internally by SStgui
* every second or so.
*/
/datum/tgui/process(force = FALSE)
if(closing)
return
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()
return
if(status && (force || autoupdate))
update() // Update the UI if the status and update settings allow it.
else
update_status(push = TRUE) // Otherwise only update status.
if(needs_update)
window.send_message("update", get_payload())
/**
* 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.
* Updates the status, and returns TRUE if status has changed.
*/
/datum/tgui/proc/push_data(data, static_data, force = FALSE)
// Update the window state.
update_status(push = FALSE)
// Cannot update UI if it is not set up yet.
if(!initialized)
return
// Cannot update UI, we have no visibility.
if(status <= STATUS_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")
/datum/tgui/proc/process_status()
var/prev_status = status
status = src_object.ui_status(user, state)
return prev_status != status
/**
* 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.
* Callback for handling incoming tgui messages.
*/
/datum/tgui/proc/update(force_open = FALSE)
src_object.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 = FALSE)
var/status = src_object.ui_status(user, state)
if(master_ui)
status = min(status, master_ui.status)
set_status(status, push)
if(status == STATUS_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 = FALSE)
// Only update if status has changed.
if(src.status != status)
if(src.status == STATUS_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 == STATUS_DISABLED || push)
push_data(null, force = TRUE)
/datum/tgui/proc/log_message(message)
log_tgui("[user] ([user.ckey]) using \"[title]\":\n[message]")
/datum/tgui/proc/on_message(type, list/payload, list/href_list)
// Pass act type messages to ui_act
if(type && copytext(type, 1, 5) == "act/")
process_status()
if(src_object.ui_act(copytext(type, 5), payload, src, state))
SStgui.update_uis(src_object)
return FALSE
switch(type)
if("ready")
// Send a full update when the user manually refreshes the UI
if(initialized)
send_full_update()
initialized = TRUE
if("ping/reply")
initialized = TRUE
if("suspend")
close(can_be_suspended = TRUE)
if("close")
close(can_be_suspended = FALSE)
if("log")
if(href_list["fatal"])
close(can_be_suspended = FALSE)
if("setSharedState")
if(status != UI_INTERACTIVE)
return
LAZYINITLIST(src_object.tgui_shared_states)
src_object.tgui_shared_states[href_list["key"]] = href_list["value"]
SStgui.update_uis(src_object)
+30
View File
@@ -0,0 +1,30 @@
## /TG/ Chat
/TG/ Chat, which will be referred to as TgChat from this point onwards, is a system in which we can send messages to clients in a controlled and semi-reliable manner. The standard way of sending messages to BYOND clients simply dumps whatever you output to them directly into their chat window, however BYOND allows us to load our own code on the client to change this behaviour in a way that allows us to do some pretty neat things.
### Message Format
TgChat handles sending messages from the server to the client through the use of JSON payloads, of which the format will change depending on the type of message and the intended client endpoint. An example of the payload for chat messages is as follows:
```json
{
"sequence": 0,
"content": {
"type": ". . .", // ?optional
"text": ". . .", // ?optional !atleast-one
"html": ". . .", // ?optional !atleast-one
"avoidHighlighting": 0 // ?optional
},
}
```
### Reliability
In the past there have been issues where BYOND will silently and without reason lose a message we sent to the client, to detect this and recover from it seamlessly TgChat also has a baked in reliability layer. This reliability layer is very primitive, and simply keeps track of recieved sequence numbers. Should the client recieve an unexpected sequence number TgChat asks the server to resend any missing packets.
### Ping System
TgChat supports a round trip time ping measurement, which is displayed to the client so they can know how long it takes for their commands and inputs to reach the server. This is done by sending the client a ping request, `ping/soft`, which tells the client to send a ping to the server. When the server recieves said ping it sends a reply, `ping/reply`, to the client with a payload containing the current DateTime which the client can reference against the initial ping request.
### Chat Tabs, Local Storage, and Highlighting
To make organizing and managing chat easier and more functional for both players and admins, TgChat has the ability to filter out messages based on their primary tag, such as individual departmental radios, to a dedicated chat tab for easier reading and comprehension. These tabs can also be configured to highlist messages based on a simple keyword search. You can set a multitude of different keywords to search for and they will be highlighting for instant alerting of the client. Said tabs, highlighting rules, and your chat history will persist thanks to use of local storage on the client. Using local storage TgChat can ensure that your preferences are saved and maintained between client restarts and switching between other /TG/ servers. Local Storage is also used to keep your chat history aswell, should you need to scroll through your chat logs.
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/// Admin music volume, from 0 to 1.
/client/var/admin_music_volume = 1
/**
* public
*
* Sends music data to the browser.
*
* Optional settings:
* - pitch: the playback rate
* - start: the start time of the sound
* - end: when the musics stops playing
*
* required url string Must be an https URL.
* optional extra_data list Optional settings.
*/
/datum/tgui_panel/proc/play_music(url, extra_data)
if(!is_ready())
return
var/list/payload = list()
if(length(extra_data) > 0)
for(var/key in extra_data)
payload[key] = extra_data[key]
payload["url"] = url
window.send_message("audio/playMusic", payload)
/**
* public
*
* Stops playing music through the browser.
*/
/datum/tgui_panel/proc/stop_music()
if(!is_ready())
return
window.send_message("audio/stopMusic")
+80
View File
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* Maximum number of connection records allowed to analyze.
* Should match the value set in the browser.
*/
#define TGUI_TELEMETRY_MAX_CONNECTIONS 5
/**
* Maximum time allocated for sending a telemetry packet.
*/
#define TGUI_TELEMETRY_RESPONSE_WINDOW 30 SECONDS
/// Time of telemetry request
/datum/tgui_panel/var/telemetry_requested_at
/// Time of telemetry analysis completion
/datum/tgui_panel/var/telemetry_analyzed_at
/// List of previous client connections
/datum/tgui_panel/var/list/telemetry_connections
/**
* private
*
* Requests some telemetry from the client.
*/
/datum/tgui_panel/proc/request_telemetry()
telemetry_requested_at = world.time
telemetry_analyzed_at = null
window.send_message("telemetry/request", list(
"limits" = list(
"connections" = TGUI_TELEMETRY_MAX_CONNECTIONS,
),
))
/**
* private
*
* Analyzes a telemetry packet.
*
* Is currently only useful for detecting ban evasion attempts.
*/
/datum/tgui_panel/proc/analyze_telemetry(payload)
if(world.time > telemetry_requested_at + TGUI_TELEMETRY_RESPONSE_WINDOW)
message_admins("[key_name(client)] sent telemetry outside of the allocated time window.")
return
if(telemetry_analyzed_at)
message_admins("[key_name(client)] sent telemetry more than once.")
return
telemetry_analyzed_at = world.time
if(!payload)
return
telemetry_connections = payload["connections"]
var/len = length(telemetry_connections)
if(len == 0)
return
if(len > TGUI_TELEMETRY_MAX_CONNECTIONS)
message_admins("[key_name(client)] was kicked for sending a huge telemetry payload")
qdel(client)
return
var/list/found
for(var/i in 1 to len)
if(QDELETED(client))
// He got cleaned up before we were done
return
var/list/row = telemetry_connections[i]
// Check for a malformed history object
if(!row || row.len < 3 || (!row["ckey"] || !row["address"] || !row["computer_id"]))
return
if(world.IsBanned(row["ckey"], row["address"], row["computer_id"]))
found = row
break
CHECK_TICK
// This fucker has a history of playing on a banned account.
if(found)
var/msg = "[key_name(client)] has a banned account in connection history! (Matched: [found["ckey"]], [found["address"]], [found["computer_id"]])"
message_admins(msg)
log_admin(msg)
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui_panel datum
* Hosts tgchat and other nice features.
*/
/datum/tgui_panel
var/client/client
var/datum/tgui_window/window
var/broken = FALSE
var/initialized_at
/datum/tgui_panel/New(client/client)
src.client = client
window = new(client, "browseroutput")
window.subscribe(src, PROC_REF(on_message))
/datum/tgui_panel/Del()
window.unsubscribe(src)
window.close()
return ..()
/**
* public
*
* TRUE if panel is initialized and ready to receive messages.
*/
/datum/tgui_panel/proc/is_ready()
return !broken && window.is_ready()
/**
* public
*
* Initializes tgui panel.
*/
/datum/tgui_panel/proc/initialize(force = FALSE)
set waitfor = FALSE
// Minimal sleep to defer initialization to after client constructor
sleep(1)
initialized_at = world.time
// Perform a clean initialization
window.initialize(assets = list(
get_asset_datum(/datum/asset/simple/tgui_panel),
))
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
request_telemetry()
addtimer(CALLBACK(src, PROC_REF(on_initialize_timed_out)), 5 SECONDS)
/**
* private
*
* Called when initialization has timed out.
*/
/datum/tgui_panel/proc/on_initialize_timed_out()
// Currently does nothing but sending a message to old chat.
SEND_TEXT(client, "<span class=\"userdanger\">Failed to load fancy chat, click <a href='?src=[UID()];reload_tguipanel=1'>HERE</a> to attempt to reload it.</span>")
/**
* private
*
* Callback for handling incoming tgui messages.
*/
/datum/tgui_panel/proc/on_message(type, payload)
if(type == "ready")
broken = FALSE
window.send_message("update", list(
"config" = list(
"client" = list(
"ckey" = client.ckey,
"address" = client.address,
"computer_id" = client.computer_id,
),
"window" = list(
"fancy" = FALSE,
"locked" = FALSE,
),
),
))
return TRUE
if(type == "audio/setAdminMusicVolume")
client.admin_music_volume = payload["volume"]
return TRUE
if(type == "telemetry")
analyze_telemetry(payload)
return TRUE
/**
* public
*
* Sends a round restart notification.
*/
/datum/tgui_panel/proc/send_roundrestart()
window.send_message("roundrestart")
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/client/var/datum/tgui_panel/tgui_panel
/**
* tgui panel / chat troubleshooting verb
*/
/client/verb/fix_tgui_panel()
set name = "Fix chat"
set category = "Special Verbs"
var/action
log_tgui(src, "Started fixing.")
nuke_chat()
// Failed to fix
action = alert(src, "Did that work?", "", "Yes", "No, switch to old ui")
if(action == "No, switch to old ui")
winset(src, "output", "on-show=&is-disabled=0&is-visible=1")
winset(src, "browseroutput", "is-disabled=1;is-visible=0")
log_tgui(src, "Failed to fix.")
/client/proc/nuke_chat()
// Catch all solution (kick the whole thing in the pants)
winset(src, "output", "on-show=&is-disabled=0&is-visible=1")
winset(src, "browseroutput", "is-disabled=1;is-visible=0")
if(!tgui_panel || !istype(tgui_panel))
log_tgui(src, "tgui_panel datum is missing")
tgui_panel = new(src)
tgui_panel.initialize(force = TRUE)
// Force show the panel to see if there are any errors
winset(src, "output", "is-disabled=1&is-visible=0")
winset(src, "browseroutput", "is-disabled=0;is-visible=1")
@@ -0,0 +1,17 @@
/**
* Message-related procs
*
* Message format (/list):
* - type - Message type, must be one of defines in `code/__DEFINES/chat.dm`
* - text - Plain message text
* - html - HTML message text
* - Optional metadata, can be any key/value pair.
*
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/proc/message_to_html(message)
// Here it is possible to add a switch statement
// to custom-handle various message types.
return message["html"] || message["text"]
+92
View File
@@ -0,0 +1,92 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* Circumvents the message queue and sends the message
* to the recipient (target) as soon as possible.
*/
/proc/to_chat_immediate(
target,
html,
type = null,
text = null,
avoid_highlighting = FALSE,
// FIXME: These flags are now pointless and have no effect
handle_whitespace = TRUE,
trailing_newline = TRUE,
confidential = FALSE
)
// Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "")
html = "[html]"
text = "[text]"
if(!target)
return
if(!html && !text)
CRASH("Empty or null string in to_chat proc call.")
if(target == world)
target = GLOB.clients
// Build a message
var/message = list()
if(type)
message["type"] = type
if(text)
message["text"] = text
if(html)
message["html"] = html
if(avoid_highlighting)
message["avoidHighlighting"] = avoid_highlighting
// send it immediately
SSchat.send_immediate(target, message)
/**
* Sends the message to the recipient (target).
*
* Recommended way to write to_chat calls:
* ```
* to_chat(client,
* type = MESSAGE_TYPE_INFO,
* html = "You have found <strong>[object]</strong>")
* ```
*/
/proc/to_chat(
target,
html,
type = null,
text = null,
avoid_highlighting = FALSE,
// FIXME: These flags are now pointless and have no effect
handle_whitespace = TRUE,
trailing_newline = TRUE,
confidential = FALSE
)
if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized)
to_chat_immediate(target, html, type, text)
return
// Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "")
html = "[html]"
text = "[text]"
if(!target)
return
if(!html && !text)
CRASH("Empty or null string in to_chat proc call.")
if(target == world)
target = GLOB.clients
// Build a message
var/message = list()
if(type)
message["type"] = type
if(text)
message["text"] = text
if(html)
message["html"] = html
if(avoid_highlighting)
message["avoidHighlighting"] = avoid_highlighting
SSchat.queue(target, message)
+343
View File
@@ -0,0 +1,343 @@
/**
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/datum/tgui_window
var/id
var/client/client
var/pooled
var/pool_index
var/is_browser = FALSE
var/status = TGUI_WINDOW_CLOSED
var/locked = FALSE
var/datum/tgui/locked_by
var/datum/subscriber_object
var/subscriber_delegate
var/fatally_errored = FALSE
var/message_queue
var/sent_assets = list()
// Vars passed to initialize proc (and saved for later)
var/initial_fancy
var/initial_assets
var/initial_inline_html
var/initial_inline_js
var/initial_inline_css
/**
* 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.client.tgui_windows[id] = src
src.pooled = pooled
if(pooled)
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 assets list List of assets to inline into the html.
* optional inline_html string Custom HTML to inject.
* optional fancy bool If TRUE, will hide the window titlebar.
*/
/datum/tgui_window/proc/initialize(
fancy = FALSE,
assets = list(),
inline_html = "",
inline_js = "",
inline_css = "")
log_tgui(client, "[id]/initialize")
if(!client)
return
src.initial_fancy = fancy
src.initial_assets = assets
src.initial_inline_html = inline_html
src.initial_inline_js = inline_js
src.initial_inline_css = inline_css
status = TGUI_WINDOW_LOADING
fatally_errored = FALSE
// Build window options
var/options = "file=[id].html;can_minimize=0;auto_format=0;"
// Remove titlebar and resize handles for a fancy window
if(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)
// Inject assets
var/inline_assets_str = ""
for(var/datum/asset/asset in assets)
var/mappings = asset.get_url_mappings()
for(var/name in mappings)
var/url = mappings[name]
// Not encoding since asset strings are considered safe
if(copytext(name, -4) == ".css")
inline_assets_str += "Byond.loadCss('[url]', true);\n"
else if(copytext(name, -3) == ".js")
inline_assets_str += "Byond.loadJs('[url]', true);\n"
asset.send(client)
if(length(inline_assets_str))
inline_assets_str = "<script>\n" + inline_assets_str + "</script>\n"
html = replacetextEx(html, "<!-- tgui:assets -->\n", inline_assets_str)
// Inject inline HTML
if(inline_html)
html = replacetextEx(html, "<!-- tgui:inline-html -->", inline_html)
// Inject inline JS
if(inline_js)
inline_js = "<script>\n[inline_js]\n</script>"
html = replacetextEx(html, "<!-- tgui:inline-js -->", inline_js)
// Inject inline CSS
if(inline_css)
inline_css = "<style>\n[inline_css]\n</style>"
html = replacetextEx(html, "<!-- tgui:inline-css -->", inline_css)
// Open the window
client << browse(html, "window=[id];[options]")
// Detect whether the control is a browser
is_browser = winexists(client, id) == "BROWSER"
// Instruct the client to signal UI when the window is closed.
if(!is_browser)
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 be automatically
* subscribed to incoming messages via the on_message proc.
*
* optional ui /datum/tgui
*/
/datum/tgui_window/proc/acquire_lock(datum/tgui/ui)
locked = TRUE
locked_by = ui
/**
* public
*
* 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
*
* Subscribes the datum to consume window messages on a specified proc.
*
* Note, that this supports only one subscriber, because code for that
* is simpler and therefore faster. If necessary, this can be rewritten
* to support multiple subscribers.
*/
/datum/tgui_window/proc/subscribe(datum/object, delegate)
subscriber_object = object
subscriber_delegate = delegate
/**
* public
*
* Unsubscribes the datum. Do not forget to call this when cleaning up.
*/
/datum/tgui_window/proc/unsubscribe(datum/object)
subscriber_object = null
subscriber_delegate = 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, payload, force)
if(!client)
return
var/message = TGUI_CREATE_MESSAGE(type, payload)
// 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, is_browser \
? "[id]:update" \
: "[id].browser:update")
/**
* public
*
* Sends a raw payload to tgui window.
*
* required message string JSON+urlencoded blob to send.
* optional force bool Send regardless of the ready status.
*/
/datum/tgui_window/proc/send_raw_message(message, force)
if(!client)
return
// 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, is_browser \
? "[id]:update" \
: "[id].browser:update")
/**
* public
*
* Makes an asset available to use in tgui.
*
* required asset datum/asset
*
* return bool - TRUE if any assets had to be sent to the client
*/
/datum/tgui_window/proc/send_asset(datum/asset/asset)
if(!client || !asset)
return
sent_assets |= list(asset)
. = asset.send(client)
if(istype(asset, /datum/asset/spritesheet))
var/datum/asset/spritesheet/spritesheet = asset
send_message("asset/stylesheet", spritesheet.css_filename())
send_raw_message(asset.get_serialized_url_mappings())
/**
* 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, is_browser \
? "[id]:update" \
: "[id].browser:update")
message_queue = null
/**
* private
*
* Callback for handling incoming tgui messages.
*/
/datum/tgui_window/proc/on_message(type, payload, href_list)
// Status can be READY if user has refreshed the window.
if(type == "ready" && status == TGUI_WINDOW_READY)
// Resend the assets
for(var/asset in sent_assets)
send_asset(asset)
// Mark this window as fatally errored which prevents it from
// being suspended.
if(type == "log" && href_list["fatal"])
fatally_errored = TRUE
// Mark window as ready since we received this message from somewhere
if(status != TGUI_WINDOW_READY)
status = TGUI_WINDOW_READY
flush_message_queue()
// Pass message to UI that requested the lock
if(locked && locked_by)
var/prevent_default = locked_by.on_message(type, payload, href_list)
if(prevent_default)
return
// Pass message to the subscriber
else if(subscriber_object)
var/prevent_default = call(
subscriber_object,
subscriber_delegate)(type, payload, href_list)
if(prevent_default)
return
// If not locked, handle these message types
switch(type)
if("ping")
send_message("ping/reply", payload)
if("suspend")
close(can_be_suspended = TRUE)
if("close")
close(can_be_suspended = FALSE)
if("openLink")
client << link(href_list["url"])
if("cacheReloaded")
// Reinitialize
initialize(
fancy = initial_fancy,
assets = initial_assets,
inline_html = initial_inline_html,
inline_js = initial_inline_js,
inline_css = initial_inline_css)
// Resend the assets
for(var/asset in sent_assets)
send_asset(asset)
if("chat/resend")
SSchat.handle_resend(client, payload)