Byond 516 v2.0 (#37553)

* The TGS thing

* Revert the 516 revert

* Further segment the world/New() proc

* Fixes an issue here
This commit is contained in:
ShiftyRail
2025-05-12 06:50:25 +01:00
committed by GitHub
parent 90dfcd7df8
commit d79c1fe070
552 changed files with 37857 additions and 26434 deletions

View File

@@ -20,8 +20,8 @@ on:
workflow_dispatch:
env:
BYOND_MAJOR: 515
BYOND_MINOR: 1603
BYOND_MAJOR: 516
BYOND_MINOR: 1659
#This can be declared here because build.py will ignore it if DM_UNIT_TESTS is true.
ALL_MAPS: tgstation metaclub defficiency packedstation roidstation test_tiny test_vault snaxi tgstation-sec tgstation-snow LampreyStation xoq synergy bagelstation lowfatbagel Dorfstation waystation line horizon wheelstation

9
.vscode/tasks.json vendored
View File

@@ -89,6 +89,15 @@
],
"group": "build",
"label": "tgui: sonar"
},
{
"type": "npm",
"script": "install",
"path": "tgui",
"group": "clean",
"problemMatcher": [],
"label": "npm: install - tgui",
"detail": "install dependencies from package"
}
]
}

View File

@@ -439,3 +439,6 @@ var/global/list/visible_spaces = list(/turf/simulated/open, /turf/simulated/floo
#define OMNI_LINK(A,B) isliving(A) && A:omnitool_connect(B)
#define is_real_champion(A) ismob(A) && A.is_wearing_item(/obj/item/weapon/storage/belt/champion) && A.is_wearing_item(/obj/item/clothing/mask/luchador)
// Call by name proc references, checks if the proc exists on either this type () (AND ONLY THIS TYPE) or as a global proc.
#define PROC_REF(X) (nameof(.proc/##X))

6
__DEFINES/cooldowns.dm Normal file
View File

@@ -0,0 +1,6 @@
//Returns true if the cooldown has run its course, false otherwise
#define COOLDOWN_FINISHED(cd_source, cd_index) (cd_source.cd_index <= world.time)
#define COOLDOWN_TIMELEFT(cd_source, cd_index) (max(0, cd_source.cd_index - world.time))
#define COOLDOWN_START(cd_source, cd_index, cd_time) (cd_source.cd_index = world.time + (cd_time))

View File

@@ -368,7 +368,7 @@ var/obj/abstract/screen/plane_master/overdark_planemaster/overdark_planemaster =
appearance_flags = 0
plane = BASE_PLANE
mouse_opacity = 0
screen_loc = "CENTER,CENTER"
screen_loc = "SOUTHWEST,SOUTHWEST"
render_source = "*overdark"
var/obj/abstract/screen/plane_master/overdark_planemaster_target/overdark_planemaster_target = new()

View File

@@ -6,3 +6,6 @@
#define QDEL_LIST_ASSOC_NULL(L) QDEL_LIST_ASSOC(L); L = null
#define QDEL_LIST_CUT(L) QDEL_LIST(L); L.Cut()
#define QDEL_LIST_ASSOC_CUT(L) QDEL_LIST_ASSOC(L); L.Cut()
// QDEL macros borrowed from TG
#define QDELETED(X) (!X || X.gcDestroyed)

View File

@@ -1,5 +1,5 @@
//This file was auto-corrected by findeclaration.exe on 25.5.2012 20:42:31
#if DM_VERSION < 513
#if DM_VERSION < 516
#error Your version of byond is too old, you need version 513 or higher
#endif
#define RUNWARNING // disable if they re-enable run() in 507 or newer.

View File

@@ -19,6 +19,9 @@
/// Maximum ping timeout allowed to detect zombie windows
#define TGUI_PING_TIMEOUT 4 SECONDS
/// Used for rate-limiting to prevent DoS by excessively refreshing a TGUI window
#define TGUI_REFRESH_FULL_UPDATE_COOLDOWN (1 SECONDS)
/// Window does not exist
#define TGUI_WINDOW_CLOSED 0
/// Window was just opened, but is still not ready to be sent data
@@ -38,3 +41,12 @@
#define TGUI_CREATE_MESSAGE(type, payload) ( \
"%7b%22type%22%3a%22[type]%22%2c%22payload%22%3a[url_encode(json_encode(payload))]%7d" \
)
/**
* NAMEOF: Compile time checked variable name to string conversion
* evaluates to a string equal to "X", but compile errors if X isn't a var on datum.
**/
#define NAMEOF(datum, X) (#X || ##datum.##X)
/// Call by name proc reference, checks if the proc exists on either the given type or as a global proc
#define TYPE_PROC_REF(TYPE, X) (nameof(##TYPE.proc/##X))

View File

@@ -14,7 +14,7 @@
var/internal_volume = 10000
var/max_pressure = 10000
var/target_pressure = 4500 //Output pressure.
var/target_pressure = 2500 //Output pressure.
var/datum/gas_mixture/air //Internal tank.
@@ -102,7 +102,7 @@
))
return data
/obj/machinery/atmospherics/binary/msgs/ui_act(action, list/params)
/obj/machinery/atmospherics/binary/msgs/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
@@ -110,9 +110,11 @@
if("toggle_power")
on = !on
update_icon()
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
if("set_pressure")
target_pressure = round(clamp(text2num(params["new_pressure"]), 0, 4500))
SStgui.try_update_ui(ui.user, src, ui)
update_icon()
return TRUE

View File

@@ -29,6 +29,9 @@ var/cmp_field = "name"
/proc/cmp_records_dsc(datum/data/record/a, datum/data/record/b)
return sorttext(a.fields[cmp_field], b.fields[cmp_field])
/proc/cmp_records_numerically(datum/data/record/a, datum/data/record/b)
return b.fields[cmp_field] - a.fields[cmp_field]
/proc/cmp_ckey_asc(client/a, client/b)
return sorttext(b.ckey, a.ckey)

View File

@@ -1,6 +1,7 @@
/datum
var/list/datum_components
var/list/active_timers
var/list/open_uis
/datum/proc/initialize()
return TRUE

View File

@@ -59,3 +59,23 @@
fileaccess_timer = world.time + FTPDELAY
return 0
#undef FTPDELAY
// Similar to clamp but the bottom rolls around to the top and vice versa. min is inclusive, max is exclusive
#define WRAP(val, min, max) clamp(( min == max ? min : (val) - (round(((val) - (min))/((max) - (min))) * ((max) - (min))) ),min,max)
/// Save file as an external file then md5 it.
/// Used because md5ing files stored in the rsc sometimes gives incorrect md5 results.
/// https://www.byond.com/forum/post/2611357
/proc/md5asfile(file)
var/static/notch = 0
// its importaint this code can handle md5filepath sleeping instead of hard blocking, if it's converted to use rust_g.
var/filename = "tmp/md5asfile.[world.realtime].[world.timeofday].[world.time].[world.tick_usage].[notch]"
notch = WRAP(notch+1, 0, 2**15)
fcopy(file, filename)
. = md5filepath(filename)
fdel(filename)
/// Returns the md5 of a file at a given path.
/proc/md5filepath(path)
. = md5(file(path))

View File

@@ -210,3 +210,64 @@
return HSVtoRGB(hsv(hsv_list[1], hsv_list[2], hsv_list[3], hsv_list[4]))
return HSVtoRGB(hsv(hsv_list[1], hsv_list[2], hsv_list[3]))
/// given an icon object, dmi file path, or atom/image/mutable_appearance, attempts to find and return an associated dmi file path.
/// a weird quirk about dm is that /icon objects represent both compile-time or dynamic icons in the rsc,
/// but stringifying rsc references returns a dmi file path
/// ONLY if that icon represents a completely unchanged dmi file from when the game was compiled.
/// so if the given object is associated with an icon that was in the rsc when the game was compiled, this returns a path. otherwise it returns ""
/proc/get_icon_dmi_path(icon/icon)
/// the dmi file path we attempt to return if the given object argument is associated with a stringifiable icon
/// if successful, this looks like "icons/path/to/dmi_file.dmi"
var/icon_path = ""
if(isatom(icon) || istype(icon, /image) || istype(icon, /mutable_appearance))
var/atom/atom_icon = icon
icon = atom_icon.icon
//atom icons compiled in from 'icons/path/to/dmi_file.dmi' are weird and not really icon objects that you generate with icon().
//if they're unchanged dmi's then they're stringifiable to "icons/path/to/dmi_file.dmi"
if(isicon(icon) && isfile(icon))
//icons compiled in from 'icons/path/to/dmi_file.dmi' at compile time are weird and aren't really /icon objects,
///but they pass both isicon() and isfile() checks. they're the easiest case since stringifying them gives us the path we want
var/icon_ref = ref(icon)
var/locate_icon_string = "[locate(icon_ref)]"
icon_path = locate_icon_string
else if(isicon(icon) && "[icon]" == "/icon")
// icon objects generated from icon() at runtime are icons, but they AREN'T files themselves, they represent icon files.
// if the files they represent are compile time dmi files in the rsc, then
// the rsc reference returned by fcopy_rsc() will be stringifiable to "icons/path/to/dmi_file.dmi"
var/rsc_ref = fcopy_rsc(icon)
var/icon_ref = ref(rsc_ref)
var/icon_path_string = "[locate(icon_ref)]"
icon_path = icon_path_string
else if(istext(icon))
var/rsc_ref = fcopy_rsc(icon)
//if its the text path of an existing dmi file, the rsc reference returned by fcopy_rsc() will be stringifiable to a dmi path
var/rsc_ref_ref = ref(rsc_ref)
var/rsc_ref_string = "[locate(rsc_ref_ref)]"
icon_path = rsc_ref_string
if(is_valid_dmi_file(icon_path))
return icon_path
return FALSE
///given a text string, returns whether it is a valid dmi icons folder path
/proc/is_valid_dmi_file(icon_path)
if(!istext(icon_path) || !length(icon_path))
return FALSE
var/is_in_icon_folder = findtextEx(icon_path, "icons/")
var/is_dmi_file = findtextEx(icon_path, ".dmi")
if(is_in_icon_folder && is_dmi_file)
return TRUE
return FALSE

View File

@@ -18,9 +18,9 @@
/ray
var/z //the z-level we are casting our ray in
var/vector/origin //the origin of the ray
var/vector/origin_floored //the floored origin vector
var/vector/direction //direction of the ray
var/_vector/origin //the origin of the ray
var/_vector/origin_floored //the floored origin vector
var/_vector/direction //direction of the ray
var/original_damage //original damage of the ray when applicable
var/turf/final_turf
var/turf/previous_turf
@@ -29,7 +29,7 @@
return "\[Ray\](\n- origin = " + origin.toString() + "\n- origin_floored = "+ origin_floored.toString() + "\n- direction = " + direction.toString() + "\n- z-level = " + num2text(z) + "\n)"
//use atom2vector for the origin, atoms2vector for the direction
/ray/New(var/vector/p_origin, var/vector/p_direction, var/z)
/ray/New(var/_vector/p_origin, var/_vector/p_direction, var/z)
origin = p_origin
origin_floored = origin.floored() //to save us from calculating it all over again
direction = p_direction.chebyshev_normalized()
@@ -53,7 +53,7 @@
return hitsPoint(other_ray.origin)
//returns true if point is on our ray (can be called with a max distance)
/ray/proc/hitsPoint(var/vector/point, var/max_distance = 0)
/ray/proc/hitsPoint(var/_vector/point, var/max_distance = 0)
if(origin.equals(point)) //the easy way out
return TRUE
@@ -73,23 +73,23 @@
// wrong?
/ray/proc/getReboundOnAtom(var/rayCastHit/hit)
//calc where we hit the atom
var/vector/hit_point = hit.point_raw
var/_vector/hit_point = hit.point_raw
var/atom/movable/resolved_hit_atom = hit.hit_atom?.get()
var/vector/hit_atom_loc = atom2vector(resolved_hit_atom) + new /vector(0.5, 0.5)
var/_vector/hit_atom_loc = atom2vector(resolved_hit_atom) + new /_vector(0.5, 0.5)
var/vector/hit_vector = hit_point - hit_atom_loc
var/_vector/hit_vector = hit_point - hit_atom_loc
//we assume every atom is a octogonal, hence we use all_vectors
//here we calculate the "face" of the octagonal atom we want to rebound on
var/entry_byond_dir = vector2ClosestDir(hit_vector)
var/vector/entry_dir = dir2vector(entry_byond_dir)
var/_vector/entry_dir = dir2vector(entry_byond_dir)
return src.direction.mirrorWithNormal(entry_dir)
//gets a point along the ray
/ray/proc/getPoint(var/distance)
var/vector/path = direction * distance
var/_vector/path = direction * distance
return origin + path
//inherit and override this for costum logic
@@ -101,15 +101,15 @@
//returns list of raycasthits
/ray/proc/cast(var/max_distance = RAY_CAST_DEFAULT_MAX_DISTANCE, var/max_hits = RAY_CAST_UNLIMITED_HITS, var/ignore_origin = TRUE)
//calculating a step and its distance to use in the loop
var/vector/a_step = direction * RAY_CAST_STEP
var/_vector/a_step = direction * RAY_CAST_STEP
var/step_distance = a_step.chebyshev_norm()
//setting up our pointer and distance to track where we are
var/vector/pointer = new /vector(0,0)
var/_vector/pointer = new /_vector(0,0)
var/distance = 0
//positions list to easier check if we already found this position (since we are moving in tiny steps, not full numbers)
var/list/vector/positions = list()
var/list/_vector/positions = list()
//our result
var/list/rayCastHit/hits = list()
@@ -124,12 +124,12 @@
distance += step_distance
//calculating our current position in world space (its two lines cause byond)
var/vector/new_position_unfloored = origin + pointer
var/vector/new_position = new_position_unfloored.floored()
var/_vector/new_position_unfloored = origin + pointer
var/_vector/new_position = new_position_unfloored.floored()
//check if we already checked this (floored) vector
var/exists = FALSE
for(var/vector/V in positions)
for(var/_vector/V in positions)
if(V.equals(new_position))
exists = TRUE
if(exists)
@@ -191,14 +191,14 @@ var/list/ray_draw_icon_cache = list()
var/angle = direction.toAngle()
var/max_distance = draw_distance - distance_from_endpoint
while(distance_pointer < max_distance)
var/vector/point
var/_vector/point
if(distance_pointer > max_distance - step_size) //last loop
point = getPoint(max_distance - step_size)
else
point = getPoint(distance_pointer)
var/vector/point_floored = point.floored()
var/_vector/point_floored = point.floored()
var/vector/pixels = (point - point_floored - new /vector(0.5, 0.5)) * WORLD_ICON_SIZE
var/_vector/pixels = (point - point_floored - new /_vector(0.5, 0.5)) * WORLD_ICON_SIZE
var/turf/T = locate(point_floored.x, point_floored.y, z)

View File

@@ -1,8 +1,8 @@
/rayCastHit
var/ray/used_ray
var/datum/weakref/hit_atom
var/vector/point
var/vector/point_raw
var/_vector/point
var/_vector/point_raw
var/distance
var/hit_type // see defines in ray.dm

View File

@@ -1,11 +1,11 @@
/rayCastHitInfo
var/ray/used_ray
var/datum/weakref/hit_atom
var/vector/point
var/vector/point_raw
var/_vector/point
var/_vector/point_raw
var/distance
/rayCastHitInfo/New(var/ray/used_ray, var/datum/weakref/hit_atom, var/vector/point, var/vector/point_raw, var/distance)
/rayCastHitInfo/New(var/ray/used_ray, var/datum/weakref/hit_atom, var/_vector/point, var/_vector/point_raw, var/distance)
src.used_ray = used_ray
src.hit_atom = hit_atom
src.point = point

View File

@@ -1,7 +1,7 @@
/proc/raycast_test(var/x = 1, var/y = 1, var/dist)
var/vector/origin = new /vector(usr.x, usr.y)
var/vector/direction = new /vector(x, y)
var/_vector/origin = new /_vector(usr.x, usr.y)
var/_vector/direction = new /_vector(x, y)
var/ray/our_ray = new /ray(origin, direction, usr.z)
var/list/res = our_ray.cast(dist)

View File

@@ -1,50 +1,50 @@
// Basic geometry things.
/vector
/_vector
var/x = 0
var/y = 0
/vector/New(var/x, var/y)
/_vector/New(var/x, var/y)
src.x = x
src.y = y
/vector/proc/duplicate()
return new /vector(x, y)
/_vector/proc/duplicate()
return new /_vector(x, y)
/vector/proc/euclidian_norm()
/_vector/proc/euclidian_norm()
return sqrt(x*x + y*y)
/vector/proc/squared_norm()
/_vector/proc/squared_norm()
return x*x + y*y
/vector/proc/normalized()
/_vector/proc/normalized()
var/norm = euclidian_norm()
return new /vector(x/norm, y/norm)
return new /_vector(x/norm, y/norm)
/vector/proc/floored()
return new /vector(Floor(x), Floor(y))
/_vector/proc/floored()
return new /_vector(Floor(x), Floor(y))
//use this one
/vector/proc/chebyshev_norm()
/_vector/proc/chebyshev_norm()
return max(abs(x), abs(y))
//use this one
/vector/proc/chebyshev_normalized()
/_vector/proc/chebyshev_normalized()
var/norm = chebyshev_norm()
return new /vector(x/norm, y/norm)
return new /_vector(x/norm, y/norm)
/vector/proc/is_integer()
/_vector/proc/is_integer()
return IS_INT(x) && IS_INT(y)
/vector/proc/is_null()
/_vector/proc/is_null()
return chebyshev_norm() == 0
/vector/proc/toString()
/_vector/proc/toString()
return "\[Vector\]([x],[y])"
//returns angle from 0 to 360
//-1 if vector is (0,0)
//angle calculated on north
/vector/proc/toAngle()
/_vector/proc/toAngle()
if(x == 0)
if(y == 0)
return -1
@@ -58,30 +58,30 @@
else if(x < 0)
return 270
var/vector/src_norm = src.chebyshev_normalized()
var/_vector/src_norm = src.chebyshev_normalized()
var/angle = arctan(src_norm.y,src_norm.x) - 360 * -1 //this is broken
return (angle >= 360) ? angle - 360 : angle
/vector/proc/dot(var/vector/B)
/_vector/proc/dot(var/_vector/B)
return src.x * B.x + src.y * B.y
/vector/proc/mirrorWithNormal(var/vector/N)
var/vector/n_norm = N.normalized()
/_vector/proc/mirrorWithNormal(var/_vector/N)
var/_vector/n_norm = N.normalized()
return src - n_norm * ( 2 * ( src * n_norm ))
//operator overloading
/vector/proc/operator+(var/vector/B)
return new /vector(x + B.x, y + B.y)
/_vector/proc/operator+(var/_vector/B)
return new /_vector(x + B.x, y + B.y)
/vector/proc/operator-(var/vector/B)
return new /vector(x - B.x, y - B.y)
/_vector/proc/operator-(var/_vector/B)
return new /_vector(x - B.x, y - B.y)
/vector/proc/operator*(var/mult)
if(istype(mult, /vector))
/_vector/proc/operator*(var/mult)
if(istype(mult, /_vector))
return dot(mult)
return new /vector(x * mult, y * mult)
return new /_vector(x * mult, y * mult)
/vector/proc/equals(var/vector/vectorB)
/_vector/proc/equals(var/_vector/vectorB)
return (x == vectorB.x && y == vectorB.y)

View File

@@ -1,7 +1,7 @@
/atom/movable/proc/vector_translate(var/vector/V, var/delay)
/atom/movable/proc/vector_translate(var/_vector/V, var/delay)
var/turf/T = get_turf(src)
var/turf/destination = locate(T.x + V.x, T.y + V.y, z)
var/vector/V_norm = V.chebyshev_normalized()
var/_vector/V_norm = V.chebyshev_normalized()
if (!V_norm.is_integer())
return
var/turf/destination_temp
@@ -11,61 +11,61 @@
T = get_turf(src)
sleep(delay + world.tick_lag) // Shortest possible time to sleep
/atom/proc/get_translated_turf(var/vector/V)
/atom/proc/get_translated_turf(var/_vector/V)
var/turf/T = get_turf(src)
return locate(T.x + V.x, T.y + V.y, z)
//Vector representing world-pos of A
/proc/atom2vector(var/atom/A)
return new /vector(A.x, A.y)
return new /_vector(A.x, A.y)
//Vector from A -> B
/proc/atoms2vector(var/atom/A, var/atom/B)
return new /vector((B.x - A.x), (B.y - A.y))
return new /_vector((B.x - A.x), (B.y - A.y))
/proc/dir2vector(var/dir)
switch(dir)
if(NORTH)
return new /vector(0,1)
return new /_vector(0,1)
if(NORTHEAST)
return new /vector(1,1)
return new /_vector(1,1)
if(EAST)
return new /vector(1,0)
return new /_vector(1,0)
if(SOUTHEAST)
return new /vector(1,-1)
return new /_vector(1,-1)
if(SOUTH)
return new /vector(0,-1)
return new /_vector(0,-1)
if(SOUTHWEST)
return new /vector(-1,-1)
return new /_vector(-1,-1)
if(WEST)
return new /vector(-1,0)
return new /_vector(-1,0)
if(NORTHWEST)
return new /vector(-1,1)
return new /_vector(-1,1)
//defaults to north
/proc/vector2ClosestDir(var/vector/V)
var/vector/V_norm = V.chebyshev_normalized()
/proc/vector2ClosestDir(var/_vector/V)
var/_vector/V_norm = V.chebyshev_normalized()
var/smallest_dist = 2 //since all vectors are normalized, the biggest possible distance is 2
var/closestDir = NORTH
for(var/d in alldirs)
var/vector/dir = dir2vector(d)
var/vector/delta = dir.chebyshev_normalized() - V_norm
var/_vector/dir = dir2vector(d)
var/_vector/delta = dir.chebyshev_normalized() - V_norm
var/dist = delta.chebyshev_norm()
if(dist < smallest_dist)
smallest_dist = dist
closestDir = d
return closestDir
/proc/drawLaser(var/vector/A, var/vector/B, var/icon='icons/obj/projectiles.dmi', var/icon_state = "laser")
var/vector/delta = (B - A)
/proc/drawLaser(var/_vector/A, var/_vector/B, var/icon='icons/obj/projectiles.dmi', var/icon_state = "laser")
var/_vector/delta = (B - A)
var/ray/laser_ray = new /ray(A, delta)
var/distance = delta.chebyshev_norm()
laser_ray.draw(distance, icon, icon_state)
/proc/vector2turf(var/vector/V, var/z)
/proc/vector2turf(var/_vector/V, var/z)
var/turf/T = locate(V.x, V.y, z)
return T

View File

@@ -2,6 +2,8 @@
#define UTF_LIMIT (1 << 20) + (1 << 16) - 1
#define string2charlist(string) (splittext(string, regex("(.)")) - splittext(string, ""))
#define PREVENT_CHARACTER_TRIM_LOSS(length) (length + 1)
/*
* Holds procs designed to help with filtering text
* Contains groups:
@@ -128,6 +130,15 @@ var/list/whitelist_name_diacritics_min = list(
"à", "á", "â", "ã", "ä", "ä", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "ö", "ø", "ù", "ú", "û", "ü", "ý",
)
/proc/stripped_multiline_input(mob/user, message = "", title = "", default = "", max_length=MAX_MESSAGE_LEN, no_trim=FALSE)
var/user_input = input(user, message, title, default) as message|null
if(isnull(user_input)) // User pressed cancel
return
if(no_trim)
return copytext_char(html_encode(user_input), 1, max_length)
else
return trim(html_encode(user_input), max_length)
/proc/reject_bad_name(var/t_in, var/allow_numbers=0, var/max_length=MAX_NAME_LEN)
if(!t_in || length(t_in) > max_length)
return //Rejects the input if it is null or if it is longer then the max length allowed

View File

@@ -8,6 +8,11 @@
return x!=0?x/abs(x):0
#endif
/// Return html to load a url.
/// for use inside of browse() calls to html assets that might be loaded on a cdn.
/proc/url2htmlloader(url)
return {"<html><head><meta http-equiv="refresh" content="0;URL='[url]'"/></head><body onLoad="parent.location='[url]'"></body></html>"}
/proc/getline(atom/M,atom/N)//Ultra-Fast Bresenham Line-Drawing Algorithm
var/px=M.x //starting x
var/py=M.y

View File

@@ -97,6 +97,9 @@
if(modifiers["right"])
RightClickOn(A)
return
// Equivalent to button M4/M5 on gaming mouses.
if(modifiers["xbutton1"] || modifiers["xbutton2"])
return
if(attempt_crawling(A))
return

View File

@@ -218,6 +218,20 @@
var/rsclist = ""
var/rscstring = ""
// TGUI & tg asset thing
var/tgui_max_chunk_count = 32
var/tg_asset_transport = "simple" // simple or "webroot". Webroot is via CDN.
var/cache_assets = 1 // Disabled during dev, enabled during prod
var/smart_cache_assets = 1
var/save_spritesheets = 0 // Disabled by default.
var/asset_simple_preload = 0 // Disabled by default
// tg asset cdn via webroot systme. Currently unused.
var/asset_cdn_webroot = ""
var/asset_cdn_url = ""
/datum/configuration/New()
. = ..()
var/list/L = subtypesof(/datum/gamemode)-/datum/gamemode/cult

View File

@@ -0,0 +1,42 @@
var/datum/subsystem/assets/SSassets
/datum/subsystem/assets
name = "Assets"
flags = SS_NO_FIRE
var/list/datum/asset_cache_item/cache = list()
var/list/preload = list()
var/datum/asset_transport/transport = new()
/datum/subsystem/assets/New()
NEW_SS_GLOBAL(SSassets)
/datum/subsystem/assets/Initialize()
// -- FIXME ASSETS:.
// -- Old, crusty /vg/ style of populating assets (relying on 'global cache.dm')
// TOFIX asap!!! Having two concurrent, parralel & independant asset delivery system is BAD
populate_asset_cache()
var/newtransporttype = /datum/asset_transport
switch (config.tg_asset_transport)
if ("webroot")
newtransporttype = /datum/asset_transport/webroot
if (newtransporttype == transport.type)
return
var/datum/asset_transport/newtransport = new newtransporttype ()
if (newtransport.validate_config())
transport = newtransport
transport.Load()
for(var/type in typesof(/datum/tg_asset))
var/datum/tg_asset/A = type
if (type != initial(A._abstract))
load_asset_datum(type)
transport.Initialize(cache)
/datum/subsystem/assets/Recover()
cache = SSassets.cache
preload = SSassets.preload

View File

@@ -1,16 +0,0 @@
var/datum/subsystem/assets/SSassets
/datum/subsystem/assets
name = "Asset Cache"
init_order = SS_INIT_ASSETS
flags = SS_NO_FIRE
/datum/subsystem/assets/New()
NEW_SS_GLOBAL(SSassets)
/datum/subsystem/assets/Initialize(timeofday)
populate_asset_cache()
..()

View File

@@ -232,37 +232,37 @@ var/datum/subsystem/persistence_misc/SSpersistence_misc
log_debug("[name] task found an empty file on [file_path]")
return
for(var/list/L in to_read)
var/datum/record/money/record = new(L["ckey"], L["role"], L["cash"], L["shift_duration"], L["date"])
var/datum/data/record/money/record = new(L["ckey"], L["role"], L["cash"], L["shift_duration"], L["date"])
data += record
/datum/persistence_task/highscores/on_shutdown()
var/list/L = list()
for(var/datum/record/money/record in data)
for(var/datum/data/record/money/record in data)
L += list(record.vars)
write_file(L)
/datum/persistence_task/highscores/proc/insert_records(list/records)
data += records
cmp_field = "cash"
sortTim(data, /proc/cmp_list_by_element_desc)
global.cmp_field = "cash"
sortTim(data, /proc/cmp_records_numerically)
if (data.len > 5)
data.Cut(6) // we only store the top 5
for(var/datum/record/money/record in data)
for(var/datum/data/record/money/record in data)
if(record in records)
if(data[1] == record)
announce_new_highest_record(record)
else
announce_new_record(record)
/datum/persistence_task/highscores/proc/announce_new_highest_record(var/datum/record/money/record)
/datum/persistence_task/highscores/proc/announce_new_highest_record(var/datum/data/record/money/record)
var/name = "Richest escape ever"
var/desc = "You broke the record of the richest escape! $[record.cash] chips accumulated."
give_award(record.ckey, /obj/item/weapon/reagent_containers/food/drinks/golden_cup, name, desc)
var/desc = "You broke the record of the richest escape! $[record.fields["cash"]] chips accumulated."
give_award(record.fields["ckey"], /obj/item/weapon/reagent_containers/food/drinks/golden_cup, name, desc)
/datum/persistence_task/highscores/proc/announce_new_record(var/datum/record/money/record)
/datum/persistence_task/highscores/proc/announce_new_record(var/datum/data/record/money/record)
var/name = "Good rich escape"
var/desc = "You made it to the top 5! You accumulated $[record.cash]."
give_award(record.ckey, /obj/item/clothing/accessory/medal/gold, name, desc, FALSE)
var/desc = "You made it to the top 5! You accumulated $[record.fields["cash"]]."
give_award(record.fields["ckey"], /obj/item/clothing/accessory/medal/gold, name, desc, FALSE)
/datum/persistence_task/highscores/proc/clear_records()
data = list()
@@ -273,15 +273,15 @@ var/datum/subsystem/persistence_misc/SSpersistence_misc
name = "Trader shoal highscores"
file_path = "data/persistence/trader_highscores.json"
/datum/persistence_task/highscores/trader/announce_new_highest_record(var/datum/record/money/record)
/datum/persistence_task/highscores/trader/announce_new_highest_record(var/datum/data/record/money/record)
var/name = "Richest shoal haul ever"
var/desc = "You broke the record of the richest shoal haul! $[record.cash] chips accumulated."
give_award(record.ckey, /obj/item/weapon/reagent_containers/food/drinks/golden_cup, name, desc)
var/desc = "You broke the record of the richest shoal haul! $[record.fields["cash"]] chips accumulated."
give_award(record.fields["ckey"], /obj/item/weapon/reagent_containers/food/drinks/golden_cup, name, desc)
/datum/persistence_task/highscores/trader/announce_new_record(var/datum/record/money/record)
/datum/persistence_task/highscores/trader/announce_new_record(var/datum/data/record/money/record)
var/name = "Good rich shoal haul"
var/desc = "You made it to the top 5! You accumulated $[record.cash]."
give_award(record.ckey, /obj/item/clothing/accessory/medal/gold, name, desc, FALSE)
var/desc = "You made it to the top 5! You accumulated $[record.fields["cash"]]."
give_award(record.fields["ckey"], /obj/item/clothing/accessory/medal/gold, name, desc, FALSE)
//stores map votes for code/modules/html_interface/voting/voting.dm
/datum/persistence_task/vote

View File

@@ -13,8 +13,10 @@ var/datum/subsystem/tgui/SStgui
/// A list of UIs scheduled to process
var/list/current_run = list()
/// A list of open UIs
var/list/open_uis = list()
/// A list of all UIs
var/list/all_uis = list()
/// All curent open UIs
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.
@@ -25,17 +27,30 @@ var/datum/subsystem/tgui/SStgui
/datum/subsystem/tgui/Initialize(timeofday)
basehtml = file2text('tgui/public/tgui.html')
..()
// Inject inline helper functions
var/helpers = file2text('tgui/public/helpers.min.js')
helpers = "<script type='text/javascript'>\n[helpers]\n</script>"
basehtml = replacetextEx(basehtml, "<!-- tgui:helpers -->", helpers)
// Inject inline ntos-error styles
var/ntos_error = file2text('tgui/public/ntos-error.min.css')
ntos_error = "<style type='text/css'>\n[ntos_error]\n</style>"
basehtml = replacetextEx(basehtml, "<!-- tgui:ntos-error -->", ntos_error)
basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "Nanotrasen (c) 2525-[game_year]")
/datum/subsystem/tgui/Shutdown()
close_all_uis()
/datum/subsystem/tgui/stat_entry()
..("P:[length(open_uis)]")
/datum/subsystem/tgui/stat_entry(msg)
msg = "P:[length(all_uis)]"
return ..()
/datum/subsystem/tgui/fire(resumed = FALSE)
if(!resumed)
src.current_run = open_uis.Copy()
src.current_run = all_uis.Copy()
// Cache for sanic speed (lists are references anyways)
var/list/current_run = src.current_run
while(current_run.len)
@@ -45,7 +60,7 @@ var/datum/subsystem/tgui/SStgui
if(ui?.user && ui.src_object)
ui.process(wait * 0.1)
else
open_uis.Remove(ui)
ui.close(0)
if(MC_TICK_CHECK)
return
@@ -56,7 +71,7 @@ var/datum/subsystem/tgui/SStgui
* Returns null if pool was exhausted.
*
* required user mob
* return datum/tgui
* return datum/tgui_window
*/
/datum/subsystem/tgui/proc/request_pooled_window(mob/user)
if(!user.client)
@@ -116,8 +131,6 @@ var/datum/subsystem/tgui/SStgui
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]")
@@ -164,11 +177,10 @@ var/datum/subsystem/tgui/SStgui
* return datum/tgui The found UI.
*/
/datum/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))
if(!(src_object?.open_uis?.len))
return null
for(var/datum/tgui/ui in open_uis_by_src[key])
for(var/datum/tgui/ui in src_object.open_uis)
// Make sure we have the right user
if(ui.user == user)
return ui
@@ -184,15 +196,14 @@ var/datum/subsystem/tgui/SStgui
* return int The number of UIs updated.
*/
/datum/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])
if(!(src_object?.open_uis?.len))
return 0
var/count = 0
for(var/datum/tgui/ui in src_object.open_uis)
// Check if UI is valid.
if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user))
ui.process(wait * 0.1, force = 1)
ui.process(wait * 0.1, TRUE)
count++
return count
@@ -206,12 +217,11 @@ var/datum/subsystem/tgui/SStgui
* return int The number of UIs closed.
*/
/datum/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])
if(!(src_object?.open_uis?.len))
return 0
var/count = 0
for(var/datum/tgui/ui in src_object.open_uis)
// Check if UI is valid.
if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user))
ui.close()
@@ -227,8 +237,7 @@ var/datum/subsystem/tgui/SStgui
*/
/datum/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])
for(var/datum/tgui/ui in all_uis)
// Check if UI is valid.
if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user))
ui.close()
@@ -288,8 +297,16 @@ var/datum/subsystem/tgui/SStgui
open_uis_by_src[key] = list()
ui.user.tgui_open_uis |= ui
var/list/uis = open_uis_by_src[key]
// this feels a bit silly
if (ui.src_object.open_uis)
ui.src_object.open_uis += ui
else
ui.src_object.open_uis = list(ui)
uis |= ui
open_uis |= ui
all_uis |= ui
/**
* private
@@ -301,18 +318,24 @@ var/datum/subsystem/tgui/SStgui
* return bool If the UI was removed or not.
*/
/datum/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)
all_uis -= ui
open_uis -= ui
var/key = "[ref(ui.src_object)]"
open_uis_by_src[key] -= ui
if (!length(open_uis_by_src[key]))
open_uis_by_src -= key
current_run -= 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)
ui.user.tgui_open_uis -= ui
// basically a lazy remove
if(ui.src_object.open_uis)
ui.src_object.open_uis -= ui
if (!length(ui.src_object.open_uis))
ui.src_object.open_uis = null
return TRUE
/**
@@ -347,7 +370,7 @@ var/datum/subsystem/tgui/SStgui
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)
target.tgui_open_uis += ui
// Clear the old list.
source.tgui_open_uis.Cut()
return TRUE

View File

@@ -60,7 +60,7 @@ var/timer_id = 1
// Optimization or waste of time?
// If a datum has many timers it may be beneficial to check whether thing_to_call.gcDestroyed has been set,
// indicating there's no need to remove this timer from its active_timers list, as the list will be nulled right afterwards.
if(thing_to_call != GLOBAL_PROC && !thing_to_call.gcDestroyed)
if(thing_to_call != GLOBAL_PROC && (isclient(thing_to_call) || !thing_to_call.gcDestroyed))
thing_to_call.active_timers -= src
if(!thing_to_call.active_timers.len)
thing_to_call.active_timers = null

View File

@@ -71,7 +71,7 @@
for(var/obj/item/weapon/spacecash/C2 in get_contents_in_object(player, /obj/item/weapon/spacecash))
cashscore += (C2.amount * C2.worth)
var/datum/record/money/record = new(player.key, player.job, cashscore)
var/datum/data/record/money/record = new(player.key, player.job, cashscore)
rich_escapes += record
if(cashscore > score.richestcash)
@@ -91,7 +91,7 @@
if(TR.source_name == player.real_name)
shoal_amount += text2num(TR.amount)
if(shoal_amount > 0)
var/datum/record/money/record = new(player.key, player.job, shoal_amount)
var/datum/data/record/money/record = new(player.key, player.job, shoal_amount)
rich_shoals += record
if(shoal_amount > score.biggestshoalcash)
score.biggestshoalcash = shoal_amount

View File

@@ -312,27 +312,27 @@ var/global/datum/controller/gameticker/scoreboard/score = new()
var/datum/persistence_task/highscores/leaderboard = score.money_leaderboard
dat += "<b>MONTHLY TOP 5 RICHEST ESCAPEES:</b><br>"
var/i = 1
for(var/datum/record/money/entry in leaderboard.data)
var/cash = num2text(entry.cash, 12)
var/list/split_date = splittext(entry.date, "-")
for(var/datum/data/record/money/entry in leaderboard.data)
var/cash = num2text(entry.fields["cash"], 12)
var/list/split_date = splittext(entry.fields["date"], "-")
if(text2num(split_date[2]) != text2num(time2text(world.timeofday, "MM")))
leaderboard.clear_records()
dat += "No rich escapees yet!"
break
else
dat += "[i++]) <b>$[cash]</b> by <b>[entry.ckey]</b> ([entry.role]). That shift lasted [entry.shift_duration]. Date: [entry.date]<br>"
dat += "[i++]) <b>$[cash]</b> by <b>[entry.fields["ckey"]]</b> ([entry.fields["role"]]). That shift lasted [entry.fields["shift_duration"]]. Date: [entry.fields["date"]]<br>"
var/datum/persistence_task/highscores/trader/leaderboard2 = score.shoal_leaderboard
dat += "<br><b>MONTHLY TOP 5 RICHEST TRADERS:</b><br>"
i = 1
for(var/datum/record/money/entry in leaderboard2.data)
var/cash = num2text(entry.cash, 12)
var/list/split_date = splittext(entry.date, "-")
for(var/datum/data/record/money/entry in leaderboard2.data)
var/cash = num2text(entry.fields["cash"], 12)
var/list/split_date = splittext(entry.fields["date"], "-")
if(text2num(split_date[2]) != text2num(time2text(world.timeofday, "MM")))
leaderboard2.clear_records()
dat += "No rich traders yet!"
break
else
dat += "[i++]) <b>$[cash]</b> by <b>[entry.ckey]</b>. That shift lasted [entry.shift_duration]. Date: [entry.date]<br>"
dat += "[i++]) <b>$[cash]</b> by <b>[entry.fields["ckey"]]</b>. That shift lasted [entry.fields["shift_duration"]]. Date: [entry.fields["date"]]<br>"
return dat
/mob/proc/display_round_end_scoreboard()

View File

@@ -1,13 +1,15 @@
/datum/record/money
var/ckey
var/role
var/cash
var/shift_duration
var/date
/datum/data/record/money
fields = list(
"ckey" = "",
"role" = "",
"cash" = -999999,
"shift_duration" = -999999,
"date" = "0000-00-00",
)
/datum/record/money/New(ckey, role, cash, shift_duration = worldtime2text(), date = SQLtime())
src.ckey = ckey
src.role = role
src.cash = cash
src.shift_duration = shift_duration
src.date = date
/datum/data/record/money/New(ckey, role, cash, shift_duration = worldtime2text(), date = SQLtime())
fields["ckey"] = ckey
fields["role"] = role
fields["cash"] = cash
fields["shift_duration"]= shift_duration
fields["date"] = date

View File

@@ -132,7 +132,6 @@ var/list/obj/machinery/camera/cyborg_cams = list(
/obj/machinery/computer/security/ui_static_data()
var/list/data = list()
data["title"] = name
data["mapRef"] = map_name
var/list/cameras = get_available_cameras()
data["cameras"] = list()
@@ -140,17 +139,20 @@ var/list/obj/machinery/camera/cyborg_cams = list(
var/obj/machinery/camera/C = cameras[i]
data["cameras"] += list(list(
name = C.c_tag,
ref = ref(C),
))
return data
/obj/machinery/computer/security/ui_act(action, params)
. = ..()
message_admins("Active camera change")
if(.)
return
if(action == "switch_camera")
var/c_tag = params["name"]
message_admins("Act = switch_camera, [c_tag]")
var/list/cameras = get_available_cameras()
var/obj/machinery/camera/selected_camera = cameras[c_tag]
active_camera = selected_camera

View File

@@ -176,7 +176,7 @@ var/list/message_monitors = list()
var/imgdat = ""
if(pda.img_sent)
user << browse_rsc(pda.img_sent, "tmp_photo_[index].png")
imgdat = "<img src='tmp_photo_[index].png' width = '192' style='-ms-interpolation-mode:nearest-neighbor'>"
imgdat = "<img src='tmp_photo_[index].png' width = '192' style='image-rendering: pixelated'>"
dat += "<tr><td width = '5%'><center><A href='?src=\ref[src];delete=\ref[pda]' style='color: rgb(255,0,0)'>X</a></center></td><td width='15%'>[pda.sender]</td><td width='15%'>[pda.recipient]</td><td width='300px'>[pda.message]</td><th width='30%'>[imgdat]</th></tr>"
dat += "</table>"
//Hacking screen.

View File

@@ -71,26 +71,28 @@
if(!can_control(R,user))
continue
var/list/cyborg_data = list(
name = R.name,
locked_down = R.lockdown,
status = R.stat,
charge = R.cell ? R.cell.percent() : null,
module = R.module ? "[R.modtype] Module" : "No Module Installed",
master = R.connected_ai,
emagged = R.emagged,
borgimage = iconsouth2base64(getFlatIconDeluxe(sort_image_datas(get_content_image_datas(R)), override_dir = SOUTH)),
ref = ref(R)
"name" = R.name,
"locked_down" = R.lockdown,
"status" = R.stat,
"charge" = R.cell ? R.cell.percent() : null,
"module" = R.module ? "[R.modtype] Module" : "No Module Installed",
"master" = R.connected_ai,
"emagged" = R.emagged,
"borgimage" = iconsouth2base64(getFlatIconDeluxe(sort_image_datas(get_content_image_datas(R)), override_dir = SOUTH)),
"ref" = ref(R)
)
data["cyborgs"] += list(cyborg_data)
return data
/obj/machinery/computer/robotics/ui_act(action, params)
/obj/machinery/computer/robotics/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
message_admins("ui_act called on robotborg")
. = ..()
if(.)
message_admins("uuuh didn't work :)")
return
message_admins("action: [action], params: [json_encode(params)]")
switch(action)
if("killbot")
if(allowed(usr))
@@ -106,7 +108,7 @@
to_chat(R.connected_ai, "<span style=\"font-family:Courier\"><b>\[<span class='danger'>ALERT</span>\] Slaved Cyborg [R.name] detonated. Signal traced to [get_area(src).name].</b></span>")
R.connected_ai << 'sound/machines/twobeep.ogg'
R.self_destruct()
SStgui.try_update_ui(ui.user, src, ui)
else
to_chat(usr, "<span class='warning'>Access Denied.</span>")
@@ -128,6 +130,7 @@
else
to_chat(R.connected_ai, "<span style=\"font-family:Courier\"><b>\[<span class='notice'>INFO</span>\] The lockdown on cyborg [R.name] has been lifted. Signal traced to [get_area(src).name]</b></span>")
R.connected_ai << 'sound/misc/notice2.ogg'
SStgui.try_update_ui(ui.user, src, ui)
else
to_chat(usr, "<span class='warning'>Access Denied.</span>")
if("hack")
@@ -137,6 +140,7 @@
log_game("[key_name(usr)] emagged [key_name(R)] using a robotics console!")
message_admins("[key_name(usr)] emagged [key_name(R)] using a robotics console!")
R.SetEmagged(TRUE)
SStgui.try_update_ui(ui.user, src, ui)
if(can_control(R, usr) && ismalf(usr))
if (!hacking)
hacking = 1
@@ -148,6 +152,7 @@
to_chat(usr, "<span style=\"font-family:Courier\"><b>\[<span class='danger'>ERROR</span>\] [R.name]: SAFETIES DISABLED.</b></span>")
usr << 'sound/misc/notice2.ogg'
hacking = 0
SStgui.try_update_ui(ui.user, src, ui)
else
to_chat(usr, "You are already hacking a cyborg.")
if("sequence")
@@ -159,6 +164,7 @@
stop_sequence()
message_admins("<span class='notice'>[key_name_admin(usr)] [formatJumpTo(usr)] has halted the global cyborg killswitch!</span>")
log_game("<span class='notice'>[key_name(usr)] has halted the global cyborg killswitch!</span>")
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
/obj/machinery/computer/robotics/proc/can_control(var/mob/living/silicon/robot/robot, var/mob/controller)

View File

@@ -432,7 +432,7 @@ var/list/obj/machinery/newscaster/allCasters = list() //Global list that will co
if(MESSAGE.img)
usr << browse_rsc(MESSAGE.img, "tmp_photo[i].png")
dat+="<BR><a href='?src=\ref[src];show_photo_info=\ref[MESSAGE]'><img src='tmp_photo[i].png' width = '192' style='-ms-interpolation-mode:nearest-neighbor'></a><BR>"
dat+="<BR><a href='?src=\ref[src];show_photo_info=\ref[MESSAGE]'><img src='tmp_photo[i].png' width = '192' style='image-rendering: pixelated'></a><BR>"
dat+="<BR><FONT SIZE=1>\[Story by <FONT COLOR='maroon'>[MESSAGE.author]</FONT>\]</FONT><BR><HR>"
dat += {"<A href='?src=\ref[src];refresh=1'>Refresh</A>

View File

@@ -33,23 +33,24 @@
var/area/A = get_area(M)
if(!M)
continue
var/icon/mech_icon = getFlatIconDeluxe(sort_image_datas(get_content_image_datas(M)), override_dir = SOUTH)
var/list/mecha_data = list(
name = M.name,
health = round((M.health/initial(M.health))*100),
charge = M.cell ? round(M.cell.percent()) : null,
pilot = M.occupant || "None",
location = A.name || "Unknown",
active = M.selected ? M.selected.name : "None",
status = M.state,
mechaimage = iconsouth2base64(getFlatIconDeluxe(sort_image_datas(get_content_image_datas(M)), override_dir = SOUTH)),
log = TR.get_mecha_log(),
ref = ref(M)
"name" = M.name,
"health" = round((M.health/initial(M.health))*100),
"charge" = M.cell ? round(M.cell.percent()) : null,
"pilot" = M.occupant || "None",
"location" = A.name || "Unknown",
"active" = M.selected ? M.selected.name : "None",
"status" = M.state,
"mechaimage" = iconsouth2base64(mech_icon),
"log" = TR.get_mecha_log(),
"ref" = ref(M)
)
data["mechas"] += list(mecha_data)
return data
/obj/machinery/computer/mecha/ui_act(action, params)
/obj/machinery/computer/mecha/ui_act(action, list/params)
. = ..()
if(.)
return

View File

@@ -557,7 +557,7 @@
var/icon/img = app.imglist[note]
if(img)
usr << browse_rsc(img, "tmp_photo_[note].png")
dat += "<img src='tmp_photo_[note].png' width = '192' style='-ms-interpolation-mode:nearest-neighbor'><BR>"
dat += "<img src='tmp_photo_[note].png' width = '192' style='image-rendering: pixelated'><BR>"
dat += "</body></html>"
usr << browse(HTML_SKELETON(dat), "window=log;size=400x444;border=1;can_resize=1;can_close=1;can_minimize=0")
@@ -576,7 +576,7 @@
var/icon/img = app.imglist[note]
if(img)
usr << browse_rsc(img, "tmp_photo_[note].png")
dat += "<img src='tmp_photo_[note].png' width = '192' style='-ms-interpolation-mode:nearest-neighbor'><BR>"
dat += "<img src='tmp_photo_[note].png' width = '192' style='image-rendering: pixelated'><BR>"
dat += "</body></html>"
usr << browse(HTML_SKELETON(dat), "window=log;size=400x444;border=1;can_resize=1;can_close=1;can_minimize=0")
else

View File

@@ -55,7 +55,7 @@
var/icon/img = imglist[note]
if(img)
user << browse_rsc(ImagePDA(img), "tmp_photo_[note].png")
dat += "<img src='tmp_photo_[note].png' width = '192' style='-ms-interpolation-mode:nearest-neighbor'><BR>"
dat += "<img src='tmp_photo_[note].png' width = '192' style='image-rendering: pixelated'><BR>"
dat += "<br>"
return dat

View File

@@ -250,7 +250,7 @@
if(7)
displaylength = 448
dat += {"<div style='float: left'> <img src='tmp_photo_gallery_[i].png' width='[displaylength]' style='-ms-interpolation-mode:nearest-neighbor' /> </div>"}
dat += {"<div style='float: left'> <img src='tmp_photo_gallery_[i].png' width='[displaylength]' style='image-rendering: pixelated' /> </div>"}
i++
return dat

View File

@@ -199,6 +199,7 @@
transmitting = TRUE
set_light(1)
update_icon()
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
if("turn_off")
if(emped || !transmitting || !Adjacent(usr) || usr.incapacitated())
@@ -206,13 +207,16 @@
transmitting = FALSE
set_light(0)
update_icon()
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
if("toggle_refresh")
autorefreshing = !autorefreshing
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
if("toggle_injury")
if(fullmode)
injuryonly = !injuryonly
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
else
return FALSE

View File

@@ -19,7 +19,6 @@
feedback_add_details("admin_verb","DG2") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/* 21st sept 2010
Updated by Skie -- Still not perfect but better!
Stuff you can't do:
@@ -37,7 +36,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
return
spawn(0)
var/target = null
var/datum/target = null
var/targetselected = 0
var/lst[] // List reference
lst = new/list() // Make the list

View File

@@ -0,0 +1,53 @@
// Client tg_asset things
/client
var/last_completed_asset_job
var/list/sent_assets = list()
// Connection time
var/connection_time
/// Process asset cache client topic calls for `"asset_cache_confirm_arrival=[INT]"`
/client/proc/asset_cache_confirm_arrival(job_id)
var/asset_cache_job = round(text2num(job_id))
//because we skip the limiter, we have to make sure this is a valid arrival and not somebody tricking us into letting them append to a list without limit.
if (asset_cache_job > 0 && asset_cache_job <= last_asset_job && !(completed_asset_jobs["[asset_cache_job]"]))
completed_asset_jobs["[asset_cache_job]"] = TRUE
last_completed_asset_job = max(last_completed_asset_job, asset_cache_job)
else
return asset_cache_job || TRUE
/// Process asset cache client topic calls for `"asset_cache_preload_data=[HTML+JSON_STRING]"`
/client/proc/asset_cache_preload_data(data)
var/json = data
var/list/preloaded_assets = json_decode(json)
for (var/preloaded_asset in preloaded_assets)
if (copytext(preloaded_asset, findlasttext(preloaded_asset, ".")+1) in list("js", "jsm", "htm", "html"))
preloaded_assets -= preloaded_asset
continue
sent_assets |= preloaded_assets
/// Updates the client side stored json file used to keep track of what assets the client has between restarts/reconnects.
/client/proc/asset_cache_update_json()
if (world.time - connection_time < 10 SECONDS) //don't override the existing data file on a new connection
return
src << browse(json_encode(sent_assets), "file=asset_data.json&display=0")
/// Blocks until all currently sending browse and browse_rsc assets have been sent.
/// Due to byond limitations, this proc will sleep for 1 client round trip even if the client has no pending asset sends.
/// This proc will return an untrue value if it had to return before confirming the send, such as timeout or the client going away.
/client/proc/tg_browse_queue_flush(timeout = 50)
var/job = ++last_asset_job
var/t = 0
var/timeout_time = timeout
src << browse({"<script>window.location.href='byond://?asset_cache_confirm_arrival=[job]'</script>"}, "window=asset_cache_browser&file=asset_cache_send_verify.htm")
while(!completed_asset_jobs["[job]"] && t < timeout_time) // Reception is handled in Topic()
stoplag(1) // Lock up the caller until this is received.
t++
if (t < timeout_time)
return TRUE

View File

@@ -0,0 +1,51 @@
/**
* # asset_cache_item
*
* An internal datum containing info on items in the asset cache. Mainly used to cache md5 info for speed.
*/
/datum/asset_cache_item
/// the name of this asset item, becomes the key in SSassets.cache list
var/name
/// md5() of the file this asset item represents.
var/hash
/// the file this asset represents
var/resource
/// our file extension e.g. .png, .gif, etc
var/ext = ""
/// Should this file also be sent via the legacy browse_rsc system
/// when cdn transports are enabled?
var/legacy = FALSE
/// Used by the cdn system to keep legacy css assets with their parent
/// css file. (css files resolve urls relative to the css file, so the
/// legacy system can't be used if the css file itself could go out over
/// the cdn)
var/namespace = null
/// True if this is the parent css or html file for an asset's namespace
var/namespace_parent = FALSE
/// TRUE for keeping local asset names when browse_rsc backend is used
var/keep_local_name = FALSE
///pass in a valid file_hash if you have one to save it from needing to do it again.
///pass in a valid dmi file path string e.g. "icons/path/to/dmi_file.dmi" to make generating the hash less expensive
/datum/asset_cache_item/New(name, file, file_hash, dmi_file_path)
if (!isfile(file))
file = fcopy_rsc(file)
hash = file_hash
//the given file is directly from a dmi file and is thus in the rsc already, we know that its file_hash will be correct
if(!hash)
if(dmi_file_path)
hash = md5(file)
else
hash = md5asfile(file) //icons sent to the rsc md5 incorrectly when theyre given incorrect data
if (!hash)
CRASH("invalid asset sent to asset cache")
src.name = name
var/extstart = findlasttext(name, ".")
if (extstart)
ext = ".[copytext(name, extstart+1)]"
resource = file
/datum/asset_cache_item/variable_edited(variable_name, old_value, new_value)
return FALSE

View File

@@ -0,0 +1,225 @@
//These datums are used to populate the asset cache, the proc "register()" does this.
//Place any asset datums you create in asset_list_items.dm
//all of our asset datums, used for referring to these later
var/list/tg_asset_datums = list()
//get an assetdatum or make a new one
//does NOT ensure it's filled, if you want that use get_tg_asset_datum()
/proc/load_asset_datum(type)
return global.tg_asset_datums[type] || new type()
/proc/get_tg_asset_datum(type)
var/datum/tg_asset/loaded_asset = global.tg_asset_datums[type] || new type()
return loaded_asset.ensure_ready()
/datum/tg_asset
var/_abstract = /datum/tg_asset
var/cached_serialized_url_mappings
var/cached_serialized_url_mappings_transport_type
/// Whether or not this asset should be loaded in the "early assets" SS
var/early = FALSE
/// Whether or not this asset can be cached across rounds of the same commit under the `CACHE_ASSETS` config.
/// This is not a *guarantee* the asset will be cached. Not all asset subtypes respect this field, and the
/// config can, of course, be disabled.
/// Disable this if your asset can change between rounds on the same exact version of the code.
var/cross_round_cachable = FALSE
/datum/tg_asset/New()
global.tg_asset_datums[type] = src
register()
/// Stub that allows us to react to something trying to get us
/// Not useful here, more handy for sprite sheets
/datum/tg_asset/proc/ensure_ready()
return src
/// Stub to hook into if your asset is having its generation queued by SSasset_loading
/datum/tg_asset/proc/queued_generation()
CRASH("[type] inserted into SSasset_loading despite not implementing /proc/queued_generation")
/datum/tg_asset/proc/get_url_mappings()
return list()
/// Returns a cached tgui message of URL mappings
/datum/tg_asset/proc/get_serialized_url_mappings()
if (isnull(cached_serialized_url_mappings) || cached_serialized_url_mappings_transport_type != SSassets.transport.type)
cached_serialized_url_mappings = TGUI_CREATE_MESSAGE("asset/mappings", get_url_mappings())
cached_serialized_url_mappings_transport_type = SSassets.transport.type
return cached_serialized_url_mappings
/datum/tg_asset/proc/register()
return
/datum/tg_asset/proc/send(client)
return
/// Returns whether or not the asset should attempt to read from cache
/datum/tg_asset/proc/should_refresh()
return !cross_round_cachable || !config.cache_assets
/// Immediately regenerate the asset, overwriting any cache.
/datum/tg_asset/proc/regenerate()
unregister()
cached_serialized_url_mappings = null
cached_serialized_url_mappings_transport_type = null
register()
/// Unregisters any assets from the transport.
/datum/tg_asset/proc/unregister()
CRASH("unregister() not implemented for asset [type]!")
/// Simply takes any generated file and saves it to the round-specific /logs folder. Useful for debugging potential issues with spritesheet generation/display.
/// Only called when the SAVE_SPRITESHEETS config option is uncommented.
/datum/tg_asset/proc/save_to_logs(file_name, file_location)
var/asset_path = "data/logs/[date_string]/generated_assets/[file_name]"
fdel(asset_path) // just in case, sadly we can't use rust_g stuff here.
fcopy(file_location, asset_path)
/// If you don't need anything complicated.
/datum/tg_asset/simple
_abstract = /datum/tg_asset/simple
/// list of assets for this datum in the form of:
/// asset_filename = asset_file. At runtime the asset_file will be
/// converted into a asset_cache datum.
var/assets = list()
/// Set to true to have this asset also be sent via the legacy browse_rsc
/// system when cdn transports are enabled?
var/legacy = FALSE
/// TRUE for keeping local asset names when browse_rsc backend is used
var/keep_local_name = FALSE
/datum/tg_asset/simple/register()
for(var/asset_name in assets)
var/datum/asset_cache_item/ACI = SSassets.transport.register_asset(asset_name, assets[asset_name])
if (!ACI)
log_debug("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
continue
if (legacy)
ACI.legacy = legacy
if (keep_local_name)
ACI.keep_local_name = keep_local_name
assets[asset_name] = ACI
/datum/tg_asset/simple/send(client)
. = SSassets.transport.send_assets(client, assets)
/datum/tg_asset/simple/get_url_mappings()
. = list()
for (var/asset_name in assets)
.[asset_name] = SSassets.transport.get_asset_url(asset_name, assets[asset_name])
/datum/tg_asset/simple/unregister()
for (var/asset_name in assets)
SSassets.transport.unregister_asset(asset_name)
// For registering or sending multiple others at once
/datum/tg_asset/group
_abstract = /datum/tg_asset/group
var/list/children
/datum/tg_asset/group/register()
for(var/type in children)
load_asset_datum(type)
/datum/tg_asset/group/send(client/C)
for(var/type in children)
var/datum/tg_asset/A = get_tg_asset_datum(type)
. = A.send(C) || .
/datum/tg_asset/group/get_url_mappings()
. = list()
for(var/type in children)
var/datum/tg_asset/A = get_tg_asset_datum(type)
. += A.get_url_mappings()
/datum/tg_asset/group/unregister()
for (var/type in children)
var/datum/tg_asset/A = get_tg_asset_datum(type)
A.unregister()
// -- IMPLEMENTABLE : tg_asset for changelog items
//Generates assets based on iconstates of a single icon
// -- IMPLEMENTABLE : tg_asset for icon_states
/// Namespace'ed assets (for static css and html files)
/// When sent over a cdn transport, all assets in the same asset datum will exist in the same folder, as their plain names.
/// Used to ensure css files can reference files by url() without having to generate the css at runtime, both the css file and the files it depends on must exist in the same namespace asset datum. (Also works for html)
/// For example `blah.css` with asset `blah.png` will get loaded as `namespaces/a3d..14f/f12..d3c.css` and `namespaces/a3d..14f/blah.png`. allowing the css file to load `blah.png` by a relative url rather then compute the generated url with get_url_mappings().
/// The namespace folder's name will change if any of the assets change. (excluding parent assets)
/datum/tg_asset/simple/namespaced
_abstract = /datum/tg_asset/simple/namespaced
/// parents - list of the parent asset or assets (in name = file assoicated format) for this namespace.
/// parent assets must be referenced by their generated url, but if an update changes a parent asset, it won't change the namespace's identity.
var/list/parents = list()
/datum/tg_asset/simple/namespaced/register()
if (legacy)
assets |= parents
var/list/hashlist = list()
var/list/sorted_assets = sortList(assets)
for (var/asset_name in sorted_assets)
var/datum/asset_cache_item/ACI = new(asset_name, sorted_assets[asset_name])
if (!ACI?.hash)
log_debug("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
continue
hashlist += ACI.hash
sorted_assets[asset_name] = ACI
var/namespace = md5(hashlist.Join())
for (var/asset_name in parents)
var/datum/asset_cache_item/ACI = new(asset_name, parents[asset_name])
if (!ACI?.hash)
log_debug("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
continue
ACI.namespace_parent = TRUE
sorted_assets[asset_name] = ACI
for (var/asset_name in sorted_assets)
var/datum/asset_cache_item/ACI = sorted_assets[asset_name]
if (!ACI?.hash)
log_debug("ERROR: Invalid asset: [type]:[asset_name]:[ACI]")
continue
ACI.namespace = namespace
assets = sorted_assets
..()
/// Get a html string that will load a html asset.
/// Needed because byond doesn't allow you to browse() to a url.
/datum/tg_asset/simple/namespaced/proc/get_htmlloader(filename)
return url2htmlloader(SSassets.transport.get_asset_url(filename, assets[filename]))
/// A subtype to generate a JSON file from a list
/datum/tg_asset/json
_abstract = /datum/tg_asset/json
/// The filename, will be suffixed with ".json"
var/name
/datum/tg_asset/json/send(client)
return SSassets.transport.send_assets(client, "[name].json")
/datum/tg_asset/json/get_url_mappings()
return list(
"[name].json" = SSassets.transport.get_asset_url("[name].json"),
)
/datum/tg_asset/json/register()
var/filename = "data/[name].json"
fdel(filename)
rustg_file_write(json_encode(generate()), filename)
SSassets.transport.register_asset("[name].json", fcopy_rsc(filename))
fdel(filename)
/// Returns the data that will be JSON encoded
/datum/tg_asset/json/proc/generate()
CRASH("generate() not implemented for [type]!")
/datum/tg_asset/json/unregister()
SSassets.transport.unregister_asset("[name].json")

View File

@@ -0,0 +1,6 @@
/datum/tg_asset/simple/tgfont
assets = list(
"tgfont.eot" = 'tgui/packages/tgfont/static/tgfont.eot',
"tgfont.woff2" = 'tgui/packages/tgfont/static/tgfont.woff2',
"tgfont.css" = 'tgui/packages/tgfont/static/tgfont.css',
)

View File

@@ -0,0 +1,35 @@
// If you use a file(...) object, instead of caching the asset it will be loaded from disk every time it's requested.
// This is useful for development, but not recommended for production.
// And if TGS is defined, we're being run in a production environment.
#ifdef TGS
/datum/tg_asset/simple/tgui
keep_local_name = FALSE
assets = list(
"tgui.bundle.js" = "tgui/public/tgui.bundle.js",
"tgui.bundle.css" = "tgui/public/tgui.bundle.css",
)
/datum/tg_asset/simple/tgui_panel
keep_local_name = FALSE
assets = list(
"tgui-panel.bundle.js" = "tgui/public/tgui-panel.bundle.js",
"tgui-panel.bundle.css" = "tgui/public/tgui-panel.bundle.css",
)
#else
/datum/tg_asset/simple/tgui
keep_local_name = TRUE
assets = list(
"tgui.bundle.js" = file("tgui/public/tgui.bundle.js"),
"tgui.bundle.css" = file("tgui/public/tgui.bundle.css"),
)
/datum/tg_asset/simple/tgui_panel
keep_local_name = TRUE
assets = list(
"tgui-panel.bundle.js" = file("tgui/public/tgui-panel.bundle.js"),
"tgui-panel.bundle.css" = file("tgui/public/tgui-panel.bundle.css"),
)
#endif

View File

@@ -0,0 +1,8 @@
/datum/tg_asset/simple/fontawesome
assets = list(
"fa-regular-400.ttf" = 'html/font-awesome/webfonts/fa-regular-400.ttf',
"fa-solid-900.ttf" = 'html/font-awesome/webfonts/fa-solid-900.ttf',
"fa-v4compatibility.ttf" = 'html/font-awesome/webfonts/fa-v4compatibility.ttf',
"v4shim.css" = 'html/font-awesome/css/v4-shims.min.css',
"font-awesome.css" = 'html/font-awesome/css/all.min.css',
)

View File

@@ -0,0 +1,28 @@
/// Maps icon names to ref values
/datum/tg_asset/json/icon_ref_map
name = "icon_ref_map"
early = TRUE
/datum/tg_asset/json/icon_ref_map/generate()
var/list/data = list() //"icons/obj/drinks.dmi" => "[0xc000020]"
//var/start = "0xc000000"
var/value = 0
while(TRUE)
value += 1
var/ref = "\[0xc[num2text(value,6,16)]\]"
var/mystery_meat = locate(ref)
if(isicon(mystery_meat))
if(!isfile(mystery_meat)) // Ignore the runtime icons for now
continue
var/path = get_icon_dmi_path(mystery_meat) //Try to get the icon path
if(path)
data[path] = ref
else if(mystery_meat)
continue; //Some other non-icon resource, ogg/json/whatever
else //Out of resources end this, could also try to end this earlier as soon as runtime generated icons appear but eh
break;
return data

View File

@@ -0,0 +1,37 @@
# Asset cache system
## Framework for managing browser assets (javascript,css,images,etc)
This manages getting the asset to the client without doing unneeded re-sends, as well as utilizing any configured cdns.
There are two frameworks for using this system:
### Asset datum:
Make a datum in asset_list_items.dm with your browser assets for your thing.
Checkout asset_list.dm for the helper subclasses
The `simple` subclass will most likely be of use for most cases.
Call get_tg_asset_datum() with the type of the datum you created to get your asset cache datum
Call .send(client|usr) on that datum to send the asset to the client. Depending on the asset transport this may or may not block.
Call .get_url_mappings() to get an associated list with the urls your assets can be found at.
### Manual backend:
See the documentation for `/datum/asset_transport` for the backend api the asset datums utilize.
The global variable `SSassets.transport` contains the currently configured transport.
### Notes:
Because byond browse() calls use non-blocking queues, if your code uses output() (which bypasses all of these queues) to invoke javascript functions you will need to first have the javascript announce to the server it has loaded before trying to invoke js functions.
To make your code work with any CDNs configured by the server, you must make sure assets are referenced from the url returned by `get_url_mappings()` or by asset_transport's `get_asset_url()`. (TGUI also has helpers for this.) If this can not be easily done, you can bypass the cdn using legacy assets, see the simple asset datum for details.
CSS files that use url() can be made to use the CDN without needing to rewrite all url() calls in code by using the namespaced helper datum. See the documentation for `/datum/tg_asset/simple/namespaced` for details.

View File

@@ -0,0 +1,171 @@
/// When sending mutiple assets, how many before we give the client a quaint little sending resources message
#define ASSET_CACHE_TELL_CLIENT_AMOUNT 8
/// How many assets can be sent at once during legacy asset transport
#define SLOW_ASSET_SEND_RATE 6
/// Base browse_rsc asset transport
/datum/asset_transport
var/name = "Simple browse_rsc asset transport"
var/static/list/preload
/// Don't mutate the filename of assets when sending via browse_rsc.
/// This is to make it easier to debug issues with assets, and allow server operators to bypass issues that make it to production.
/// If turning this on fixes asset issues, something isn't using get_asset_url and the asset isn't marked legacy, fix one of those.
var/dont_mutate_filenames = FALSE
/// Called when the transport is loaded by the config controller, not called on the default transport unless it gets loaded by a config change.
/datum/asset_transport/proc/Load()
if (config.asset_simple_preload)
for(var/client/C in global.clients)
add_timer(new /callback(src, PROC_REF(send_assets_slow), C, preload), 1 SECONDS)
/// Initialize - Called when SSassets initializes.
/datum/asset_transport/proc/Initialize(list/assets)
preload = assets.Copy()
if (!config.asset_simple_preload)
return
for(var/client/C in global.clients)
add_timer(new /callback(src, PROC_REF(send_assets_slow), C, preload), 1 SECONDS)
/**
* Register a browser asset with the asset cache system.
* returns a /datum/tg_asset_cache_item.
* mutiple calls to register the same asset under the same asset_name return the same datum.
*
* Arguments:
* * asset_name - the identifier of the asset.
* * asset - the actual asset file (or an asset_cache_item datum).
* * file_hash - optional, a hash of the contents of the asset files contents. used so asset_cache_item doesnt have to hash it again
* * dmi_file_path - optional, means that the given asset is from the rsc and thus we dont need to do some expensive operations
*/
/datum/asset_transport/proc/register_asset(asset_name, asset, file_hash, dmi_file_path)
var/datum/asset_cache_item/ACI = asset
if (!istype(ACI))
ACI = new(asset_name, asset, file_hash, dmi_file_path)
if (!ACI || !ACI.hash)
CRASH("ERROR: Invalid asset: [asset_name]:[asset]:[ACI]")
if (SSassets.cache[asset_name])
var/datum/asset_cache_item/OACI = SSassets.cache[asset_name]
OACI.legacy = ACI.legacy = (ACI.legacy|OACI.legacy)
OACI.namespace_parent = ACI.namespace_parent = (ACI.namespace_parent | OACI.namespace_parent)
OACI.namespace = OACI.namespace || ACI.namespace
if (OACI.hash != ACI.hash)
var/error_msg = "ERROR: new asset added to the asset cache with the same name as another asset: [asset_name] existing asset hash: [OACI.hash] new asset hash:[ACI.hash]"
stack_trace(error_msg)
log_debug(error_msg)
else
if (length(ACI.namespace))
return ACI
return OACI
SSassets.cache[asset_name] = ACI
return ACI
/// Immediately removes an asset from the asset cache.
/datum/asset_transport/proc/unregister_asset(asset_name)
SSassets.cache[asset_name] = null
SSassets.cache.Remove(null)
/// Returns a url for a given asset.
/// asset_name - Name of the asset.
/// asset_cache_item - asset cache item datum for the asset, optional, overrides asset_name
/datum/asset_transport/proc/get_asset_url(asset_name, datum/asset_cache_item/asset_cache_item)
if (!istype(asset_cache_item))
asset_cache_item = SSassets.cache[asset_name]
// To ensure code that breaks on cdns breaks in local testing, we only
// use the normal filename on legacy assets and name space assets.
var/keep_local_name = dont_mutate_filenames \
|| asset_cache_item.legacy \
|| asset_cache_item.keep_local_name \
|| (asset_cache_item.namespace && !asset_cache_item.namespace_parent)
if (keep_local_name)
return url_encode(asset_cache_item.name)
return url_encode("asset.[asset_cache_item.hash][asset_cache_item.ext]")
/// Sends a list of browser assets to a client
/// client - a client or mob
/// asset_list - A list of asset filenames to be sent to the client. Can optionally be assoicated with the asset's asset_cache_item datum.
/// Returns TRUE if any assets were sent.
/datum/asset_transport/proc/send_assets(client/client, list/asset_list)
#if defined(UNIT_TESTS)
return
#endif
if (!istype(client))
if (ismob(client))
var/mob/our_mob = client
if (our_mob.client)
client = our_mob.client
else //no stacktrace because this will mainly happen because the client went away
return
else
CRASH("Invalid argument: client: `[client]`")
if (!islist(asset_list))
asset_list = list(asset_list)
var/list/unreceived = list()
for (var/asset_name in asset_list)
var/datum/asset_cache_item/ACI = asset_list[asset_name]
if (!istype(ACI) && !(ACI = SSassets.cache[asset_name]))
log_debug("ERROR: can't send asset `[asset_name]`: unregistered or invalid state: `[ACI]`")
continue
var/asset_file = ACI.resource
if (!asset_file)
log_debug("ERROR: can't send asset `[asset_name]`: invalid registered resource: `[ACI.resource]`")
continue
var/asset_hash = ACI.hash
var/new_asset_name = asset_name
var/keep_local_name = dont_mutate_filenames \
|| ACI.legacy \
|| ACI.keep_local_name \
|| (ACI.namespace && !ACI.namespace_parent)
if (!keep_local_name)
new_asset_name = "asset.[ACI.hash][ACI.ext]"
if (client.sent_assets[new_asset_name] == asset_hash)
if (global.Debug2)
log_debug("DEBUG: Skipping send of `[asset_name]` (as `[new_asset_name]`) for `[client]` because it already exists in the client's sent_assets list")
continue
unreceived[asset_name] = ACI
if (unreceived.len)
if (unreceived.len >= ASSET_CACHE_TELL_CLIENT_AMOUNT)
to_chat(client, ("<span class='info'>Sending Resources...</span>"))
for (var/asset_name in unreceived)
var/new_asset_name = asset_name
var/datum/asset_cache_item/ACI = unreceived[asset_name]
var/keep_local_name = dont_mutate_filenames \
|| ACI.legacy \
|| ACI.keep_local_name \
|| (ACI.namespace && !ACI.namespace_parent)
if (!keep_local_name)
new_asset_name = "asset.[ACI.hash][ACI.ext]"
client << browse_rsc(ACI.resource, new_asset_name)
client.sent_assets[new_asset_name] = ACI.hash
add_timer(new /callback(client, TYPE_PROC_REF(/client, asset_cache_update_json)), 1 SECONDS)
return TRUE
return FALSE
/// Precache files without clogging up the browse() queue, used for passively sending files on connection start.
/datum/asset_transport/proc/send_assets_slow(client/client, list/files, filerate = SLOW_ASSET_SEND_RATE)
var/startingfilerate = filerate
for (var/file in files)
if (!client)
break
if (send_assets(client, file))
if (!(--filerate))
filerate = startingfilerate
client.tg_browse_queue_flush()
stoplag(0) //queuing calls like this too quickly can cause issues in some client versions
/// Check the config is valid to load this transport
/// Returns TRUE or FALSE
/datum/asset_transport/proc/validate_config(log = TRUE)
return TRUE
#undef ASSET_CACHE_TELL_CLIENT_AMOUNT

View File

@@ -0,0 +1,87 @@
/// CDN Webroot asset transport.
/datum/asset_transport/webroot
name = "CDN Webroot asset transport"
/datum/asset_transport/webroot/Load()
if (validate_config(log = FALSE))
load_existing_assets()
/// Processes thru any assets that were registered before we were loaded as a transport.
/datum/asset_transport/webroot/proc/load_existing_assets()
for (var/asset_name in SSassets.cache)
var/datum/asset_cache_item/ACI = SSassets.cache[asset_name]
save_asset_to_webroot(ACI)
/// Register a browser asset with the asset cache system
/// We also save it to the CDN webroot at this step instead of waiting for send_assets()
/// asset_name - the identifier of the asset
/// asset - the actual asset file or an asset_cache_item datum.
/datum/asset_transport/webroot/register_asset(asset_name, asset, file_hash, dmi_path)
. = ..()
var/datum/asset_cache_item/ACI = .
if (istype(ACI) && ACI.hash)
save_asset_to_webroot(ACI)
/// Saves the asset to the webroot taking into account namespaces and hashes.
/datum/asset_transport/webroot/proc/save_asset_to_webroot(datum/asset_cache_item/ACI)
var/webroot = config.asset_cdn_webroot
var/newpath = "[webroot][get_asset_suffex(ACI)]"
if (fexists(newpath))
return
if (fexists("[newpath].gz")) //its a common pattern in webhosting to save gzip'ed versions of text files and let the webserver serve them up as gzip compressed normal files, sometimes without keeping the original version.
return
return fcopy(ACI.resource, newpath)
/// Returns a url for a given asset.
/// asset_name - Name of the asset.
/// asset_cache_item - asset cache item datum for the asset, optional, overrides asset_name
/datum/asset_transport/webroot/get_asset_url(asset_name, datum/asset_cache_item/asset_cache_item)
if (!istype(asset_cache_item))
asset_cache_item = SSassets.cache[asset_name]
var/url = config.asset_cdn_url //config loading will handle making sure this ends in a /
return "[url][get_asset_suffex(asset_cache_item)]"
/datum/asset_transport/webroot/proc/get_asset_suffex(datum/asset_cache_item/asset_cache_item)
var/base = "[copytext(asset_cache_item.hash, 1, 3)]/"
var/filename = "asset.[asset_cache_item.hash][asset_cache_item.ext]"
if (length(asset_cache_item.namespace))
base = "namespaces/[copytext(asset_cache_item.namespace, 1, 3)]/[asset_cache_item.namespace]/"
if (!asset_cache_item.namespace_parent)
filename = "[asset_cache_item.name]"
return base + filename
/// webroot asset sending - does nothing unless passed legacy assets
/datum/asset_transport/webroot/send_assets(client/client, list/asset_list)
. = FALSE
var/list/legacy_assets = list()
if (!islist(asset_list))
asset_list = list(asset_list)
for (var/asset_name in asset_list)
var/datum/asset_cache_item/ACI = asset_list[asset_name]
if (!istype(ACI))
ACI = SSassets.cache[asset_name]
if (!ACI)
legacy_assets += asset_name //pass it on to base send_assets so it can output an error
continue
if (ACI.legacy)
legacy_assets[asset_name] = ACI
if (length(legacy_assets))
. = ..(client, legacy_assets)
/// webroot slow asset sending - does nothing.
/datum/asset_transport/webroot/send_assets_slow(client/client, list/files, filerate)
return FALSE
/datum/asset_transport/webroot/validate_config(log = TRUE)
if (!config.asset_cdn_url)
if (log)
log_debug("ERROR: [type]: Invalid Config: ASSET_CDN_URL")
return FALSE
if (!config.asset_cdn_url)
if (log)
log_debug("ERROR: [type]: Invalid Config: ASSET_CDN_WEBROOT")
return FALSE
return TRUE

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
</head>
<body>
<script>
//this is used over window.location because window.location has a character limit in IE.
function sendbyond(text) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '?'+text, true);
xhr.send(null);
}
var xhr = new XMLHttpRequest();
xhr.open('GET', 'asset_data.json', true);
xhr.responseType = 'text';
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
var status = xhr.status;
if (status >= 200 && status < 400) {
sendbyond('asset_cache_preload_data=' + encodeURIComponent(xhr.responseText));
}
}
};
xhr.send(null);
</script>
</body>
</html>

View File

@@ -112,5 +112,8 @@
// Last Round Scoreboard images have been sent
var/received_last_round_images = FALSE
// Duplicate from /datum
var/list/active_timers = list()
var/list/person_animation_viewers = list()
var/list/item_animation_viewers = list()

View File

@@ -149,7 +149,7 @@ var/updated_stats = 0
bunker_setting = 1
load_bunker()
if(config)
winset(src, null, "window1.msay_output.style=[config.world_style_config];")
winset_wrapper(null, "window1.msay_output.style=[config.world_style_config];") // The TG people told me to do it. If this fails for reason X or Y, the entire client/New() will runtime and we'll be in huge trouble.
else
to_chat(src, "<span class='warning'>The stylesheet wasn't properly setup call an administrator to reload the stylesheet or relog.</span>")
@@ -331,22 +331,22 @@ var/updated_stats = 0
prefs.save_preferences_sqlite(src, src.ckey)
if(prefs.lastchangelog != changelog_hash) //bolds the changelog button on the interface so we know there are updates.
winset(src, "rpane.changelog", "background-color=#eaeaea;font-style=bold")
winset_wrapper("rpane.changelog", "background-color=#eaeaea;font-style=bold")
prefs.SetChangelog(ckey,changelog_hash)
to_chat(src, "<span class='info'>Changelog has changed since your last visit.</span>")
//Set map label to correct map name
winset(src, "rpane.mapb", "text=\"[map.nameLong]\"")
winset_wrapper(src, "rpane.mapb", "text=\"[map.nameLong]\"")
if (round_end_info)
winset(src, "rpane.round_end", "is-visible=true")
winset(src, "rpane.last_round_end", "is-visible=false")
winset_wrapper("rpane.round_end", "is-visible=true")
winset_wrapper("rpane.last_round_end", "is-visible=false")
else if (last_round_end_info)
winset(src, "rpane.round_end", "is-visible=false")
winset(src, "rpane.last_round_end", "is-visible=true")
winset_wrapper("rpane.round_end", "is-visible=false")
winset_wrapper("rpane.last_round_end", "is-visible=true")
else
winset(src, "rpane.round_end", "is-visible=false")
winset(src, "rpane.last_round_end", "is-visible=false")
winset_wrapper("rpane.round_end", "is-visible=false")
winset_wrapper("rpane.last_round_end", "is-visible=false")
if (runescape_pvp)
to_chat(src, "<span class='userdanger'>WARNING: Wilderness mode is enabled; players can only harm one another in maintenance areas!</span>")
@@ -359,6 +359,10 @@ var/updated_stats = 0
fps = (prefs.fps < 0) ? RECOMMENDED_CLIENT_FPS : prefs.fps
// This is wrapped so that if the winset fucks up somehow, this doesn't crash the entire client login proc.
/client/proc/winset_wrapper(control_id, params)
winset(src, control_id, params)
//////////////
//DISCONNECT//
//////////////
@@ -518,6 +522,7 @@ var/updated_stats = 0
if(query_connection_log.ErrorMsg())
WARNING("FINGERPRINT: [query_connection_log.ErrorMsg()]")
qdel(query_connection_log)
connection_time = world.time
#undef TOPIC_SPAM_DELAY
#undef UPLOAD_LIMIT

View File

@@ -13,6 +13,21 @@ var/list/asset_datums = list()
var/list/sending = list()
var/last_asset_job = 0 // Last job done.
/// Blocks until all currently sending browse and browse_rsc assets have been sent.
/// Due to byond limitations, this proc will sleep for 1 client round trip even if the client has no pending asset sends.
/// This proc will return an untrue value if it had to return before confirming the send, such as timeout or the client going away.
/client/proc/browse_queue_flush(timeout = 50)
var/job = ++last_asset_job
var/t = 0
var/timeout_time = timeout
src << browse({"<script>window.location.href='byond://?asset_cache_confirm_arrival=[job]'</script>"}, "window=asset_cache_browser&file=asset_cache_send_verify.htm")
while(!completed_asset_jobs["[job]"] && t < timeout_time) // Reception is handled in Topic()
stoplag(1) // Lock up the caller until this is received.
t++
if (t < timeout_time)
return TRUE
//This proc sends the asset to the client, but only if it needs it.
/proc/send_asset(var/client/client, var/asset_name, var/verify = TRUE)
if(!istype(client))
@@ -438,12 +453,11 @@ var/list/asset_datums = list()
/datum/asset/simple/fontawesome
assets = list(
"fa-regular-400.eot" = 'html/font-awesome/webfonts/fa-regular-400.eot',
"fa-regular-400.woff" = 'html/font-awesome/webfonts/fa-regular-400.woff',
"fa-solid-900.eot" = 'html/font-awesome/webfonts/fa-solid-900.eot',
"fa-solid-900.woff" = 'html/font-awesome/webfonts/fa-solid-900.woff',
"fa-regular-400.ttf" = 'html/font-awesome/webfonts/fa-regular-400.ttf',
"fa-solid-900.ttf" = 'html/font-awesome/webfonts/fa-solid-900.ttf',
"fa-v4compatibility.ttf" = 'html/font-awesome/webfonts/fa-v4compatibility.ttf',
"v4shim.css" = 'html/font-awesome/css/v4-shims.min.css',
"font-awesome.css" = 'html/font-awesome/css/all.min.css',
"v4shim.css" = 'html/font-awesome/css/v4-shims.min.css'
)
/datum/asset/simple/tgui

View File

@@ -241,7 +241,16 @@ var/const/MAX_SAVE_SLOTS = 16
var/obj_chat_on_map = FALSE
var/no_goonchat_for_obj = FALSE
// TGUI things
var/tgui_fancy = TRUE
//// -- Unimplemented for tgui alerts --
var/tgui_input = FALSE
var/tgui_input_large = FALSE
var/tgui_input_swapped = FALSE
var/tgui_lock = FALSE
var/tgui_scale = TRUE
var/layout_prefs_used = FALSE
var/fps = -1
var/client/client

View File

@@ -762,7 +762,7 @@ Target Machine: "}
var/icon/img = message_app.imglist[note]
if(img)
usr << browse_rsc(ImagePDA(img), "tmp_photo_[note].png")
dat += "<img src='tmp_photo_[note].png' width = '192' style='-ms-interpolation-mode:nearest-neighbor'><BR>"
dat += "<img src='tmp_photo_[note].png' width = '192' style='image-rendering: pixelated'><BR>"
return dat
/mob/living/silicon/pai/proc/softwareHolomap()

View File

@@ -184,7 +184,7 @@
if(dark_plane)
if (master_plane)
master_plane.blend_mode = BLEND_ADD
dark_plane.alphas["spider"] = 15 // with the master_plane at BLEND_ADD, shadows appear well lit while actually well lit places appear blinding.
dark_plane.alphas["spider"] = 0 // with the master_plane at BLEND_ADD, shadows appear well lit while actually well lit places appear blinding.
client.color = list(
1,0,0,0,
0,0.2,0,0,

View File

@@ -430,7 +430,7 @@
if(dark_plane)
if(master_plane)
master_plane.blend_mode = BLEND_ADD
dark_plane.alphas["grue"] = 15 // with the master_plane at BLEND_ADD, shadows appear well lit while actually well lit places appear blinding.
dark_plane.alphas["grue"] = 0 // with the master_plane at BLEND_ADD, shadows appear well lit while actually well lit places appear blinding.
client.color = list(
1,0,0,0,
-1,0.2,0.2,0,

View File

@@ -44,5 +44,5 @@
return
// Used by the Nano UI Manager (/datum/nanomanager) to track UIs opened by this mob
/client/var/list/open_uis = list()
/mob/var/list/open_uis = list()
/client/var/list/nano_open_uis = list()
/mob/var/list/nano_open_uis = list()

View File

@@ -2,7 +2,7 @@
// There should only ever be one (global) instance of nanomanger
/datum/nanomanager
// a list of current open /nanoui UIs, grouped by src_object and ui_key
var/open_uis[0]
var/nano_open_uis[0]
// a list of current open /nanoui UIs, not grouped, for use in processing
var/list/processing_uis = list()
// a list of asset filenames which are to be sent to the client on user logon
@@ -107,14 +107,14 @@
*/
/datum/nanomanager/proc/get_open_ui(var/mob/user, 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))
if (isnull(nano_open_uis[src_object_key]) || !istype(nano_open_uis[src_object_key], /list))
//testing("nanomanager/get_open_ui mob [user.name] [src_object:name] [ui_key] - there are no uis open")
return null
else if (isnull(open_uis[src_object_key][ui_key]) || !istype(open_uis[src_object_key][ui_key], /list))
else if (isnull(nano_open_uis[src_object_key][ui_key]) || !istype(nano_open_uis[src_object_key][ui_key], /list))
//testing("nanomanager/get_open_ui mob [user.name] [src_object:name] [ui_key] - there are no uis open for this object")
return null
for (var/datum/nanoui/ui in open_uis[src_object_key][ui_key])
for (var/datum/nanoui/ui in nano_open_uis[src_object_key][ui_key])
if (ui.user == user)
return ui
@@ -130,12 +130,12 @@
*/
/datum/nanomanager/proc/update_uis(src_object)
var/src_object_key = "\ref[src_object]"
if (isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list))
if (isnull(nano_open_uis[src_object_key]) || !istype(nano_open_uis[src_object_key], /list))
return 0
var/update_count = 0
for (var/ui_key in open_uis[src_object_key])
for (var/datum/nanoui/ui in open_uis[src_object_key][ui_key])
for (var/ui_key in nano_open_uis[src_object_key])
for (var/datum/nanoui/ui in nano_open_uis[src_object_key][ui_key])
if(ui && ui.src_object && ui.user)
ui.process(1)
update_count++
@@ -151,11 +151,11 @@
* @return int The number of uis updated
*/
/datum/nanomanager/proc/update_user_uis(var/mob/user, src_object = null, ui_key = null)
if (isnull(user.open_uis) || !istype(user.open_uis, /list) || open_uis.len == 0)
if (isnull(user.nano_open_uis) || !istype(user.nano_open_uis, /list) || nano_open_uis.len == 0)
return 0 // has no open uis
var/update_count = 0
for (var/datum/nanoui/ui in user.open_uis)
for (var/datum/nanoui/ui in user.nano_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(1)
update_count++
@@ -172,17 +172,17 @@
* @return int The number of uis closed
*/
/datum/nanomanager/proc/close_user_uis(var/mob/user, src_object = null, ui_key = null)
if (isnull(user.open_uis) || !istype(user.open_uis, /list) || open_uis.len == 0)
if (isnull(user.nano_open_uis) || !istype(user.nano_open_uis, /list) || nano_open_uis.len == 0)
//testing("nanomanager/close_user_uis mob [user.name] has no open uis")
return 0 // has no open uis
var/close_count = 0
for (var/datum/nanoui/ui in user.open_uis)
for (var/datum/nanoui/ui in user.nano_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_count++
//testing("nanomanager/close_user_uis mob [user.name] closed [open_uis.len] of [close_count] uis")
//testing("nanomanager/close_user_uis mob [user.name] closed [nano_open_uis.len] of [close_count] uis")
return close_count
@@ -195,12 +195,12 @@
*/
/datum/nanomanager/proc/close_uis(src_object)
var/src_object_key = "\ref[src_object]"
if(!istype(open_uis[src_object_key], /list))
if(!istype(nano_open_uis[src_object_key], /list))
return 0
var/close_count = 0
for(var/ui_key in open_uis[src_object_key])
for(var/datum/nanoui/ui in open_uis[src_object_key][ui_key])
for(var/ui_key in nano_open_uis[src_object_key])
for(var/datum/nanoui/ui in nano_open_uis[src_object_key][ui_key])
ui.close()
close_count++
@@ -216,16 +216,16 @@
*/
/datum/nanomanager/proc/ui_opened(var/datum/nanoui/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())
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();
if (isnull(nano_open_uis[src_object_key]) || !istype(nano_open_uis[src_object_key], /list))
nano_open_uis[src_object_key] = list(ui.ui_key = list())
else if (isnull(nano_open_uis[src_object_key][ui.ui_key]) || !istype(nano_open_uis[src_object_key][ui.ui_key], /list))
nano_open_uis[src_object_key][ui.ui_key] = list();
ui.user.open_uis.Add(ui)
var/list/uis = open_uis[src_object_key][ui.ui_key]
ui.user.nano_open_uis.Add(ui)
var/list/uis = nano_open_uis[src_object_key][ui.ui_key]
uis.Add(ui)
processing_uis.Add(ui)
//testing("nanomanager/ui_opened mob [ui.user.name] [ui.src_object:name] [ui.ui_key] - user.open_uis [ui.user.open_uis.len] | uis [uis.len] | processing_uis [processing_uis.len]")
//testing("nanomanager/ui_opened mob [ui.user.name] [ui.src_object:name] [ui.ui_key] - user.nano_open_uis [ui.user.nano_open_uis.len] | uis [uis.len] | processing_uis [processing_uis.len]")
/**
* Remove a /nanoui ui from the list of open uis
@@ -237,18 +237,18 @@
*/
/datum/nanomanager/proc/ui_closed(var/datum/nanoui/ui)
var/src_object_key = "\ref[ui.src_object]"
if (isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list))
if (isnull(nano_open_uis[src_object_key]) || !istype(nano_open_uis[src_object_key], /list))
return 0 // wasn't open
else if (isnull(open_uis[src_object_key][ui.ui_key]) || !istype(open_uis[src_object_key][ui.ui_key], /list))
else if (isnull(nano_open_uis[src_object_key][ui.ui_key]) || !istype(nano_open_uis[src_object_key][ui.ui_key], /list))
return 0 // wasn't open
processing_uis.Remove(ui)
if(ui.user)
ui.user.open_uis.Remove(ui)
var/list/uis = open_uis[src_object_key][ui.ui_key]
ui.user.nano_open_uis.Remove(ui)
var/list/uis = nano_open_uis[src_object_key][ui.ui_key]
uis.Remove(ui)
//testing("nanomanager/ui_closed mob [ui.user.name] [ui.src_object:name] [ui.ui_key] - user.open_uis [ui.user.open_uis.len] | uis [uis.len] | processing_uis [processing_uis.len]")
//testing("nanomanager/ui_closed mob [ui.user.name] [ui.src_object:name] [ui.ui_key] - user.nano_open_uis [ui.user.nano_open_uis.len] | uis [uis.len] | processing_uis [processing_uis.len]")
return 1
@@ -279,18 +279,18 @@
if(!istype(oldMob))
return 0 // no mob, no uis
//testing("nanomanager/user_transferred from mob [oldMob.name] to mob [newMob.name]")
if (isnull(oldMob.open_uis) || !istype(oldMob.open_uis, /list) || open_uis.len == 0)
if (isnull(oldMob.nano_open_uis) || !istype(oldMob.nano_open_uis, /list) || nano_open_uis.len == 0)
//testing("nanomanager/user_transferred mob [oldMob.name] has no open uis")
return 0 // has no open uis
if (isnull(newMob.open_uis) || !istype(newMob.open_uis, /list))
newMob.open_uis = list()
if (isnull(newMob.nano_open_uis) || !istype(newMob.nano_open_uis, /list))
newMob.nano_open_uis = list()
for (var/datum/nanoui/ui in oldMob.open_uis)
for (var/datum/nanoui/ui in oldMob.nano_open_uis)
ui.user = newMob
newMob.open_uis.Add(ui)
newMob.nano_open_uis.Add(ui)
oldMob.open_uis.len = 0
oldMob.nano_open_uis.len = 0
return 1 // success
@@ -321,13 +321,13 @@
*/
/datum/nanomanager/proc/send_message(src_object, ui_key, js_function, data, mob/user = null)
var/src_object_key = "\ref[src_object]"
if (isnull(open_uis[src_object_key]))
if (isnull(nano_open_uis[src_object_key]))
return 0
var/paramlist = list2params(data)
var/update_count = 0
for (var/datum/nanoui/ui in open_uis[src_object_key][ui_key])
for (var/datum/nanoui/ui in nano_open_uis[src_object_key][ui_key])
if (!ui.src_object || (user != null && ui.user != user))
continue

View File

@@ -10,6 +10,10 @@
var/list/colourmatrix = list()
/datum/organ/internal/eyes/proc/update_perception(var/mob/living/carbon/human/M)
// Bad hack but in 516 any non-zero value of the dark plane will result in glitch for night vision googles
// Fix by reworking dark planes?
if (istype(M.glasses, /obj/item/clothing/glasses/scanner/night))
return
M.dark_plane.alphas["human"] = 5
/datum/organ/internal/eyes/process() //Eye damage replaces the old eye_stat var.

View File

@@ -58,7 +58,7 @@
if(img)
user << browse_rsc(img.img, "tmp_photo.png")
info_image = "<img src='tmp_photo.png' width='192' style='-ms-interpolation-mode:nearest-neighbor' /><br><a href='?src=\ref[src];picture=1'>Remove</a><br>"
info_image = "<img src='tmp_photo.png' width='192' style='image-rendering: pixelated' /><br><a href='?src=\ref[src];picture=1'>Remove</a><br>"
user << browse("<HTML><HEAD><TITLE>[name]</TITLE></HEAD><BODY[color ? " bgcolor=[src.color]":""]>[info_image][info_text][stamps]</BODY></HTML>", "window=[name];size=[display_x]x[display_y]")
onclose(user, "[name]")

View File

@@ -80,7 +80,7 @@
user << browse("<html><head><title>[name]</title></head>" \
+ "<body style='overflow:hidden;margin:0;text-align:center'>" \
+ "<img src='tmp_photo.png' width='[displaylength]' style='-ms-interpolation-mode:nearest-neighbor' />" \
+ "<img src='tmp_photo.png' width='[displaylength]' style='image-rendering: pixelated' />" \
+ "[scribble ? "<br>Written on the back:<br><i>[scribble]</i>" : ""]"\
+ "</body></html>", "window=book;size=[displaylength]x[scribble ? displaylength+108 : displaylength]")
if(info) //Would rather not display a blank line of text

View File

@@ -24,7 +24,7 @@ var/list/beam_master = list()
var/obj/item/projectile/beam/fired_beam
var/list/rayCastHit/hit_cache
/ray/beam_ray/New(var/vector/p_origin, var/vector/p_direction, var/obj/item/projectile/beam/fired_beam)
/ray/beam_ray/New(var/_vector/p_origin, var/_vector/p_direction, var/obj/item/projectile/beam/fired_beam)
..(p_origin, p_direction, fired_beam.starting.z)
src.fired_beam = fired_beam
original_damage = fired_beam.damage
@@ -90,9 +90,9 @@ var/list/beam_master = list()
..()
/obj/item/projectile/beam/proc/fireto(var/vector/origin, var/vector/direction)
/obj/item/projectile/beam/proc/fireto(var/_vector/origin, var/_vector/direction)
// + 0.5 because we want to start in the middle of the tile
var/ray/beam_ray/shot_ray = new /ray/beam_ray(origin + new /vector(0.5, 0.5), direction, src)
var/ray/beam_ray/shot_ray = new /ray/beam_ray(origin + new /_vector(0.5, 0.5), direction, src)
for(var/ray/beam_ray/other_ray in past_rays)
if(other_ray.equals(shot_ray))
return //we already went here
@@ -135,8 +135,8 @@ var/list/beam_master = list()
qdel(shot_ray)
/obj/item/projectile/beam/process()
var/vector/origin = atom2vector(starting)
var/vector/direction = atoms2vector(starting, original)
var/_vector/origin = atom2vector(starting)
var/_vector/direction = atoms2vector(starting, original)
fireto(origin, direction)
@@ -149,8 +149,8 @@ var/list/beam_master = list()
//make new ray
var/list/rayCastHit/hit_cache = latest_ray.hit_cache
var/vector/origin = hit_cache[hit_cache.len].point
var/vector/direction = latest_ray.getReboundOnAtom(hit_cache[hit_cache.len])
var/_vector/origin = hit_cache[hit_cache.len].point
var/_vector/direction = latest_ray.getReboundOnAtom(hit_cache[hit_cache.len])
//check if raypath was already traveled
var/ray/temp_ray = new /ray(origin, direction)
@@ -173,8 +173,8 @@ var/list/beam_master = list()
var/ray/beam_ray/latest_ray = past_rays[past_rays.len]
//make new ray
var/vector/origin = atom2vector(dest)
var/vector/direction = latest_ray.direction
var/_vector/origin = atom2vector(dest)
var/_vector/direction = latest_ray.direction
fireto(origin, direction)
shot_from = dest
@@ -184,8 +184,8 @@ var/list/beam_master = list()
src.dir = dir || src.dir
src.starting = starting || loc
var/vector/origin = atom2vector(src.starting)
var/vector/direction = dir2vector(src.dir)
var/_vector/origin = atom2vector(src.starting)
var/_vector/direction = dir2vector(src.dir)
fireto(origin, direction)
// Special laser the captains gun uses
@@ -971,7 +971,7 @@ var/list/laser_tag_vests = list(/obj/item/clothing/suit/tag/redtag, /obj/item/cl
has_splashed = TRUE
..()
//I never want to deal wity ray casts ever again
/obj/item/projectile/beam/liquid_stream/fireto(var/vector/origin, var/vector/direction)//splashes reagents on the turf if the projectile ran out
/obj/item/projectile/beam/liquid_stream/fireto(var/_vector/origin, var/_vector/direction)//splashes reagents on the turf if the projectile ran out
..()
if (reagents && reagents.total_volume && final_turf && !has_splashed)
loc = final_turf

View File

@@ -2331,8 +2331,8 @@
return ..()
// Geometrically checking if we're on a straight line.
var/vector/V = atoms2vector(src, over_location)
var/vector/V_norm = V.normalized()
var/_vector/V = atoms2vector(src, over_location)
var/_vector/V_norm = V.normalized()
if (!V_norm.is_integer())
return ..() // Only a cardinal vector (north, south, east, west) can pass this test
@@ -2343,7 +2343,7 @@
do
temp_turf = temp_turf.get_translated_turf(V_norm)
if (!locate(/obj/structure/table) in temp_turf)
var/vector/V2 = atoms2vector(src, temp_turf)
var/_vector/V2 = atoms2vector(src, temp_turf)
vector_translate(V2, 0.1 SECONDS)
user.visible_message("<span class='warning'>\The [user] slides \the [src] down the table... and straight into the ground!</span>", "<span class='warning'>You slide \the [src] down the table, and straight into the ground!</span>")
create_broken_bottle()

View File

@@ -132,6 +132,7 @@ var/list/all_GPS_list = list()
return FALSE
transmitting = TRUE
update_icon()
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
if("set_tag")
if(isobserver(usr))
@@ -148,9 +149,11 @@ var/list/all_GPS_list = list()
else
gpstag = new_tag
update_name()
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
if("toggle_refresh")
autorefreshing = !autorefreshing
SStgui.try_update_ui(ui.user, src, ui)
return TRUE
// end tgui

View File

@@ -42,7 +42,7 @@
*
* required user mob The mob interacting with the UI.
*
* return list Statuic Data to be sent to the UI.
* return list Static Data to be sent to the UI.
*/
/datum/proc/ui_static_data(mob/user)
return list()
@@ -62,6 +62,17 @@
if(ui)
ui.send_full_update()
/**
* public
*
* Will force an update on static data for all viewers.
* Should be done manually whenever something happens to
* change static data.
*/
/datum/proc/update_static_data_for_all_viewers()
for (var/datum/tgui/window as anything in open_uis)
window.send_full_update()
/**
* public
*
@@ -76,10 +87,20 @@
/datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
SHOULD_CALL_PARENT(TRUE)
INVOKE_EVENT(src, /event/ui_act, usr, action)
// -- FIXME : Should implement the components/signal system to register the ui_act params.
//SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action, params)
// If UI is not interactive or usr calling Topic is not the UI user, bail.
if(!ui || ui.status != UI_INTERACTIVE)
return TRUE
// -- FIXME : Should implement this.
// if(action == "change_ui_state")
// var/mob/living/user = ui.user
// //write_preferences will make sure it's valid for href exploits.
// user.client.prefs.layout_prefs_used = params["new_state"]
/**
* public
*
@@ -147,6 +168,8 @@
*/
/datum/proc/ui_close(mob/user)
SHOULD_NOT_SLEEP(TRUE)
// -- FIXME: Should use signals when we get to implement them.
//SIGNAL_HANDLER
/**
* verb
@@ -207,10 +230,18 @@
context = window_id)
SStgui.force_close_window(usr, window_id)
return TRUE
// Decode payload
var/payload
if(href_list["payload"])
payload = json_decode(href_list["payload"])
var/payload_text = href_list["payload"]
if (!rustg_json_is_valid(payload_text))
log_tgui(usr, "Error: Invalid JSON")
return TRUE
payload = json_decode(payload_text)
// Pass message to window
if(window)
window.on_message(type, payload, href_list)

View File

@@ -63,10 +63,10 @@
// Close UIs if mindless.
if(!client)
return UI_CLOSE
// Disable UIs if unconcious.
// Disable UIs if unconscious.
else if(stat)
return UI_DISABLED
// Update UIs if incapicitated but concious.
// Update UIs if incapicitated but conscious.
else if(incapacitated())
return UI_UPDATE
return UI_INTERACTIVE
@@ -93,7 +93,14 @@
*
* 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, allow_tk = TRUE)
/* Unimplemented : remote code
var/obj/item/item_in_hand = get_active_held_item()
if(istype(item_in_hand, /obj/item/machine_remote)) //snowflake, this lets you interact with all.
var/obj/item/machine_remote/remote = item_in_hand
if(remote.controlling_machine_or_bot == src_object)
return UI_INTERACTIVE
*/
// If the object is obscured, close it.
if(viewcheck && !(src_object in view(src)))
return UI_CLOSE
@@ -110,7 +117,7 @@
// Otherwise, we got nothing.
return UI_CLOSE
/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE)
if(M_TK in mutations)
/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE, allow_tk = TRUE)
if(allow_tk && (M_TK in mutations))
return UI_INTERACTIVE
return ..()

View File

@@ -6,12 +6,25 @@
/**
* tgui state: admin_state
*
* Checks that the user is an admin, end-of-story.
* Checks if the user has specific admin permissions.
*/
var/datum/ui_state/admin_state/admin_state = new
var/list/datum/ui_state/admin_state/admin_states = list()
/datum/ui_state/admin_state
/// The specific admin permissions required for the UI using this state.
VAR_FINAL/required_perms = R_ADMIN
/datum/ui_state/admin_state/New(required_perms = R_ADMIN)
. = ..()
src.required_perms = required_perms
/datum/ui_state/admin_state/can_use_topic(src_object, mob/user)
if(user.check_rights(R_ADMIN))
if(user.check_rights(required_perms))
return UI_INTERACTIVE
return UI_CLOSE
/datum/ui_state/admin_state/variable_edited(variable_name, old_value, new_value)
if(variable_name == NAMEOF(src, required_perms))
return 1 // block var edit
return ..()

View File

@@ -22,7 +22,7 @@ var/datum/ui_state/default/default_state = new
. = shared_ui_interaction(src_object)
if(. > UI_CLOSE && loc) //must not be in nullspace.
. = min(., shared_living_ui_distance(src_object)) // Check the distance...
if(. == UI_INTERACTIVE && !ishigherbeing(src)) // unhandy living mobs can only look, not touch.
if(. == UI_INTERACTIVE && !dexterity_check()) // unhandy living mobs can only look, not touch.
return UI_UPDATE
/mob/living/silicon/robot/default_can_use_topic(src_object)
@@ -42,14 +42,13 @@ var/datum/ui_state/default/default_state = new
return
// The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled.
if(!control_disabled && cameranet.checkTurfVis(get_turf(src_object)))
if(!control_disabled && can_see(src_object))
return UI_INTERACTIVE
return UI_CLOSE
/mob/living/silicon/pai/default_can_use_topic(src_object)
// pAIs can only use themselves and itself.
var/atom/src_atom = src_object
if((src_object == src || istype(src_atom) && src_atom.loc == src) && !stat)
// pAIs can only use themselves and the owner's radio.
if((src_object == src || src_object == radio) && !stat)
return UI_INTERACTIVE
else
return min(..(), UI_UPDATE)

View File

@@ -0,0 +1,14 @@
/**
* tgui state: greyscale menu
*
* Checks that the target var of the greyscale menu meets the default can_use_topic criteria
*/
var/datum/ui_state/greyscale_menu_state/greyscale_menu_state = new
/datum/ui_state/greyscale_menu_state/can_use_topic(src_object, mob/user)
var/datum/greyscale_modify_menu/menu = src_object
if(!isatom(menu.target))
return UI_INTERACTIVE
return global.default_state.can_use_topic(menu.target, user)

View File

@@ -20,7 +20,7 @@ var/datum/ui_state/hands_state/hands_state = new
return UI_CLOSE
/mob/living/hands_can_use_topic(src_object)
if(is_holding_item(src_object))
if(get_active_hand() == src_object)
return UI_INTERACTIVE
return UI_CLOSE

View File

@@ -0,0 +1,12 @@
/**
* tgui state: view
*
* Checks if the object is in view or the mob is holding it, otherwise close the UI
*/
var/datum/ui_state/hold_or_view_state/hold_or_view_state = new
/datum/ui_state/hold_or_view_state/can_use_topic(src_object, mob/user)
if((user in viewers(user.client?.view, src_object)) || user.is_holding(src_object))
return UI_INTERACTIVE
return UI_CLOSE

View File

@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: language_menu_state
*/
/// -- /vg/: unticked, need to be reimplemented
var/datum/ui_state/language_menu_state/language_menu_state = new
/datum/ui_state/language_menu/can_use_topic(src_object, mob/user)
. = UI_CLOSE
if(user.client.holder.check_rights(R_ADMIN))
. = UI_INTERACTIVE
else if(istype(src_object, /datum/language_menu))
var/datum/language_menu/my_languages = src_object
if(my_languages.language_holder.owner == user)
. = UI_INTERACTIVE

View File

@@ -1,13 +1,10 @@
/**
* tgui state: new_player_state
*
* Checks that the user is a new_player, or if user is an admin
* Checks that the user is a /mob/dead/new_player
*/
var/datum/ui_state/new_player_state/new_player_state = new
/datum/ui_state/new_player_state/can_use_topic(src_object, mob/user)
if(isnewplayer(user) || user.check_rights(R_ADMIN))
return UI_INTERACTIVE
return UI_CLOSE
return isnewplayer(user) ? UI_INTERACTIVE : UI_CLOSE

View File

@@ -10,13 +10,14 @@
*/
var/datum/ui_state/not_incapacitated_state/not_incapacitated_state = new
/**
* tgui state: not_incapacitated_turf_state
*
* Checks that the user isn't incapacitated and that their loc is a turf
*/
var/datum/ui_state/not_incapacitated_state/not_incapacitated_turf_state = new(no_turfs = TRUE)
var/datum/ui_state/not_incapacitated_state/not_incapacitated_turf_state = new
/datum/ui_state/not_incapacitated_state
var/turf_check = FALSE

View File

@@ -27,3 +27,6 @@ var/datum/ui_state/notcontained_state/notcontained_state = new
/mob/living/silicon/notcontained_can_use_topic(src_object)
return default_can_use_topic(src_object) // Silicons use default bevhavior.
/mob/living/basic/drone/notcontained_can_use_topic(src_object)
return default_can_use_topic(src_object) // Drones use default bevhavior.

View File

@@ -0,0 +1,15 @@
/**
* tgui state: standing_state
*
* Checks that the user isn't incapacitated and is standing upright
*/
/datum/ui_state/not_incapacitated_state/standing = new
/datum/ui_state/not_incapacitated_state/standing/can_use_topic(src_object, mob/user)
if (!isliving(user))
return ..()
var/mob/living/living_user = user
if (living_user.body_position)
return UI_DISABLED
return ..()

View File

@@ -15,10 +15,10 @@
/// far away users will be able to see, and anyone farther won't see anything.
/// Dead users will receive updates no matter what, though you likely want to add
/// a [`ui_status_only_living`] check for finer observer interactions.
/proc/ui_status_user_is_adjacent(mob/user, atom/source)
/proc/ui_status_user_is_adjacent(mob/user, atom/source, allow_tk = TRUE)
if (isliving(user))
var/mob/living/living_user = user
return living_user.shared_living_ui_distance(source)
return living_user.shared_living_ui_distance(source, allow_tk = allow_tk)
else
return UI_UPDATE
@@ -33,9 +33,8 @@
return UI_INTERACTIVE
// Regular ghosts can always at least view if in range.
var/client/client = user.client
if(client)
var/clientviewlist = getviewsize(client.view)
if(user.client)
var/clientviewlist = getviewsize(user.client.view)
if(get_dist(source, user) < max(clientviewlist[1], clientviewlist[2]))
return UI_UPDATE
@@ -50,13 +49,14 @@
/// Returns a UI status such that those without blocked hands will be able to interact,
/// but everyone else can only watch.
/proc/ui_status_user_has_free_hands(mob/user, atom/source)
return user.find_empty_hand_index() ? UI_INTERACTIVE : UI_UPDATE
/// TODO: implement (allowed source)
/proc/ui_status_user_has_free_hands(mob/user, atom/source, allowed_source)
return user.find_empty_hand_index() ? UI_UPDATE : UI_INTERACTIVE
/// Returns a UI status such that advanced tool users will be able to interact,
/// but everyone else can only watch.
/proc/ui_status_user_is_advanced_tool_user(mob/user)
return ishigherbeing(user) ? UI_INTERACTIVE : UI_UPDATE
return user.dexterity_check() ? UI_INTERACTIVE : UI_UPDATE
/// Returns a UI status such that silicons will be able to interact with whatever
/// they would have access to if this was a machine. For example, AIs can
@@ -81,13 +81,13 @@
/mob/living/silicon/ai/get_ui_access(atom/source)
// The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled.
if(!control_disabled && cameranet.checkTurfVis(get_turf(source)))
if(!control_disabled && can_see(source))
return UI_INTERACTIVE
return UI_CLOSE
/mob/living/silicon/pai/get_ui_access(atom/source)
// pAIs can only use themselves and their contents.
if((source == src || source.loc == src) && !stat)
// pAIs can only use themselves and the owner's radio.
if((source == src || source == radio) && !stat)
return UI_INTERACTIVE
else
return UI_CLOSE
@@ -99,7 +99,7 @@
return UI_UPDATE
var/mob/living/living_user = user
return (living_user.lying && living_user.stat == CONSCIOUS) \
return ((living_user.resting > 0) && living_user.stat == CONSCIOUS) \
? UI_INTERACTIVE \
: UI_UPDATE
@@ -110,3 +110,11 @@
return UI_CLOSE
return UI_INTERACTIVE
/// Return UI_INTERACTIVE if the user is inside the target atom, whether they can see it or not.
/// Return UI_CLOSE otherwise.
/proc/ui_status_user_inside(mob/user, atom/target)
if(target.contains(user))
return UI_INTERACTIVE
return UI_CLOSE

View File

@@ -11,7 +11,7 @@
var/mob/user
/// The object which owns the UI.
var/datum/src_object
/// The title of te UI.
/// The title of the UI.
var/title
/// The window_id for browse() and onclose().
var/datum/tgui_window/window
@@ -31,8 +31,12 @@
var/closing = FALSE
/// The status/visibility of the UI.
var/status = UI_INTERACTIVE
/// Timed refreshing state
var/refreshing = FALSE
/// Topic state used to determine status/interactability.
var/datum/ui_state/state = null
/// Rate limit client refreshes to prevent DoS.
var/refresh_cooldown = 0
/**
* public
@@ -54,7 +58,7 @@
src_object = src_object)
src.user = user
src.src_object = src_object
src.window_key = "[ref(src_object)]-main"
src.window_key = "\ref[src_object]-main"
src.interface = interface
if(title)
src.title = title
@@ -90,15 +94,14 @@
window.acquire_lock(src)
if(!window.is_ready())
window.initialize(
strict_mode = TRUE,
fancy = user.client.prefs.tgui_fancy,
inline_assets = list(/datum/asset/simple/tgui)
)
assets = list(
get_tg_asset_datum(/datum/tg_asset/simple/tgui),
))
else
window.send_message("ping")
window.send_asset(/datum/asset/simple/fontawesome)
window.send_asset(/datum/asset/simple/tgfont)
for(var/asset in src_object.ui_assets(user))
window.send_asset(asset)
send_assets()
window.send_message("update", get_payload(
with_data = TRUE,
with_static_data = TRUE))
@@ -106,6 +109,30 @@
return TRUE
/datum/tgui/proc/send_assets()
var/flush_queue = window.send_asset(get_tg_asset_datum(
/datum/tg_asset/simple/fontawesome))
flush_queue |= window.send_asset(get_tg_asset_datum(
/datum/tg_asset/simple/tgfont))
flush_queue |= window.send_asset(get_tg_asset_datum(
/datum/tg_asset/json/icon_ref_map))
for(var/datum/tg_asset/asset in src_object.ui_assets(user))
flush_queue |= window.send_asset(asset)
// -- Legacy code for /vg/ style spritesheet datums --
// TOFIX!!! One thing at the time..
for(var/asset_type in src_object.ui_assets(user))
window.sent_assets |= list(asset_type)
var/datum/asset/instance = get_asset_datum(asset_type)
instance.send(window.client)
if(istype(instance, /datum/asset/spritesheet))
var/datum/asset/spritesheet/spritesheet = instance
window.send_message("asset/stylesheet", spritesheet.css_filename())
window.send_raw_message(TGUI_CREATE_MESSAGE("asset/mappings", instance.get_url_mappings()))
// -- END legacy code for /vg/ style spritesheet datums --
if (flush_queue)
user.client.tg_browse_queue_flush()
/**
* public
*
@@ -175,11 +202,17 @@
/datum/tgui/proc/send_full_update(custom_data, force)
if(!user.client || !initialized || closing)
return
if(!COOLDOWN_FINISHED(src, refresh_cooldown))
refreshing = TRUE
add_timer(new /callback(src, PROC_REF(send_full_update), custom_data, force), COOLDOWN_TIMELEFT(src, refresh_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
@@ -209,12 +242,17 @@
json_data["config"] = list(
"title" = title,
"status" = status,
"interface" = interface,
"interface" = list(
"name" = interface,
"layout" = user.client.prefs.layout_prefs_used,
),
"refreshing" = refreshing,
"window" = list(
"key" = window_key,
"size" = window_size,
"fancy" = user.client.prefs.tgui_fancy,
"locked" = TRUE,
"locked" = user.client.prefs.tgui_lock,
"scale" = user.client.prefs.tgui_scale,
),
"client" = list(
"ckey" = user.client.ckey,
@@ -242,12 +280,12 @@
* Run an update cycle for this UI. Called internally by SStgui
* every second or so.
*/
/datum/tgui/proc/process(delta_time, force = FALSE)
/datum/tgui/proc/process(seconds_per_tick, force = FALSE)
if(closing)
return
var/datum/host = src_object.ui_host(user)
// If the object or user died (or something else), abort.
if(!src_object || !host || !user || !window)
if(QDELETED(src_object) || QDELETED(host) || QDELETED(user) || QDELETED(window))
close(can_be_suspended = FALSE)
return
// Validate ping
@@ -297,8 +335,11 @@
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("pingReply")
if("ping/reply")
initialized = TRUE
if("suspend")
close(can_be_suspended = TRUE)
@@ -310,7 +351,13 @@
if("setSharedState")
if(status != UI_INTERACTIVE)
return
if(!src_object.tgui_shared_states)
src_object.tgui_shared_states = list()
LAZYINITLIST(src_object.tgui_shared_states)
src_object.tgui_shared_states[href_list["key"]] = href_list["value"]
SStgui.update_uis(src_object)
/// Wrapper for behavior to potentially wait until the next tick if the server is overloaded
/datum/tgui/proc/on_act_message(act_type, payload, state)
if(QDELETED(src) || QDELETED(src_object))
return
if(src_object.ui_act(act_type, payload, src, state))
SStgui.update_uis(src_object)

View File

@@ -1,164 +0,0 @@
/**
* Creates a TGUI alert window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the alert to.
* * message - The content of the alert, shown in the body of the TGUI window.
* * title - The of the alert modal, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
* * autofocus - The bool that controls if this alert should grab window focus.
*/
/proc/tgui_alert(mob/user, message = null, title = null, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout, autofocus)
alert.tgui_interact(user)
alert.wait()
if (alert)
. = alert.choice
qdel(alert)
/**
* Creates an asynchronous TGUI alert window with an associated callback.
*
* This proc should be used to create alerts that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the alert to.
* * message - The content of the alert, shown in the body of the TGUI window.
* * title - The of the alert modal, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise.
* * autofocus - The bool that controls if this alert should grab window focus.
*/
/proc/tgui_alert_async(mob/user, message = null, title = null, list/buttons = list("Ok"), callback/callback, timeout = 0, autofocus = TRUE)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout, autofocus)
alert.tgui_interact(user)
/**
* # tgui_modal
*
* Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
* a message and has buttons for responses.
*/
/datum/tgui_modal
/// The title of the TGUI window
var/title
/// The textual body of the TGUI window
var/message
/// The list of buttons (responses) provided on the TGUI window
var/list/buttons
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The time at which the tgui_modal was created, for displaying timeout progress.
var/start_time
/// The lifespan of the tgui_modal, after which the window will close and delete itself.
var/timeout
/// The bool that controls if this modal should grab window focus
var/autofocus
/// Boolean field describing if the tgui_modal was closed by the user.
var/closed
/datum/tgui_modal/New(mob/user, message, title, list/buttons, timeout, autofocus)
src.title = title
src.message = message
src.buttons = buttons.Copy()
src.autofocus = autofocus
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/datum/tgui_modal/Destroy(force, ...)
SStgui.close_uis(src)
QDEL_NULL(buttons)
. = ..()
/**
* Waits for a user's response to the tgui_modal's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_modal/proc/wait()
while (!choice && !closed && !src.gcDestroyed)
stoplag()
/datum/tgui_modal/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AlertModal")
ui.open()
/datum/tgui_modal/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_modal/ui_state(mob/user)
return global.always_state
/datum/tgui_modal/ui_data(mob/user)
. = list(
"title" = title,
"message" = message,
"buttons" = buttons,
"autofocus" = autofocus
)
if(timeout)
.["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
/datum/tgui_modal/ui_act(action, list/params)
. = ..()
if (.)
return
switch(action)
if("choose")
if (!(params["choice"] in buttons))
return
set_choice(params["choice"])
SStgui.close_uis(src)
return TRUE
/datum/tgui_modal/proc/set_choice(choice)
src.choice = choice
/**
* # async tgui_modal
*
* An asynchronous version of tgui_modal to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_modal/async
/// The callback to be invoked by the tgui_modal upon having a choice made.
var/callback/callback
/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus)
..(user, message, title, buttons, timeout, autofocus)
src.callback = callback
/datum/tgui_modal/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_modal/async/set_choice(choice)
. = ..()
if(!isnull(src.choice))
callback?.invoke_async(src.choice)
/datum/tgui_modal/async/wait()
return

View File

@@ -1,186 +0,0 @@
/**
* Creates a TGUI input list window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_list(mob/user, message, title, list/buttons, timeout = 0)
if (!user)
user = usr
if(!length(buttons))
return
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_list_input/input = new(user, message, title, buttons, timeout)
input.tgui_interact(user)
input.wait()
if (input)
. = input.choice
qdel(input)
/**
* Creates an asynchronous TGUI input list window with an associated callback.
*
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * callback - The callback to be invoked when a choice is made.
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_list_async(mob/user, message, title, list/buttons, callback/callback, timeout = 60 SECONDS)
if (!user)
user = usr
if(!length(buttons))
return
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_list_input/async/input = new(user, message, title, buttons, callback, timeout)
input.tgui_interact(user)
/**
* # tgui_list_input
*
* Datum used for instantiating and using a TGUI-controlled list input that prompts the user with
* a message and shows a list of selectable options
*/
/datum/tgui_list_input
/// The title of the TGUI window
var/title
/// The textual body of the TGUI window
var/message
/// The list of buttons (responses) provided on the TGUI window
var/list/buttons
/// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
var/list/buttons_map
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The time at which the tgui_list_input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the tgui_list_input, after which the window will close and delete itself.
var/timeout
/// Boolean field describing if the tgui_list_input was closed by the user.
var/closed
/datum/tgui_list_input/New(mob/user, message, title, list/buttons, timeout)
src.title = title
src.message = message
src.buttons = list()
src.buttons_map = list()
var/list/repeat_buttons = list()
// Gets rid of illegal characters
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
for(var/i in buttons)
var/string_key = whitelistedWords.Replace("[i]", "")
//avoids duplicated keys E.g: when areas have the same name
string_key = avoid_assoc_duplicate_keys(string_key, repeat_buttons)
src.buttons += string_key
src.buttons_map[string_key] = i
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/datum/tgui_list_input/Destroy()
SStgui.close_uis(src)
QDEL_NULL(buttons)
. = ..()
/**
* Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_list_input/proc/wait()
while (!choice && !closed)
stoplag(1)
/datum/tgui_list_input/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ListInput")
ui.open()
/datum/tgui_list_input/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_list_input/ui_state(mob/user)
return global.always_state
/datum/tgui_list_input/ui_static_data(mob/user)
. = list(
"title" = title,
"message" = message,
"buttons" = buttons
)
/datum/tgui_list_input/ui_data(mob/user)
. = list()
if(timeout)
.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
/datum/tgui_list_input/ui_act(action, list/params)
. = ..()
if (.)
return
switch(action)
if("choose")
if (!(params["choice"] in buttons))
return
set_choice(buttons_map[params["choice"]])
SStgui.close_uis(src)
return TRUE
if("cancel")
SStgui.close_uis(src)
closed = TRUE
return TRUE
/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice
/**
* # async tgui_list_input
*
* An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_list_input/async
/// The callback to be invoked by the tgui_list_input upon having a choice made.
var/callback/callback
/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, callback, timeout)
..(user, message, title, buttons, timeout)
src.callback = callback
/datum/tgui_list_input/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()
/datum/tgui_list_input/async/set_choice(choice)
. = ..()
if(!isnull(src.choice))
callback?.invoke_async(src.choice)
/datum/tgui_list_input/async/wait()
return

View File

@@ -11,6 +11,7 @@
var/is_browser = FALSE
var/status = TGUI_WINDOW_CLOSED
var/locked = FALSE
var/visible = FALSE
var/datum/tgui/locked_by
var/datum/subscriber_object
var/subscriber_delegate
@@ -18,8 +19,14 @@
var/message_queue
var/sent_assets = list()
// Vars passed to initialize proc (and saved for later)
var/inline_assets
var/fancy
var/initial_strict_mode
var/initial_fancy
var/initial_assets
var/initial_inline_html
var/initial_inline_js
var/initial_inline_css
var/list/oversized_payloads = list()
/**
* public
@@ -44,21 +51,30 @@
* 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.
* optional inline_html string Custom HTML to inject.
* optional fancy bool If TRUE, will hide the window titlebar.
* optional strict_mode bool - Enables strict error handling and BSOD.
* optional fancy bool - If TRUE and if this is NOT a panel, will hide the window titlebar.
* optional assets list - List of assets to load during initialization.
* optional inline_html string - Custom HTML to inject.
* optional inline_js string - Custom JS to inject.
* optional inline_css string - Custom CSS to inject.
*/
/datum/tgui_window/initialize(
inline_assets = list(),
strict_mode = FALSE,
fancy = FALSE,
assets = list(),
inline_html = "",
fancy = FALSE)
inline_js = "",
inline_css = "")
log_tgui(client,
context = "[id]/initialize",
window = src)
if(!client)
return
src.inline_assets = inline_assets
src.fancy = fancy
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
@@ -71,11 +87,11 @@
// Generate page html
var/html = SStgui.basehtml
html = replacetextEx(html, "\[tgui:windowId]", id)
// Inject inline assets
html = replacetextEx(html, "\[tgui:strictMode]", strict_mode)
// Inject assets
var/inline_assets_str = ""
for(var/asset in inline_assets)
var/datum/asset/instance = new asset
var/mappings = instance.get_url_mappings()
for(var/datum/tg_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
@@ -83,12 +99,21 @@
inline_assets_str += "Byond.loadCss('[url]', true);\n"
else if(copytext(name, -3) == ".js")
inline_assets_str += "Byond.loadJs('[url]', true);\n"
send_asset(asset)
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 custom HTML
html = replacetextEx(html, "<!-- tgui:html -->\n", inline_html)
// Inject inline HTML
if (inline_html)
html = replacetextEx(html, "<!-- tgui:inline-html -->", isfile(inline_html) ? file2text(inline_html) : inline_html)
// Inject inline JS
if (inline_js)
inline_js = "<script>\n'use strict';\n[isfile(inline_js) ? file2text(inline_js) : inline_js]\n</script>"
html = replacetextEx(html, "<!-- tgui:inline-js -->", inline_js)
// Inject inline CSS
if (inline_css)
inline_css = "<style>\n[isfile(inline_css) ? file2text(inline_css) : 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
@@ -97,6 +122,23 @@
if(!is_browser)
winset(client, id, "on-close=\"uiclose [id]\"")
/**
* public
*
* Reinitializes the panel with previous data used for initialization.
*/
/datum/tgui_window/proc/reinitialize()
initialize(
strict_mode = initial_strict_mode,
fancy = initial_fancy,
assets = initial_assets,
inline_html = initial_inline_html,
inline_js = initial_inline_js,
inline_css = initial_inline_css)
// Resend assets
for(var/datum/tg_asset/asset in sent_assets)
send_asset(asset)
/**
* public
*
@@ -184,6 +226,7 @@
log_tgui(client,
context = "[id]/close (suspending)",
window = src)
visible = FALSE
status = TGUI_WINDOW_READY
send_message("suspend")
return
@@ -191,6 +234,7 @@
context = "[id]/close",
window = src)
release_lock()
visible = FALSE
status = TGUI_WINDOW_CLOSED
message_queue = null
// Do not close the window to give user some time
@@ -251,16 +295,17 @@
*
* return bool - TRUE if any assets had to be sent to the client
*/
/datum/tgui_window/proc/send_asset(datum/asset/asset)
/datum/tgui_window/proc/send_asset(datum/tg_asset/asset)
if(!client || !asset)
return
sent_assets |= list(asset)
var/datum/asset/instance = get_asset_datum(asset)
instance.send(client)
if(istype(instance, /datum/asset/spritesheet))
var/datum/asset/spritesheet/spritesheet = instance
. = asset.send(client)
/* FIXME : TG CSS
if(istype(asset, /datum/tg_asset/spritesheet))
var/datum/tg_asset/spritesheet/spritesheet = asset
send_message("asset/stylesheet", spritesheet.css_filename())
send_raw_message(TGUI_CREATE_MESSAGE("asset/mappings", instance.get_url_mappings()))
*/
send_raw_message(asset.get_serialized_url_mappings())
/**
* private
@@ -276,6 +321,18 @@
: "[id].browser:update")
message_queue = null
/**
* public
*
* Replaces the inline HTML content.
*
* required inline_html string HTML to inject
*/
/datum/tgui_window/proc/replace_html(inline_html = "")
client << output(url_encode(inline_html), is_browser \
? "[id]:replaceHtml" \
: "[id].browser:replaceHtml")
/**
* private
*
@@ -310,7 +367,12 @@
// If not locked, handle these message types
switch(type)
if("ping")
send_message("pingReply", payload)
send_message("ping/reply", payload)
/*
if("visible")
visible = TRUE
SEND_SIGNAL(src, COMSIG_TGUI_WINDOW_VISIBLE, client)
*/
if("suspend")
close(can_be_suspended = TRUE)
if("close")
@@ -318,8 +380,51 @@
if("openLink")
client << link(href_list["url"])
if("cacheReloaded")
// Reinitialize
initialize(inline_assets = inline_assets, fancy = fancy)
// Resend the assets
for(var/asset in sent_assets)
send_asset(asset)
reinitialize()
/* Unimplemented
if("chat/resend")
SSchat.handle_resend(client, payload)
*/
if("oversizedPayloadRequest")
var/payload_id = payload["id"]
var/chunk_count = payload["chunkCount"]
var/permit_payload = chunk_count <= config.tgui_max_chunk_count
if(permit_payload)
create_oversized_payload(payload_id, payload["type"], chunk_count)
send_message("oversizePayloadResponse", list("allow" = permit_payload, "id" = payload_id))
if("payloadChunk")
var/payload_id = payload["id"]
append_payload_chunk(payload_id, payload["chunk"])
send_message("acknowlegePayloadChunk", list("id" = payload_id))
/datum/tgui_window/variable_edited(variable_name, old_value, new_value)
return (variable_name != NAMEOF(src, id)) && ..()
/datum/tgui_window/proc/create_oversized_payload(payload_id, message_type, chunk_count)
if(oversized_payloads[payload_id])
stack_trace("Attempted to create oversized tgui payload with duplicate ID.")
return
oversized_payloads[payload_id] = list(
"type" = message_type,
"count" = chunk_count,
"chunks" = list(),
"timeout" = add_timer(new /callback(src, PROC_REF(remove_oversized_payload), payload_id), 1 SECONDS)
)
/datum/tgui_window/proc/append_payload_chunk(payload_id, chunk)
var/list/payload = oversized_payloads[payload_id]
if(!payload)
return
var/list/chunks = payload["chunks"]
chunks += chunk
if(length(chunks) >= payload["count"])
del_timer(payload["timeout"])
var/message_type = payload["type"]
var/final_payload = chunks.Join()
remove_oversized_payload(payload_id)
on_message(message_type, json_decode(final_payload), list("type" = message_type, "payload" = final_payload, "tgui" = TRUE, "window_id" = id))
else
payload["timeout"] = add_timer(new /callback(src, PROC_REF(remove_oversized_payload), payload_id), 1 SECONDS)
/datum/tgui_window/proc/remove_oversized_payload(payload_id)
oversized_payloads -= payload_id

View File

@@ -0,0 +1,142 @@
/**
* Creates a TGUI alert window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the alert to.
* * message - The content of the alert, shown in the body of the TGUI window.
* * title - The of the alert modal, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
* * autofocus - The bool that controls if this alert should grab window focus.
*/
/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE, ui_state = global.always_state)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return null
if(isnull(user.client))
return null
// A gentle nudge - you should not be using TGUI alert for anything other than a simple message.
if(length(buttons) > 3)
log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert")
return tgui_input_list(user, message, title, buttons, timeout, autofocus)
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input)
if(length(buttons) == 2)
return alert(user, message, title, buttons[1], buttons[2])
if(length(buttons) == 3)
return alert(user, message, title, buttons[1], buttons[2], buttons[3])
var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus, ui_state)
alert.ui_interact(user)
alert.wait()
if (alert)
. = alert.choice
qdel(alert)
/**
* # tgui_alert
*
* Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
* a message and has buttons for responses.
*/
/datum/tgui_alert
/// The title of the TGUI window
var/title
/// The textual body of the TGUI window
var/message
/// The list of buttons (responses) provided on the TGUI window
var/list/buttons
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The time at which the tgui_alert was created, for displaying timeout progress.
var/start_time
/// The lifespan of the tgui_alert, after which the window will close and delete itself.
var/timeout
/// The bool that controls if this modal should grab window focus
var/autofocus
/// Boolean field describing if the tgui_alert was closed by the user.
var/closed
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus, ui_state)
src.autofocus = autofocus
src.buttons = buttons.Copy()
src.message = message
src.title = title
src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/datum/tgui_alert/Destroy(force)
SStgui.close_uis(src)
state = null
buttons?.Cut()
return ..()
/**
* Waits for a user's response to the tgui_alert's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_alert/proc/wait()
while (!choice && !closed && !QDELETED(src))
stoplag(1)
/datum/tgui_alert/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AlertModal")
ui.open()
/datum/tgui_alert/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_alert/ui_state(mob/user)
return state
/datum/tgui_alert/ui_static_data(mob/user)
var/list/data = list()
data["autofocus"] = autofocus
data["buttons"] = buttons
data["message"] = message
data["large_buttons"] = user.client.prefs.tgui_input_large
data["swapped_buttons"] = user.client.prefs.tgui_input_swapped
data["title"] = title
return data
/datum/tgui_alert/ui_data(mob/user)
var/list/data = list()
if(timeout)
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_alert/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("choose")
if (!(params["choice"] in buttons))
CRASH("[usr] entered a non-existent button choice: [params["choice"]]")
set_choice(params["choice"])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
/datum/tgui_alert/proc/set_choice(choice)
src.choice = choice

View File

@@ -0,0 +1,140 @@
/**
* ### tgui_input_checkbox
* Opens a window with a list of checkboxes and returns a list of selected choices.
*
* user - The mob to display the window to
* message - The message inside the window
* title - The title of the window
* list/items - The list of items to display
* min_checked - The minimum number of checkboxes that must be checked (defaults to 1)
* max_checked - The maximum number of checkboxes that can be checked (optional)
* timeout - The timeout for the input (optional)
*/
/proc/tgui_input_checkboxes(mob/user, message, title = "Select", list/items, min_checked = 1, max_checked = 50, timeout = 0, ui_state = global.always_state)
if (!user)
user = usr
if(!length(items))
return null
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return null
if(isnull(user.client))
return null
if(!user.client.prefs.tgui_input)
return input(user, message, title) as null|anything in items
var/datum/tgui_checkbox_input/input = new(user, message, title, items, min_checked, max_checked, timeout, ui_state)
input.ui_interact(user)
input.wait()
if (input)
. = input.choices
qdel(input)
/// Window for tgui_input_checkboxes
/datum/tgui_checkbox_input
/// Title of the window
var/title
/// Message to display
var/message
/// List of items to display
var/list/items
/// List of selected items
var/list/choices
/// Time when the input was created
var/start_time
/// Timeout for the input
var/timeout
/// Whether the input was closed
var/closed
/// Minimum number of checkboxes that must be checked
var/min_checked
/// Maximum number of checkboxes that can be checked
var/max_checked
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
/datum/tgui_checkbox_input/New(mob/user, message, title, list/items, min_checked, max_checked, timeout, ui_state)
src.title = title
src.message = message
src.items = items.Copy()
src.min_checked = min_checked
src.max_checked = max_checked
src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/datum/tgui_checkbox_input/Destroy(force)
SStgui.close_uis(src)
state = null
items?.Cut()
return ..()
/datum/tgui_checkbox_input/proc/wait()
while (!closed && !QDELETED(src))
stoplag(1)
/datum/tgui_checkbox_input/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "CheckboxInput")
ui.open()
/datum/tgui_checkbox_input/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_checkbox_input/ui_state(mob/user)
return state
/datum/tgui_checkbox_input/ui_data(mob/user)
var/list/data = list()
if(timeout)
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_checkbox_input/ui_static_data(mob/user)
var/list/data = list()
data["items"] = items
data["min_checked"] = min_checked
data["max_checked"] = max_checked
data["large_buttons"] = user.client.prefs.tgui_input_large
data["message"] = message
data["swapped_buttons"] = user.client.prefs.tgui_input_swapped
data["title"] = title
return data
/datum/tgui_checkbox_input/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("submit")
var/list/selections = params["entry"]
if(length(selections) >= min_checked && length(selections) <= max_checked)
set_choices(selections)
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
return FALSE
/datum/tgui_checkbox_input/proc/set_choices(list/selections)
src.choices = selections.Copy()

View File

@@ -0,0 +1,127 @@
/**
* Creates a TGUI window with a key input. Returns the user's response as a full key with modifiers, eg ShiftK.
*
* This proc should be used to create windows for key entry that the caller will wait for a response from.
* If tgui fancy chat is turned off: Will return a normal input.
*
* Arguments:
* * user - The user to show the number input to.
* * message - The content of the number input, shown in the body of the TGUI window.
* * title - The title of the number input modal, shown on the top of the TGUI window.
* * default - The default (or current) key, shown as a placeholder.
*/
/proc/tgui_input_keycombo(mob/user = usr, message, title = "Key Input", default = 0, timeout = 0, ui_state = global.always_state)
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return null
if (isnull(user.client))
return null
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input)
var/input_key = input(user, message, title + "(Modifiers are TGUI only, sorry!)", default) as null|text
return input_key[1]
var/datum/tgui_input_keycombo/key_input = new(user, message, title, default, timeout, ui_state)
key_input.ui_interact(user)
key_input.wait()
if (key_input)
. = key_input.entry
qdel(key_input)
/**
* # tgui_input_keycombo
*
* Datum used for instantiating and using a TGUI-controlled key input that prompts the user with
* a message and listens for key presses.
*/
/datum/tgui_input_keycombo
/// Boolean field describing if the tgui_input_number was closed by the user.
var/closed
/// The default (or current) value, shown as a default. Users can press reset with this.
var/default
/// The entry that the user has return_typed in.
var/entry
/// The prompt's body, if any, of the TGUI window.
var/message
/// The time at which the number input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the number input, after which the window will close and delete itself.
var/timeout
/// The title of the TGUI window
var/title
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
/datum/tgui_input_keycombo/New(mob/user, message, title, default, timeout, ui_state)
src.default = default
src.message = message
src.title = title
src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/datum/tgui_input_keycombo/Destroy(force)
SStgui.close_uis(src)
state = null
return ..()
/**
* Waits for a user's response to the tgui_input_keycombo's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_input_keycombo/proc/wait()
while (!entry && !closed && !QDELETED(src))
stoplag(1)
/datum/tgui_input_keycombo/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "KeyComboModal")
ui.open()
/datum/tgui_input_keycombo/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_input_keycombo/ui_state(mob/user)
return state
/datum/tgui_input_keycombo/ui_static_data(mob/user)
var/list/data = list()
data["init_value"] = default // Default is a reserved keyword
data["large_buttons"] = user.client.prefs.tgui_input_large
data["message"] = message
data["swapped_buttons"] = user.client.prefs.tgui_input_swapped
data["title"] = title
return data
/datum/tgui_input_keycombo/ui_data(mob/user)
var/list/data = list()
if(timeout)
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_input_keycombo/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("submit")
set_entry(params["entry"])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
/datum/tgui_input_keycombo/proc/set_entry(entry)
src.entry = entry

View File

@@ -0,0 +1,160 @@
/**
* Creates a TGUI input list window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * items - The options that can be chosen by the user, each string is assigned a button on the UI.
* * default - If an option is already preselected on the UI. Current values, etc.
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0, ui_state = global.always_state)
if (!user)
user = usr
if(!length(items))
return null
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return null
if(isnull(user.client))
return null
/// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input)
return input(user, message, title, default) as null|anything in items
var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout, ui_state)
if(input.invalid)
qdel(input)
return
input.ui_interact(user)
input.wait()
if (input)
. = input.choice
qdel(input)
/**
* # tgui_list_input
*
* Datum used for instantiating and using a TGUI-controlled list input that prompts the user with
* a message and shows a list of selectable options
*/
/datum/tgui_list_input
/// The title of the TGUI window
var/title
/// The textual body of the TGUI window
var/message
/// The list of items (responses) provided on the TGUI window
var/list/items
/// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
var/list/items_map
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The default button to be selected
var/default
/// The time at which the tgui_list_input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the tgui_list_input, after which the window will close and delete itself.
var/timeout
/// Boolean field describing if the tgui_list_input was closed by the user.
var/closed
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
/// Whether the tgui list input is invalid or not (i.e. due to all list entries being null)
var/invalid = FALSE
/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout, ui_state)
src.title = title
src.message = message
src.items = list()
src.items_map = list()
src.default = default
src.state = ui_state
var/list/repeat_items = list()
// Gets rid of illegal characters
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
for(var/i in items)
if(!i)
continue
var/string_key = whitelistedWords.Replace("[i]", "")
//avoids duplicated keys E.g: when areas have the same name
string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
src.items += string_key
src.items_map[string_key] = i
if(length(src.items) == 0)
invalid = TRUE
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/datum/tgui_list_input/Destroy(force)
SStgui.close_uis(src)
state = null
items?.Cut()
items_map?.Cut()
return ..()
/**
* Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_list_input/proc/wait()
while (!choice && !closed)
stoplag(1)
/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ListInputWindow")
ui.open()
/datum/tgui_list_input/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_list_input/ui_state(mob/user)
return state
/datum/tgui_list_input/ui_static_data(mob/user)
var/list/data = list()
data["init_value"] = default || items[1]
data["items"] = items
data["large_buttons"] = user.client.prefs.tgui_input_large
data["message"] = message
data["swapped_buttons"] = user.client.prefs.tgui_input_swapped
data["title"] = title
return data
/datum/tgui_list_input/ui_data(mob/user)
var/list/data = list()
if(timeout)
data["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
return data
/datum/tgui_list_input/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("submit")
if (!(params["entry"] in items))
return
set_choice(items_map[params["entry"]])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice

View File

@@ -0,0 +1,163 @@
/**
* Creates a TGUI window with a number input. Returns the user's response as num | null.
*
* This proc should be used to create windows for number entry that the caller will wait for a response from.
* If tgui fancy chat is turned off: Will return a normal input. If a max or min value is specified, will
* validate the input inside the UI and ui_act.
*
* Arguments:
* * user - The user to show the number input to.
* * message - The content of the number input, shown in the body of the TGUI window.
* * title - The title of the number input modal, shown on the top of the TGUI window.
* * default - The default (or current) value, shown as a placeholder. Users can press refresh with this.
* * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000.
* * min_value - Specifies a minimum value. Often 0.
* * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
* * round_value - whether the inputted number is rounded down into an integer.
*/
/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE, ui_state = global.always_state)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return null
if (isnull(user.client))
return null
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input)
var/input_number = input(user, message, title, default) as null|num
return clamp(round_value ? round(input_number) : input_number, min_value, max_value)
var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value, ui_state)
number_input.ui_interact(user)
number_input.wait()
if (number_input)
. = number_input.entry
qdel(number_input)
/**
* # tgui_input_number
*
* Datum used for instantiating and using a TGUI-controlled number input that prompts the user with
* a message and has an input for number entry.
*/
/datum/tgui_input_number
/// Boolean field describing if the tgui_input_number was closed by the user.
var/closed
/// The default (or current) value, shown as a default. Users can press reset with this.
var/default
/// The entry that the user has return_typed in.
var/entry
/// The maximum value that can be entered.
var/max_value
/// The prompt's body, if any, of the TGUI window.
var/message
/// The minimum value that can be entered.
var/min_value
/// Whether the submitted number is rounded down into an integer.
var/round_value
/// The time at which the number input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the number input, after which the window will close and delete itself.
var/timeout
/// The title of the TGUI window
var/title
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value, ui_state)
src.default = default
src.max_value = max_value
src.message = message
src.min_value = min_value
src.title = title
src.round_value = round_value
src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/// Checks for empty numbers - bank accounts, etc.
if(max_value == 0)
src.min_value = 0
if(default)
src.default = 0
/// Sanity check
if(default < min_value)
src.default = min_value
if(default > max_value)
CRASH("Default value is greater than max value.")
/datum/tgui_input_number/Destroy(force)
SStgui.close_uis(src)
state = null
return ..()
/**
* Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_input_number/proc/wait()
while (!entry && !closed && !QDELETED(src))
stoplag(1)
/datum/tgui_input_number/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "NumberInputModal")
ui.open()
/datum/tgui_input_number/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_input_number/ui_state(mob/user)
return state
/datum/tgui_input_number/ui_static_data(mob/user)
var/list/data = list()
data["init_value"] = default // Default is a reserved keyword
data["large_buttons"] = user.client.prefs.tgui_input_large
data["max_value"] = max_value
data["message"] = message
data["min_value"] = min_value
data["swapped_buttons"] = user.client.prefs.tgui_input_swapped
data["title"] = title
data["round_value"] = round_value
return data
/datum/tgui_input_number/ui_data(mob/user)
var/list/data = list()
if(timeout)
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_input_number/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("submit")
if(!isnum(params["entry"]))
CRASH("A non number was input into tgui input number by [usr]")
var/choice = round_value ? round(params["entry"]) : params["entry"]
if(choice > max_value)
CRASH("A number greater than the max value was input into tgui input number by [usr]")
if(choice < min_value)
CRASH("A number less than the min value was input into tgui input number by [usr]")
set_entry(choice)
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
/datum/tgui_input_number/proc/set_entry(entry)
src.entry = entry

View File

@@ -0,0 +1,166 @@
/**
* Creates a TGUI window with a text input. Returns the user's response.
*
* This proc should be used to create windows for text entry that the caller will wait for a response from.
* If tgui fancy chat is turned off: Will return a normal input. If max_length is specified, will return
* stripped_multiline_input.
*
* Arguments:
* * user - The user to show the text input to.
* * message - The content of the text input, shown in the body of the TGUI window.
* * title - The title of the text input modal, shown on the top of the TGUI window.
* * default - The default (or current) value, shown as a placeholder.
* * max_length - Specifies a max length for input. By default is infinity.
* * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc.
* * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input.
* * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout.
*/
/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length, multiline = FALSE, encode = TRUE, timeout = 0, ui_state = global.always_state)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return null
if(isnull(user.client))
return null
// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.tgui_input)
if(encode)
if(multiline)
return stripped_multiline_input(user, message, title, default, PREVENT_CHARACTER_TRIM_LOSS(max_length))
else
return stripped_input(user, message, title, default, PREVENT_CHARACTER_TRIM_LOSS(max_length))
else
if(multiline)
return input(user, message, title, default) as message|null
else
return input(user, message, title, default) as text|null
var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout, ui_state)
text_input.ui_interact(user)
text_input.wait()
if (text_input)
. = text_input.entry
qdel(text_input)
/**
* tgui_input_text
*
* Datum used for instantiating and using a TGUI-controlled text input that prompts the user with
* a message and has an input for text entry.
*/
/datum/tgui_input_text
/// Boolean field describing if the tgui_input_text was closed by the user.
var/closed
/// The default (or current) value, shown as a default.
var/default
/// Whether the input should be stripped using html_encode
var/encode
/// The entry that the user has return_typed in.
var/entry
/// The maximum length for text entry
var/max_length
/// The prompt's body, if any, of the TGUI window.
var/message
/// Multiline input for larger input boxes.
var/multiline
/// The time at which the text input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the text input, after which the window will close and delete itself.
var/timeout
/// The title of the TGUI window
var/title
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state
/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout, ui_state)
src.default = default
src.encode = encode
src.max_length = max_length
src.message = message
src.multiline = multiline
src.title = title
src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
spawn(timeout)
qdel(src)
/datum/tgui_input_text/Destroy(force)
SStgui.close_uis(src)
state = null
return ..()
/**
* Waits for a user's response to the tgui_input_text's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_input_text/proc/wait()
while (!entry && !closed && !QDELETED(src))
stoplag(1)
/datum/tgui_input_text/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "TextInputModal")
ui.open()
/datum/tgui_input_text/ui_close(mob/user)
. = ..()
closed = TRUE
/datum/tgui_input_text/ui_state(mob/user)
return state
/datum/tgui_input_text/ui_static_data(mob/user)
var/list/data = list()
data["large_buttons"] = user.client.prefs.tgui_input_large
data["max_length"] = max_length
data["message"] = message
data["multiline"] = multiline
data["placeholder"] = default // Default is a reserved keyword
data["swapped_buttons"] = user.client.prefs.tgui_input_swapped
data["title"] = title
return data
/datum/tgui_input_text/ui_data(mob/user)
var/list/data = list()
if(timeout)
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
return data
/datum/tgui_input_text/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if (.)
return
switch(action)
if("submit")
if(max_length)
if(length_char(params["entry"]) > max_length)
CRASH("[usr] typed a text string longer than the max length")
if(encode && (length_char(html_encode(params["entry"])) > max_length))
to_chat(usr, span_notice("Your message was clipped due to special character usage."))
set_entry(params["entry"])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE
/**
* Sets the return value for the tgui text proc.
* If html encoding is enabled, the text will be encoded.
* This can sometimes result in a string that is longer than the max length.
* If the string is longer than the max length, it will be clipped.
*/
/datum/tgui_input_text/proc/set_entry(entry)
if(!isnull(entry))
var/converted_entry = encode ? html_encode(entry) : entry
src.entry = max_length ? trim(converted_entry, PREVENT_CHARACTER_TRIM_LOSS(max_length)) : converted_entry

View File

@@ -3,27 +3,27 @@
task.file_path = "data/persistence/money_highscores_test.json"
task.clear_records()
var/datum/record/money/test_record = new("somebody", "Assistant", 40000)
var/datum/record/money/test_record2 = new("a condom", "Captain", 35000)
var/datum/record/money/test_record3 = new("this should not appear", "Nobody", 10000)
var/datum/record/money/test_record4 = new("cuban pete", "Miner", 50000)
var/datum/record/money/test_record5 = new("pedro cubano", "Miner", 30000)
var/datum/record/money/test_record6 = new("frank sinatra", "Miner", 9999999)
var/datum/data/record/money/test_record = new("somebody", "Assistant", 40000)
var/datum/data/record/money/test_record2 = new("a condom", "Captain", 35000)
var/datum/data/record/money/test_record3 = new("this should not appear", "Nobody", 10000)
var/datum/data/record/money/test_record4 = new("cuban pete", "Miner", 50000)
var/datum/data/record/money/test_record5 = new("pedro cubano", "Miner", 30000)
var/datum/data/record/money/test_record6 = new("frank sinatra", "Miner", 9999999)
var/list/records = list(test_record, test_record2, test_record3, test_record4, test_record5, test_record6)
task.insert_records(records)
assert_eq(task.data[1].cash, 9999999)
assert_eq(task.data[2].cash, 50000)
assert_eq(task.data[3].cash, 40000)
assert_eq(task.data[4].cash, 35000)
assert_eq(task.data[5].cash, 30000)
assert_eq(task.data[1].fields["cash"], 9999999)
assert_eq(task.data[2].fields["cash"], 50000)
assert_eq(task.data[3].fields["cash"], 40000)
assert_eq(task.data[4].fields["cash"], 35000)
assert_eq(task.data[5].fields["cash"], 30000)
assert_eq(task.data[1].role, "Miner")
assert_eq(task.data[2].role, "Miner")
assert_eq(task.data[3].role, "Assistant")
assert_eq(task.data[4].role, "Captain")
assert_eq(task.data[5].role, "Miner")
assert_eq(task.data[1].fields["role"], "Miner")
assert_eq(task.data[2].fields["role"], "Miner")
assert_eq(task.data[3].fields["role"], "Assistant")
assert_eq(task.data[4].fields["role"], "Captain")
assert_eq(task.data[5].fields["role"], "Miner")
if(task.data.len > 5)
fail("[task.name] is storing more than five entries. Len: [task.data.len]")
@@ -31,8 +31,8 @@
task.on_shutdown()
task.on_init()
assert_eq(task.data[1].cash, 9999999)
assert_eq(task.data[2].cash, 50000)
assert_eq(task.data[3].cash, 40000)
assert_eq(task.data[4].cash, 35000)
assert_eq(task.data[5].cash, 30000)
assert_eq(task.data[1].fields["cash"], 9999999)
assert_eq(task.data[2].fields["cash"], 50000)
assert_eq(task.data[3].fields["cash"], 40000)
assert_eq(task.data[4].fields["cash"], 35000)
assert_eq(task.data[5].fields["cash"], 30000)

View File

@@ -1,38 +1,38 @@
/datum/unit_test/ray_equals/start()
var/vector/origin = new /vector(0,3)
var/vector/direction = new /vector(4,7)
var/_vector/origin = new /_vector(0,3)
var/_vector/direction = new /_vector(4,7)
var/ray/R1 = new /ray(origin, direction)
var/ray/R2 = new /ray(origin, direction)
if(!R1.equals(R2))
fail("Rays not equal")
/datum/unit_test/ray_overlaps/start()
var/vector/origin = new /vector(0,3)
var/vector/direction = new /vector(4,7)
var/_vector/origin = new /_vector(0,3)
var/_vector/direction = new /_vector(4,7)
var/ray/R1 = new /ray(origin, direction)
var/ray/R2 = new /ray(origin, direction)
if(!R1.overlaps(R2))
fail("Rays #1 not overlapping")
direction = new /vector(-4,-7)
direction = new /_vector(-4,-7)
R1 = new /ray(origin, direction)
R2 = new /ray(origin, direction)
if(!R1.equals(R2))
fail("Rays #2 not overlapping")
/datum/unit_test/ray_hitsPoint/start()
var/vector/origin = new /vector(0,3)
var/vector/direction = new /vector(2,10)
var/vector/P = new /vector(1,8)
var/_vector/origin = new /_vector(0,3)
var/_vector/direction = new /_vector(2,10)
var/_vector/P = new /_vector(1,8)
var/ray/R = new /ray(origin, direction)
if(!R.hitsPoint(P))
fail("Ray not hitting point")
/datum/unit_test/ray_getPoint/start()
var/vector/origin = new /vector(0,3)
var/vector/direction = new /vector(4,10)
var/_vector/origin = new /_vector(0,3)
var/_vector/direction = new /_vector(4,10)
var/distance = 0.5
var/vector/P = new /vector(0.2,3.5)
var/_vector/P = new /_vector(0.2,3.5)
var/ray/R = new /ray(origin, direction)
if(!R.getPoint(distance).equals(P))
fail("Ray returned invalid point "+R.getPoint(distance).toString())

View File

@@ -1,8 +1,8 @@
/datum/unit_test/vector_duplicate/start()
var/vector/V
var/vector/D
var/_vector/V
var/_vector/D
for(var/i in range(100))
V = new /vector(i, i*2)
V = new /_vector(i, i*2)
D = V.duplicate()
if(D == V)
fail("Reference copied")
@@ -11,47 +11,47 @@
fail("Value mismatch")
/datum/unit_test/vector_isnull/start()
var/vector/V = new /vector(0,0.0)
var/_vector/V = new /_vector(0,0.0)
if(!V.is_null())
fail("Vector not null")
/datum/unit_test/vector_isint/start()
var/vector/V = new /vector(5416,115)
var/_vector/V = new /_vector(5416,115)
if(!V.is_integer())
fail("Vector not int, should be int")
V = new /vector(5416.044,115)
V = new /_vector(5416.044,115)
if(V.is_integer())
fail("Vector int, should not be int")
/datum/unit_test/vector_toangle/start()
var/vector/V = new /vector(1,1)
var/_vector/V = new /_vector(1,1)
var/angle = V.toAngle()
if(angle != 45)
fail("Angle #1 ("+num2text(angle)+") incorrect")
V = new /vector(-1,1)
V = new /_vector(-1,1)
angle = V.toAngle()
if(angle != 315)
fail("Angle #2 ("+num2text(angle)+") incorrect")
/datum/unit_test/vector_mirror/start()
var/vector/V = new /vector(1,-1)
var/vector/N = new /vector(0,1)
var/vector/M = new /vector(1,1)
var/vector/R = V.mirrorWithNormal(N)
var/_vector/V = new /_vector(1,-1)
var/_vector/N = new /_vector(0,1)
var/_vector/M = new /_vector(1,1)
var/_vector/R = V.mirrorWithNormal(N)
if(!R.equals(M))
fail("Mirror #2 incorrect "+R.toString())
/datum/unit_test/vector_dot/start()
var/vector/V1 = new /vector(4,-1)
var/vector/V2 = new /vector(1,1)
var/_vector/V1 = new /_vector(4,-1)
var/_vector/V2 = new /_vector(1,1)
var/d = V1.dot(V2)
if(d != 3)
fail("Dot product #1 ("+num2text(d)+") incorrect")
V1 = new /vector(0,2)
V2 = new /vector(1,1)
V1 = new /_vector(0,2)
V2 = new /_vector(1,1)
d = V1.dot(V2)
if(d != 2)
fail("Dot product #2 ("+num2text(d)+") incorrect")

41
code/tgs_event_handler.dm Normal file
View File

@@ -0,0 +1,41 @@
/datum/tgs_event_handler/impl
var/reattach_timer_id
/datum/tgs_event_handler/impl/HandleEvent(event_code, ...)
switch(event_code)
if(TGS_EVENT_REBOOT_MODE_CHANGE)
var/list/reboot_mode_lookup = list ("[TGS_REBOOT_MODE_NORMAL]" = "be normal", "[TGS_REBOOT_MODE_SHUTDOWN]" = "shutdown the server", "[TGS_REBOOT_MODE_RESTART]" = "hard restart the server")
var/old_reboot_mode = args[2]
var/new_reboot_mode = args[3]
message_admins("TGS: Reboot will no longer [reboot_mode_lookup["[old_reboot_mode]"]], it will instead [reboot_mode_lookup["[new_reboot_mode]"]]")
if(TGS_EVENT_PORT_SWAP)
message_admins("TGS: Changing port from [world.port] to [args[2]]")
if(TGS_EVENT_INSTANCE_RENAMED)
message_admins("TGS: Instance renamed to from [world.TgsInstanceName()] to [args[2]]")
if(TGS_EVENT_COMPILE_START)
message_admins("TGS: Deployment started, new game version incoming...")
if(TGS_EVENT_COMPILE_CANCELLED)
message_admins("TGS: Deployment cancelled!")
if(TGS_EVENT_COMPILE_FAILURE)
message_admins("TGS: Deployment failed!")
if(TGS_EVENT_DEPLOYMENT_COMPLETE)
message_admins("TGS: Deployment complete!")
to_chat(world, "Server updated, changes will be applied on the next round...")
if(TGS_EVENT_WATCHDOG_DETACH)
message_admins("TGS restarting...")
reattach_timer_id = add_timer(new /callback(src, nameof(src::LateOnReattach())), 600)
if(TGS_EVENT_WATCHDOG_REATTACH)
var/datum/tgs_version/old_version = world.TgsVersion()
var/datum/tgs_version/new_version = args[2]
if(!old_version.Equals(new_version))
to_chat(world, "TGS updated to v[new_version.deprefixed_parameter]")
else
message_admins("TGS: Back online")
if(reattach_timer_id)
del_timer(reattach_timer_id)
reattach_timer_id = null
if(TGS_EVENT_WATCHDOG_SHUTDOWN)
to_chat(world, "<span class='userdanger'>Server is shutting down!</span>")
/datum/tgs_event_handler/impl/proc/LateOnReattach()
message_admins("Warning: TGS hasn't notified us of it coming back for a full minute! Is there a problem?")

View File

@@ -5,8 +5,8 @@ var/world_startup_time
var/date_string
var/force_restart
#if DM_VERSION < 515
#error You need at least version 515 to compile
#if DM_VERSION < 516
#error You need at least version 516 to compile.
#endif
/world
mob = /mob/new_player
@@ -51,36 +51,15 @@ var/auxtools_path
/world/New()
world_startup_time = world.timeofday
TgsNew(null, TGS_SECURITY_TRUSTED)
src.InitTgs()
for(var/i=1, i<=map.zLevels.len, i++)
WORLD_X_OFFSET += rand(-50,50)
WORLD_Y_OFFSET += rand(-50,50)
// logs
date_string = time2text(world.realtime, "YYYY/MM-Month/DD-Day")
investigations[I_HREFS] = new /datum/log_controller(I_HREFS, filename="data/logs/[date_string] hrefs.htm", persist=TRUE)
investigations[I_ATMOS] = new /datum/log_controller(I_ATMOS, filename="data/logs/[date_string] atmos.htm", persist=TRUE)
investigations[I_CHEMS] = new /datum/log_controller(I_CHEMS, filename="data/logs/[date_string] chemistry.htm", persist=TRUE)
investigations[I_WIRES] = new /datum/log_controller(I_WIRES, filename="data/logs/[date_string] wires.htm", persist=TRUE)
investigations[I_GHOST] = new /datum/log_controller(I_GHOST, filename="data/logs/[date_string] poltergeist.htm", persist=TRUE)
investigations[I_ARTIFACT] = new /datum/log_controller(I_ARTIFACT, filename="data/logs/[date_string] artifact.htm", persist=TRUE)
investigations[I_RCD] = new /datum/log_controller(I_RCD, filename="data/logs/[date_string] rcd.htm", persist=TRUE)
diary = file("data/logs/[date_string].log")
panicfile = new/savefile("data/logs/profiling/proclogs/[date_string].sav")
diaryofmeanpeople = file("data/logs/[date_string] Attack.log")
admin_diary = file("data/logs/[date_string] admin only.log")
var/now = time_stamp()
var/log_start = "---------------------\n\[[now]\]WORLD: starting up..."
diary << log_start
diaryofmeanpeople << log_start
admin_diary << log_start
panicfile.cd = now
InitializeLogs()
changelog_hash = md5('html/changelog.html') //used for telling if the changelog has changed recently
load_configuration()
@@ -114,6 +93,33 @@ var/auxtools_path
return ..()
/world/proc/InitTgs()
TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_TRUSTED)
/world/proc/InitializeLogs()
investigations[I_HREFS] = new /datum/log_controller(I_HREFS, filename="data/logs/[date_string] hrefs.htm", persist=TRUE)
investigations[I_ATMOS] = new /datum/log_controller(I_ATMOS, filename="data/logs/[date_string] atmos.htm", persist=TRUE)
investigations[I_CHEMS] = new /datum/log_controller(I_CHEMS, filename="data/logs/[date_string] chemistry.htm", persist=TRUE)
investigations[I_WIRES] = new /datum/log_controller(I_WIRES, filename="data/logs/[date_string] wires.htm", persist=TRUE)
investigations[I_GHOST] = new /datum/log_controller(I_GHOST, filename="data/logs/[date_string] poltergeist.htm", persist=TRUE)
investigations[I_ARTIFACT] = new /datum/log_controller(I_ARTIFACT, filename="data/logs/[date_string] artifact.htm", persist=TRUE)
investigations[I_RCD] = new /datum/log_controller(I_RCD, filename="data/logs/[date_string] rcd.htm", persist=TRUE)
date_string = time2text(world.realtime, "YYYY/MM-Month/DD-Day")
diary = file("data/logs/[date_string].log")
panicfile = new/savefile("data/logs/profiling/proclogs/[date_string].sav")
diaryofmeanpeople = file("data/logs/[date_string] Attack.log")
admin_diary = file("data/logs/[date_string] admin only.log")
var/now = time_stamp()
var/log_start = "---------------------\n\[[now]\]WORLD: starting up..."
diary << log_start
diaryofmeanpeople << log_start
admin_diary << log_start
panicfile.cd = now
/world/Topic(T, addr, master, key)
TGS_TOPIC

View File

@@ -6,12 +6,12 @@
#Currently, this is only used by build.cmd and TGS quick setup
# byond version
#export BYOND_MAJOR=514
#export BYOND_MINOR=1566
#export BYOND_MAJOR=516
#export BYOND_MINOR=1659
#node version
export NODE_VERSION=16
export NODE_VERSION_PRECISE=16.13.1
export NODE_VERSION_PRECISE=22.10
# SpacemanDMM git tag
#export SPACEMAN_DMM_VERSION=suite-1.7.1

View File

@@ -239,7 +239,8 @@ For the main html chat area
iconCache[iconKey] << icon(icon, dir = SOUTH, frame = 1)
var/iconData = iconCache.ExportText(iconKey)
var/list/partial = splittext(iconData, "{")
return replacetext(copytext(partial[2], 3, -5), "\n", "")
var/list/partial2 = splittext(partial[2], "}")
return replacetext(copytext(partial2[1], 2, -1), "\n", "")
/proc/bicon(var/obj)
if (!obj)

View File

@@ -39,5 +39,5 @@ table{
/* "Center" it, since we can only crop out the top-left */
margin: -16px -16px -16px -16px;
/* Remove antialias to prevent cataracts */
-ms-interpolation-mode: nearest-neighbor;
image-rendering: pixelated;
}

View File

@@ -352,7 +352,7 @@ div.notice
}
.charPreview{
-ms-interpolation-mode: nearest-neighbor;
image-rendering: pixelated;
width: 64px;
height:64px;
}

View File

@@ -2,7 +2,7 @@
/* 64x64 futureproofing! */
width: 32px;
height: 32px;
-ms-interpolation-mode: nearest-neighbor;
image-rendering: pixelated;
}
.cropped img{

Some files were not shown because too many files have changed in this diff Show More