diff --git a/code/__DEFINES/html_assistant.dm b/code/__DEFINES/html_assistant.dm new file mode 100644 index 000000000000..91af96a95c7a --- /dev/null +++ b/code/__DEFINES/html_assistant.dm @@ -0,0 +1,5 @@ +#define HTML_SKELETON_INTERNAL(head, body) \ +"[head][body]" + +#define HTML_SKELETON_TITLE(title, body) HTML_SKELETON_INTERNAL("[title]", body) +#define HTML_SKELETON(body) HTML_SKELETON_INTERNAL("", body) diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index 9c4dae49e0e8..a53e9af52995 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -270,7 +270,6 @@ block( \ if(I.imp_in) . |= I.imp_in -#define SIGNV(X) ((X<0)?-1:1) /proc/inLineOfSight(X1,Y1,X2,Y2,Z=1,PX1=16.5,PY1=16.5,PX2=16.5,PY2=16.5) var/turf/T @@ -301,7 +300,6 @@ block( \ if(IS_OPAQUE_TURF(T)) return 0 return 1 -#undef SIGNV /proc/isInSight(atom/A, atom/B) diff --git a/code/__HELPERS/stat_tracking.dm b/code/__HELPERS/stat_tracking.dm index 007cd2695d0a..2f55ea0455df 100644 --- a/code/__HELPERS/stat_tracking.dm +++ b/code/__HELPERS/stat_tracking.dm @@ -8,6 +8,6 @@ lines += "[entry] => [num2text(data[STAT_ENTRY_TIME], 10)]ms ([data[STAT_ENTRY_COUNT]]) (avg:[num2text(data[STAT_ENTRY_TIME]/(data[STAT_ENTRY_COUNT] || 1), 99)])" if (user) - user << browse("
  1. [lines.Join("
  2. ")]
", "window=[url_encode("stats:[REF(stats)]")]") + user << browse(HTML_SKELETON("
  1. [lines.Join("
  2. ")]
"), "window=[url_encode("stats:[REF(stats)]")]") . = lines.Join("\n") diff --git a/code/_onclick/hud/map_popups.dm b/code/_onclick/hud/map_popups.dm index 539d5f551055..e93200a52ad1 100644 --- a/code/_onclick/hud/map_popups.dm +++ b/code/_onclick/hud/map_popups.dm @@ -52,6 +52,17 @@ /atom/movable/screen/proc/set_position(x, y, px = 0, py = 0) if(assigned_map) screen_loc = "[assigned_map]:[x]:[px],[y]:[py]" + ASYNC + // HACK: This fixes the character creator in 516 being small and relying on other byondui things (like cameras) to open in order to update and refresh. + // This also will fix the camera console screen being offset, Gateway, and admin pod panel. + // Adding 100 then setting it back seemed to do the trick! + // Why the fuck does this work? This is some byond bug and I honestly have no fucking clue why this works. + // I don't think plane master will be affected, I hope. + // We're stuck in the belly of this awful machine. + sleep(0.2 SECONDS) // If it's too fast, it has a chance to fail? Idk. This seems like a good number. + screen_loc = "[assigned_map]:[x+100]:[px],[y+100]:[py]" + sleep(0.2 SECONDS) + screen_loc = "[assigned_map]:[x]:[px],[y]:[py]" else screen_loc = "[x]:[px],[y]:[py]" diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 408c506ef3dc..6ff69d0ed9ac 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -526,3 +526,10 @@ /datum/config_entry/string/elasticsearch_metrics_endpoint /datum/config_entry/string/elasticsearch_metrics_apikey + +/** + * Tgui ui_act payloads larger than 2kb are split into chunks a maximum of 1kb in size. + * This flag represents the maximum chunk count the server is willing to receive. + */ +/datum/config_entry/number/tgui_max_chunk_count + default = 32 diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm index 078eb605fcb3..8536ebfd1c27 100644 --- a/code/datums/looping_sounds/machinery_sounds.dm +++ b/code/datums/looping_sounds/machinery_sounds.dm @@ -30,8 +30,6 @@ end_sound = 'sound/machines/generator/generator_end.ogg' volume = 40 -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // [CELADON-ADD] - CELADON_GRAVGEN /datum/looping_sound/gravity_generator mid_sounds = list('mod_celadon/_storage_sounds/sound/effects/beam.ogg') @@ -40,7 +38,6 @@ falloff_distance = 3 // [/CELADON-ADD] -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /datum/looping_sound/oven start_sound = 'sound/machines/oven/oven_loop_start.ogg' start_length = 12 diff --git a/code/datums/profiling.dm b/code/datums/profiling.dm index 49a80d0eded6..3e8505b8db47 100644 --- a/code/datums/profiling.dm +++ b/code/datums/profiling.dm @@ -15,4 +15,6 @@ GLOBAL_REAL_VAR(PROFILE_TIME) var/list/data = PROFILE_STORE[entry] lines += "[entry] => [num2text(data[PROFILE_ITEM_TIME], 10)]ms ([data[PROFILE_ITEM_COUNT]]) (avg:[num2text(data[PROFILE_ITEM_TIME]/(data[PROFILE_ITEM_COUNT] || 1), 99)])" - user << browse("
  1. [lines.Join("
  2. ")]
", "window=[url_encode(GUID())]") + var/datum/browser/browser = new(user, "[url_encode(GUID())]", null, 500, 500) + browser.set_content("
  1. [lines.Join("
  2. ")]
") + browser.open() diff --git a/code/datums/wounds/_wounds.dm b/code/datums/wounds/_wounds.dm index 88480101f5c0..b57b1a518c0f 100644 --- a/code/datums/wounds/_wounds.dm +++ b/code/datums/wounds/_wounds.dm @@ -400,4 +400,4 @@ /// Gets the name of the wound with any interactable topic if possible /datum/wound/proc/get_topic_name(mob/user) - return show_wound_topic(user) ? "[lowertext(name)]" : lowertext(name) + return show_wound_topic(user) ? "[lowertext(name)]" : lowertext(name) diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm index d5da5fa63f9d..3d04fbd5e576 100644 --- a/code/game/machinery/cryopod.dm +++ b/code/game/machinery/cryopod.dm @@ -459,7 +459,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/cryopod/retro, 17) to_chat(target, span_boldnotice("If you ghost, log out or close your client now, your character will shortly be permanently removed from the round.")) name = "[name] ([occupant.name])" log_admin(span_notice("[key_name(target)] entered a stasis pod.")) - message_admins("[key_name_admin(target)] entered a stasis pod. (JMP)") + message_admins("[key_name_admin(target)] entered a stasis pod. (JMP)") add_fingerprint(target) /obj/machinery/cryopod/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override) diff --git a/code/game/machinery/telecomms/machines/message_server.dm b/code/game/machinery/telecomms/machines/message_server.dm index 0a09a5aa98d5..8d2f0c3d717e 100644 --- a/code/game/machinery/telecomms/machines/message_server.dm +++ b/code/game/machinery/telecomms/machines/message_server.dm @@ -210,7 +210,7 @@ M << browse_rsc(picture.picture_image, "pda_photo.png") M << browse("PDA Photo" \ + "" \ - + "" \ + + "" \ + "", "window=pdaphoto;size=[picture.psize_x]x[picture.psize_y];can-close=true") onclose(M, "pdaphoto") diff --git a/code/game/objects/items/charter.dm b/code/game/objects/items/charter.dm index 6379c3ac7197..67de48ac8912 100644 --- a/code/game/objects/items/charter.dm +++ b/code/game/objects/items/charter.dm @@ -58,7 +58,7 @@ to_chat(user, span_notice("Your name has been sent to your employers for approval.")) // Autoapproves after a certain time response_timer_id = addtimer(CALLBACK(src, PROC_REF(rename_station), new_name, user.name, user.real_name, key_name(user)), approval_time, TIMER_STOPPABLE) - to_chat(GLOB.admins, span_adminnotice("CUSTOM STATION RENAME:[ADMIN_LOOKUPFLW(user)] proposes to rename the [name_type] to [new_name] (will autoapprove in [DisplayTimeText(approval_time)]). [ADMIN_SMITE(user)] (REJECT) [ADMIN_CENTCOM_REPLY(user)]")) + to_chat(GLOB.admins, span_adminnotice("CUSTOM STATION RENAME:[ADMIN_LOOKUPFLW(user)] proposes to rename the [name_type] to [new_name] (will autoapprove in [DisplayTimeText(approval_time)]). [ADMIN_SMITE(user)] (REJECT) [ADMIN_CENTCOM_REPLY(user)]")) /obj/item/sector_charter/proc/reject_proposed(user) if(!user) diff --git a/code/game/objects/items/devices/paicard.dm b/code/game/objects/items/devices/paicard.dm index 8f24d81754df..f5c0b8921a55 100644 --- a/code/game/objects/items/devices/paicard.dm +++ b/code/game/objects/items/devices/paicard.dm @@ -57,10 +57,12 @@ dat += "\[Wipe current pAI personality\]
" else dat += "No personality installed.
" - dat += "Searching for a personality... Press view available personalities to notify potential candidates." + dat += "Searching for a personality... Press view available personalities to notify potential candidates.
" dat += "\[View available personalities\]
" - user << browse(dat, "window=paicard") - onclose(user, "paicard") + + var/datum/browser/popup = new(user, "Personal AI Device", name, 500, 430) + popup.set_content(dat) + popup.open() return /obj/item/paicard/Topic(href, href_list) diff --git a/code/game/objects/items/storage/secure.dm b/code/game/objects/items/storage/secure.dm index 3d6c0895247d..5a10d787bb93 100644 --- a/code/game/objects/items/storage/secure.dm +++ b/code/game/objects/items/storage/secure.dm @@ -76,7 +76,9 @@ if (!locked) message = "*****" dat += text("
\n>[]
\n1-2-3
\n4-5-6
\n7-8-9
\nR-0-E
\n", message) - user << browse(dat, "window=caselock;size=300x280") + var/datum/browser/popup = new(user, "Passcode Interface", name, 300, 280) + popup.set_content(dat) + popup.open() /obj/item/storage/secure/Topic(href, href_list) ..() diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 39d053f6d4dc..5511779be387 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -444,8 +444,8 @@ dat += "
" if(SSticker.IsRoundInProgress()) dat += "(Game Mode Panel)
" + dat += "
" dat += {" -
Create Object
Quick Create Object
Create Turf
@@ -460,7 +460,6 @@ var/datum/browser/popup = new(usr, "admin2", null, 240, 280) popup.set_content(dat) popup.open() -// usr << browse(dat, "window=admin2;size=240x280") return /////////////////////////////////////////////////////////////////////////////////////////////////admins2.dm merge @@ -868,10 +867,7 @@ to_chat(usr, "No AIs located" , confidential = TRUE) /datum/admins/proc/dynamic_mode_options(mob/user) - var/dat = {" -

Dynamic Mode Options


-
-

Common options

+ var/dat = {"

Common options

All these options can be changed midround.

Force extended: - Option is [GLOB.dynamic_forced_extended ? "ON" : "OFF"]. @@ -901,9 +897,11 @@ Midround injection delay:
Minimum: -> [GLOB.dynamic_midround_delay_min / 60 / 10] <- Minutes
Maximum: -> [GLOB.dynamic_midround_delay_max / 60 / 10] <- Minutes
- "} + "} - user << browse(dat, "window=dyn_mode_options;size=900x650") + var/datum/browser/browser = new(user, "dyn_mode_options", "Dynamic Mode Options", 900, 650) + browser.set_content(dat) + browser.open() /datum/admins/proc/create_or_modify_area() set category = "Debug" diff --git a/code/modules/admin/permissionedit.dm b/code/modules/admin/permissionedit.dm index 8ecfad437a1c..3bf786adb43d 100644 --- a/code/modules/admin/permissionedit.dm +++ b/code/modules/admin/permissionedit.dm @@ -121,9 +121,9 @@ deadminlink = " \[DA\]" var/verify_link = "" if (D.blocked_by_2fa) - verify_link += " | \[2FA VERIFY\]" + verify_link += " | \[2FA VERIFY\]" output += "" - output += "[adm_ckey]
[deadminlink]\[-\]\[SYNC TGDB\][verify_link]" + output += "[adm_ckey]
[deadminlink]\[-\]\[SYNC TGDB\][verify_link]" output += "[D.rank.name]" output += "[rights2text(D.rank.include_rights," ")]" output += "[rights2text(D.rank.exclude_rights," ", "-")]" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 367e4f57c8b8..9231e8b5059c 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1879,8 +1879,6 @@ var/list/dat = list() dat += thing_to_check -// usr << browse(dat.Join("
"), "window=related_[C];size=420x300") - var/datum/browser/popup = new(usr, "related_[C]", "Related accounts by [uppertext(href_list["showrelatedacc"])]:", 425, 300) popup.set_content(dat.Join("
")) popup.open() diff --git a/code/modules/admin/verbs/SDQL2/SDQL_2.dm b/code/modules/admin/verbs/SDQL2/SDQL_2.dm index bd221a45f18e..6c0549fa122f 100644 --- a/code/modules/admin/verbs/SDQL2/SDQL_2.dm +++ b/code/modules/admin/verbs/SDQL2/SDQL_2.dm @@ -541,7 +541,7 @@ GLOBAL_DATUM_INIT(sdql2_vv_statobj, /obj/effect/statclick/SDQL2_VV_all, new(null if(length(select_text)) var/text = islist(select_text)? select_text.Join() : select_text var/static/result_offset = 0 - showmob << browse(text, "window=SDQL-result-[result_offset++]") + showmob << browse(HTML_SKELETON(text), "window=SDQL-result-[result_offset++]") show_next_to_key = null if(qdel_on_finish) qdel(src) diff --git a/code/modules/admin/verbs/getlogs.dm b/code/modules/admin/verbs/getlogs.dm index 1f706091c6b4..69841500facc 100644 --- a/code/modules/admin/verbs/getlogs.dm +++ b/code/modules/admin/verbs/getlogs.dm @@ -24,7 +24,7 @@ message_admins("[key_name_admin(src)] accessed file: [path]") switch(alert("View (in game), Open (in your system's text editor), or Download?", path, "View", "Open", "Download")) if ("View") - src << browse("
[html_encode(file2text(file(path)))]
", list2params(list("window" = "viewfile.[path]"))) + src << browse(HTML_SKELETON("
[html_encode(file2text(file(path)))]
"), list2params(list("window" = "viewfile.[path]"))) if ("Open") src << run(file(path)) if ("Download") diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm index 0b45809d9013..863db4a3939f 100644 --- a/code/modules/admin/verbs/mapping.dm +++ b/code/modules/admin/verbs/mapping.dm @@ -90,7 +90,7 @@ GLOBAL_PROTECT(admin_verbs_debug_extra) for(var/turf/T in seen) T.maptext = "[seen[T]]" BLACKBOX_LOG_ADMIN_VERB("Show Camera Range") - BLACKBOX_LOG_ADMIN_VERB("Show Camera Range") + //BLACKBOX_LOG_ADMIN_VERB("Show Camera Range") // what the fuck? Why? #ifdef TESTING GLOBAL_LIST_EMPTY(dirty_vars) @@ -146,7 +146,7 @@ GLOBAL_LIST_EMPTY(dirty_vars) output += "
  • Camera not connected to wall at [ADMIN_VERBOSEJMP(C1)] Network: [json_encode(C1.network)]
  • " output += "" - usr << browse(output,"window=airreport;size=1000x500") + usr << browse(HTML_SKELETON(output),"window=airreport;size=1000x500") BLACKBOX_LOG_ADMIN_VERB("Show Camera Report") /client/proc/intercom_view() @@ -180,7 +180,7 @@ GLOBAL_LIST_EMPTY(dirty_vars) dat += "[ADMIN_VERBOSEJMP(T)]\n" dat += "
    " - usr << browse(dat, "window=at_list") + usr << browse(HTML_SKELETON(dat), "window=at_list") BLACKBOX_LOG_ADMIN_VERB("Show Roundstart Active Turfs") diff --git a/code/modules/admin/verbs/secrets.dm b/code/modules/admin/verbs/secrets.dm index c147e3a092fe..337b9794b115 100644 --- a/code/modules/admin/verbs/secrets.dm +++ b/code/modules/admin/verbs/secrets.dm @@ -55,15 +55,14 @@ switch(action) //Generic Buttons anyone can use. if("admin_log") - var/dat = "Admin Log
    " + var/dat for(var/l in GLOB.admin_log) dat += "
  • [l]
  • " if(!GLOB.admin_log.len) dat += "No-one has done anything this round!" - //holder << browse(dat, "window=admin_log") WS edit - var/datum/browser/popup = new(holder, "admin_log", null, 300, 430) - popup.set_content(dat) - popup.open() + var/datum/browser/browser = new(holder, "admin_log", "Admin Logs", 600, 500) + browser.set_content(dat) + browser.open() //WS Begin - Mentors if("mentor_log") @@ -78,15 +77,15 @@ //WS end if("show_admins") - var/dat = "Current admins:
    " + var/dat if(GLOB.admin_datums) for(var/ckey in GLOB.admin_datums) var/datum/admins/D = GLOB.admin_datums[ckey] dat += "[ckey] - [D.rank.name]
    " //holder << browse(dat, "window=showadmins;size=600x500") WS edit - var/datum/browser/popup = new(holder, "showadmins", null, 600, 500) - popup.set_content(dat) - popup.open() + var/datum/browser/browser = new(holder, "showadmins", "Current admins", 600, 500) + browser.set_content(dat) + browser.open() //Buttons for debug. if("maint_access_engiebrig") diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 800d7aecb06b..e4df6687148d 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -386,7 +386,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( msg += "Your version: [byond_version]
    " msg += "Required version to remove this message: [cwv] or later
    " msg += "Visit BYOND's website to get the latest version of BYOND.
    " - src << browse(msg, "window=warning_popup") + src << browse(HTML_SKELETON(msg), "window=warning_popup") else to_chat(src, span_danger("Your version of byond may be getting out of date:")) to_chat(src, CONFIG_GET(string/client_warn_message)) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 6ef2b74a5641..3b4aa1cf5fb5 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -487,7 +487,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Height

    " - dat += "[height_filter]
    " + dat += "[height_filter]
    " // Everyone gets mutant colors now. dat += "

    Mutant Colors

    " @@ -713,7 +713,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "[features["ipc_hair"]]
    " - dat += "    Change
    " + dat += "    Change
    " mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -941,7 +941,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Ears markings

    " dat += "[features["tajara_ears_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -954,7 +954,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Head markings

    " dat += "[features["tajara_head_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -967,7 +967,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Nose markings

    " dat += "[features["tajara_nose_markings"]]
    " - dat += "    Change" + dat += "    Change" //dat += "

    Skin Tone nose

    " //dat += "[skin_tone_nose]" @@ -984,7 +984,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Chest markings

    " dat += "[features["tajara_chest_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -997,7 +997,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Body markings

    " dat += "[features["tajara_body_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -1035,7 +1035,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Ears markings

    " dat += "[features["riol_ears_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -1048,7 +1048,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Head markings

    " dat += "[features["riol_head_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -1061,7 +1061,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Nose markings

    " dat += "[features["riol_nose_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -1074,7 +1074,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Chest markings

    " dat += "[features["riol_chest_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -1087,7 +1087,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Body markings

    " dat += "[features["riol_body_markings"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -1100,7 +1100,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) // dat += "

    Tail markings

    " // dat += "[features["riol_tail_markings"]]
    " - // dat += "    Change" + // dat += "    Change" // mutant_category++ // if(mutant_category >= MAX_MUTANT_ROWS) @@ -1125,7 +1125,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

    Tail

    " dat += "[features["riol_tail"]]
    " - dat += "    Change" + dat += "    Change" mutant_category++ if(mutant_category >= MAX_MUTANT_ROWS) @@ -1206,7 +1206,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) //for(var/index in prosthetic_limbs) //var/bodypart_name = parse_zone(index) //dat += "[bodypart_name]:" - //dat += "[prosthetic_limbs[index]]" + //dat += "[prosthetic_limbs[index]]" //dat += "
    " [/CELADON-EDIT] - ORIGINAL END if(!istype(pref_species, /datum/species/lanius)) dat += "

    Prosthetic Limbs

    " @@ -1218,7 +1218,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) for(var/index in prosthetic_limbs) var/bodypart_name = parse_zone(index) dat += "[bodypart_name]:" - dat += "[prosthetic_limbs[index]]" + dat += "[prosthetic_limbs[index]]" dat += "
    " //[/CELADON - EDIT] if(2) //Loadout diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index 703a2b4d63ee..82e67a8445e5 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -392,7 +392,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") //Collect keywords var/list/keywords = mob.get_policy_keywords() var/header = get_policy(POLICY_VERB_HEADER) - var/list/policytext = list(header,"
    ") + var/list/policytext = list(header) var/anything = FALSE for(var/keyword in keywords) var/p = get_policy(keyword) @@ -403,7 +403,9 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") if(!anything) policytext += "No related rules found." - usr << browse(policytext.Join(""),"window=policy") + var/datum/browser/browser = new(usr, "policy", "Server Policy", 600, 500) + browser.set_content(policytext.Join("")) + browser.open() /client/verb/fix_stat_panel() set name = "Fix Stat Panel" diff --git a/code/modules/mapping/verify.dm b/code/modules/mapping/verify.dm index 1f071aaec720..fe68afefcc5d 100644 --- a/code/modules/mapping/verify.dm +++ b/code/modules/mapping/verify.dm @@ -15,7 +15,6 @@ /// Show a rendered version of this report to a client. /datum/map_report/proc/show_to(client/C) var/list/html = list() - html += "

    Report for map file [original_path]

    " if(crashed) html += "

    Validation crashed: check the runtime logs.

    " if(!loadable) @@ -39,7 +38,9 @@ html += "" html += "" html += "

    " - C << browse(html.Join(), "window=[tag];size=600x400") + var/datum/browser/browser = new(C.mob, "[tag]", "Report for map file [original_path]", 600, 400) + browser.set_content(html.Join()) + browser.open() /datum/map_report/Topic(href, href_list) . = ..() diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 8b11b6e5645c..2d5987542263 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -224,12 +224,11 @@ var/less_input_message if(SSlag_switch.measures[DISABLE_DEAD_KEYLOOP]) less_input_message = " - Notice: Observer freelook is currently disabled." - var/this_is_like_playing_right = tgui_alert(src, "Are you sure you wish to observe? You will [CONFIG_GET(flag/norespawn) ? "not " : "" ]be able to respawn later.[less_input_message]", "Player Setup", list("Yes","No")) + // Don't convert this to tgui please, it's way too important + var/this_is_like_playing_right = alert(usr, "Are you sure you wish to observe? You will not be able to play this round![less_input_message]", "Observe", "Yes", "No") if(QDELETED(src) || !src.client || this_is_like_playing_right != "Yes") ready = PLAYER_NOT_READY - src << browse(null, "window=playersetup") //closes the player setup window - new_player_panel() return FALSE var/mob/dead/observer/observer = new() diff --git a/code/modules/mob/dead/new_player/poll.dm b/code/modules/mob/dead/new_player/poll.dm index efff1afef14d..b90c7a30c539 100644 --- a/code/modules/mob/dead/new_player/poll.dm +++ b/code/modules/mob/dead/new_player/poll.dm @@ -11,7 +11,7 @@ continue output += "[poll.question]" output += "" - src << browse(jointext(output, ""),"window=playerpolllist;size=500x300") + src << browse(HTML_SKELETON(jointext(output, "")),"window=playerpolllist;size=500x300") /** * Redirects a player to the correct poll window based on poll type. @@ -77,7 +77,7 @@ if(!voted_option_id || poll.allow_revoting) output += "

    " output += "" - src << browse(jointext(output, ""),"window=playerpoll;size=500x250") + src << browse(HTML_SKELETON(jointext(output, "")),"window=playerpoll;size=500x250") /** * Shows voting window for a text response type poll, listing its relevant details. @@ -113,8 +113,8 @@ "} else output += "[reply_text]" - output += "" - src << browse(jointext(output, ""),"window=playerpoll;size=500x500") + output += "" + src << browse(HTML_SKELETON(jointext(output, "")),"window=playerpoll;size=500x500") /** * Shows voting window for a rating type poll, listing its options and relevant details. @@ -169,7 +169,7 @@ if(!length(voted_ratings) || poll.allow_revoting) output += "

    " output += "" - src << browse(jointext(output, ""),"window=playerpoll;size=500x500") + src << browse(HTML_SKELETON(jointext(output, "")),"window=playerpoll;size=500x500") /** * Shows voting window for a multiple choice type poll, listing its options and relevant details. @@ -213,7 +213,7 @@ if(!length(voted_for) || poll.allow_revoting) output += "

    " output += "" - src << browse(jointext(output, ""),"window=playerpoll;size=500x300") + src << browse(HTML_SKELETON(jointext(output, "")),"window=playerpoll;size=500x300") /** * Shows voting window for an IRV type poll, listing its options and relevant details. @@ -300,8 +300,8 @@ output += "

    Least Preferred

    " if(!length(voted_for) || poll.allow_revoting) output += "

    " - output += "" - src << browse(jointext(output, ""),"window=playerpoll;size=500x500") + output += "" + src << browse(HTML_SKELETON(jointext(output, "")),"window=playerpoll;size=500x500") /** * Runs some poll validation before a vote is processed. diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index b4bc85375763..9723b799ff19 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -205,10 +205,10 @@ msg += "\t[t_His] [limb.name] is suffering [W.a_or_from] [W.get_topic_name(user)]!!" if(limb.current_gauze) var/datum/bodypart_aid/current_gauze = limb.current_gauze - msg += "\t[t_His] [limb.name] is [current_gauze.desc_prefix] with [current_gauze.get_description()]." + msg += "\t[t_His] [limb.name] is [current_gauze.desc_prefix] with [current_gauze.get_description()]." if(limb.current_splint) var/datum/bodypart_aid/current_splint = limb.current_splint - msg += "\t[t_His] [limb.name] is [current_splint.desc_prefix] with [current_splint.get_description()]." + msg += "\t[t_His] [limb.name] is [current_splint.desc_prefix] with [current_splint.get_description()]." if(!any_bodypart_damage) msg += "\t[t_He] [t_Has] no significantly damaged bodyparts." diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index c1df99934cae..528778dccc22 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -846,10 +846,10 @@ if(body_part.current_gauze) var/datum/bodypart_aid/current_gauze = body_part.current_gauze - combined_msg += "\t Your [body_part.name] is [current_gauze.desc_prefix] with [current_gauze.get_description()]." + combined_msg += "\t Your [body_part.name] is [current_gauze.desc_prefix] with [current_gauze.get_description()]." if(body_part.current_splint) var/datum/bodypart_aid/current_splint = body_part.current_splint - combined_msg += "\t Your [body_part.name] is [current_splint.desc_prefix] with [current_splint.get_description()]." + combined_msg += "\t Your [body_part.name] is [current_splint.desc_prefix] with [current_splint.get_description()]." for(var/obj/item/I in body_part.embedded_objects) if(I.isEmbedHarmless()) diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 49a550d45622..33d6aa382c60 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -276,11 +276,11 @@ if (C && istype(C, /list)) var/dat2 = "" for (var/obj/machinery/camera/I in C) - dat2 += text("[][]", (dat2=="") ? "" : " | ", I.c_tag) + dat2 += text("[][])", A.name, Ctmp.c_tag) else dat += text("-- [] (No Camera)", A.name) if (sources.len > 1) @@ -494,12 +494,12 @@ if (cameras) if (cam?.can_use()) - queueAlarm("--- [class] alarm detected in [home.name]! ([cam.c_tag])", class) + queueAlarm("--- [class] alarm detected in [home.name]! ([name]" \ + "" \ - + "" \ + + "" \ + "[scribble ? "
    Written on the back:
    [scribble]" : ""]"\ + "", "window=photo_showing;size=480x608") onclose(user, "[name]") diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index 844ba6239a0f..ecbb5ab6ee36 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -26,6 +26,8 @@ var/initial_inline_css var/mouse_event_macro_set = FALSE + var/list/oversized_payloads = list() + /** * public * @@ -361,6 +363,17 @@ // Resend the assets for(var/asset in sent_assets) send_asset(asset) + if("oversizedPayloadRequest") + var/payload_id = payload["id"] + var/chunk_count = payload["chunkCount"] + var/permit_payload = chunk_count <= CONFIG_GET(number/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/proc/set_mouse_macro() if(mouse_event_macro_set) @@ -398,3 +411,36 @@ for(var/mouseMacro in byondToTguiEventMap) winset(client, null, "[mouseMacro]Window[id]Macro.parent=null") mouse_event_macro_set = FALSE + +/datum/tgui_window/vv_edit_var(var_name, var_value) + return var_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" = addtimer(CALLBACK(src, PROC_REF(remove_oversized_payload), payload_id), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE) + ) + +/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"]) + deltimer(payload["timeout"]) + var/message_type = payload["type"] + var/final_payload = chunks.Join() + remove_oversized_payload(payload_id) + if(final_payload != "") + on_message(message_type, json_decode(final_payload), list("type" = message_type, "payload" = final_payload, "tgui" = TRUE, "window_id" = id)) + else + payload["timeout"] = addtimer(CALLBACK(src, PROC_REF(remove_oversized_payload), payload_id), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE) + +/datum/tgui_window/proc/remove_oversized_payload(payload_id) + oversized_payloads -= payload_id diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm index c37f289585a9..9faf1ed80de5 100644 --- a/code/modules/tgui_panel/external.dm +++ b/code/modules/tgui_panel/external.dm @@ -31,8 +31,7 @@ tgui_panel = new(src) tgui_panel.initialize(force = TRUE) // Force show the panel to see if there are any errors - winset(src, "output", "is-disabled=1&is-visible=0") - winset(src, "browseroutput", "is-disabled=0;is-visible=1") + winset(src, "legacy_output_selector", "left=output_browser") action = alert(src, "Method: Reinitializing the panel.\nWait a bit and tell me if it's fixed", "", "Fixed", "Nope") if(action == "Fixed") log_tgui(src, "Fixed by calling 'initialize'", @@ -41,7 +40,6 @@ // Failed to fix action = alert(src, "Welp, I'm all out of ideas. Try closing BYOND and reconnecting.\nWe could also disable tgui_panel and re-enable the old UI", "", "Thanks anyways", "Switch to old UI") if (action == "Switch to old UI") - winset(src, "output", "on-show=&is-disabled=0&is-visible=1") - winset(src, "browseroutput", "is-disabled=1;is-visible=0") + winset(src, "legacy_output_selector", "left=output_legacy") log_tgui(src, "Failed to fix.", context = "verb/fix_tgui_panel") diff --git a/config/config.txt b/config/config.txt index 08058efb44c7..ad78a9e9240d 100644 --- a/config/config.txt +++ b/config/config.txt @@ -577,3 +577,7 @@ ELASTICSEARCH_METRICS_ENDPOINT http://10.0.0.40:9201/ss13-metrics-stream/_doc ## ElasticSearch API key. This is formatted into the headers. Look at the ElasticSearch doc for how to make this ELASTICSEARCH_METRICS_APIKEY thisIsSomethingThatsBased64Encoded== + +## Tgui payloads larger than the 2kb limit for BYOND topic requests are split into roughly 1kb chunks and sent in sequence. +## This config option limits the maximum chunk count for which the server will accept a payload, default is 32 +TGUI_MAX_CHUNK_COUNT 32 diff --git a/dependencies.sh b/dependencies.sh index 671c39390775..c7c96fef39ee 100755 --- a/dependencies.sh +++ b/dependencies.sh @@ -4,8 +4,8 @@ #Final authority on what's required to fully build the project # byond version -export BYOND_MAJOR=515 -export BYOND_MINOR=1647 +export BYOND_MAJOR=516 +export BYOND_MINOR=1659 #rust version export RUST_VERSION=1.81.0 diff --git a/html/admin/search.js b/html/admin/search.js index 8c3bccba2c38..ade5d2de10c9 100644 --- a/html/admin/search.js +++ b/html/admin/search.js @@ -23,7 +23,7 @@ function updateSearch() { } if (found == 0) row.style.display = "none"; else { - row.style.display = "block"; + row.style.display='table-row'; /* DON'T make tables with block property */ row.className = alt_style; if (alt_style == "alt") alt_style = "norm"; else alt_style = "alt"; diff --git a/html/statbrowser.css b/html/statbrowser.css index dc693f42f756..7bd88fe4f297 100644 --- a/html/statbrowser.css +++ b/html/statbrowser.css @@ -1,3 +1,13 @@ +.light:root { + --scrollbar-base: #f2f2f2; + --scrollbar-thumb: #a7a7a7; +} + +html, +body { + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-base); +} + body { font-family: Verdana, Geneva, Tahoma, sans-serif; font-size: 12px !important; @@ -7,8 +17,16 @@ body { overflow-y: scroll; } +/** + * MARK: Dark theme colors + */ +.dark:root { + --scrollbar-base: #151515; + --scrollbar-thumb: #363636; +} + body.dark { - background-color: #131313; + background-color: #151515; color: #b2c4dd; scrollbar-base-color: #1c1c1c; scrollbar-face-color: #3b3b3b; diff --git a/html/statbrowser.js b/html/statbrowser.js index be49d24cdc92..1ae3337be52e 100644 --- a/html/statbrowser.js +++ b/html/statbrowser.js @@ -6,11 +6,11 @@ if (!Array.prototype.includes) { if (this[i] == thing) return true; } return false; - } + }; } if (!String.prototype.trim) { String.prototype.trim = function () { - return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ""); }; } @@ -31,9 +31,9 @@ var turfcontents = []; var turfname = ""; var imageRetryDelay = 500; var imageRetryLimit = 50; -var menu = document.getElementById('menu'); -var under_menu = document.getElementById('under_menu'); -var statcontentdiv = document.getElementById('statcontent'); +var menu = document.getElementById("menu"); +var under_menu = document.getElementById("under_menu"); +var statcontentdiv = document.getElementById("statcontent"); var storedimages = []; var split_admin_tabs = false; @@ -47,10 +47,8 @@ function run_after_focus(callback) { function createStatusTab(name) { if (name.indexOf(".") != -1) { var splitName = name.split("."); - if (split_admin_tabs && splitName[0] === "Admin") - name = splitName[1]; - else - name = splitName[0]; + if (split_admin_tabs && splitName[0] === "Admin") name = splitName[1]; + else name = splitName[0]; } if (document.getElementById(name) || name.trim() == "") { return; @@ -74,7 +72,7 @@ function createStatusTab(name) { //END ORDERING menu.appendChild(B); SendTabToByond(name); - under_menu.style.height = menu.clientHeight + 'px'; + under_menu.style.height = menu.clientHeight + "px"; } function removeStatusTab(name) { @@ -88,7 +86,7 @@ function removeStatusTab(name) { } menu.removeChild(document.getElementById(name)); TakeTabFromByond(name); - under_menu.style.height = menu.clientHeight + 'px'; + under_menu.style.height = menu.clientHeight + "px"; } function sortVerbs() { @@ -96,17 +94,16 @@ function sortVerbs() { var selector = a[0] == b[0] ? 1 : 0; if (a[selector].toUpperCase() < b[selector].toUpperCase()) { return 1; - } - else if (a[selector].toUpperCase() > b[selector].toUpperCase()) { + } else if (a[selector].toUpperCase() > b[selector].toUpperCase()) { return -1; } return 0; - }) + }); } window.onresize = function () { - under_menu.style.height = menu.clientHeight + 'px'; -} + under_menu.style.height = menu.clientHeight + "px"; +}; function addPermanentTab(name) { if (!permanent_tabs.includes(name)) { @@ -126,7 +123,10 @@ function removePermanentTab(name) { function checkStatusTab() { for (var i = 0; i < menu.children.length; i++) { - if (!verb_tabs.includes(menu.children[i].id) && !permanent_tabs.includes(menu.children[i].id)) { + if ( + !verb_tabs.includes(menu.children[i].id) && + !permanent_tabs.includes(menu.children[i].id) + ) { menu.removeChild(menu.children[i]); } } @@ -137,7 +137,7 @@ function remove_verb(v) { for (var i = verbs.length - 1; i >= 0; i--) { var part_to_remove = verbs[i]; if (part_to_remove[1] == verb_to_remove[1]) { - verbs.splice(i, 1) + verbs.splice(i, 1); } } } @@ -152,10 +152,8 @@ function verbs_cat_check(cat) { var tabCat = cat; if (cat.indexOf(".") != -1) { var splitName = cat.split("."); - if (split_admin_tabs && splitName[0] === "Admin") - tabCat = splitName[1]; - else - tabCat = splitName[0]; + if (split_admin_tabs && splitName[0] === "Admin") tabCat = splitName[1]; + else tabCat = splitName[0]; } var verbs_in_cat = 0; var verbcat = ""; @@ -170,29 +168,25 @@ function verbs_cat_check(cat) { var splitName = verbcat.split("."); if (split_admin_tabs && splitName[0] === "Admin") verbcat = splitName[1]; - else - verbcat = splitName[0]; + else verbcat = splitName[0]; } if (verbcat != tabCat || verbcat.trim() == "") { continue; - } - else { + } else { verbs_in_cat = 1; break; // we only need one } } if (verbs_in_cat != 1) { removeStatusTab(tabCat); - if (current_tab == tabCat) - tab_change("Status"); + if (current_tab == tabCat) tab_change("Status"); } } function findVerbindex(name, verblist) { for (var i = 0; i < verblist.length; i++) { var part = verblist[i]; - if (part[1] == name) - return i; + if (part[1] == name) return i; } } function wipe_verbs() { @@ -215,12 +209,12 @@ function SendTabsToByond() { } function SendTabToByond(tab) { - Byond.sendMessage("Send-Tabs", {tab: tab}); + Byond.sendMessage("Send-Tabs", { tab: tab }); } //Byond can't have this tab anymore since we're removing it function TakeTabFromByond(tab) { - Byond.sendMessage("Remove-Tabs", {tab: tab}); + Byond.sendMessage("Remove-Tabs", { tab: tab }); } function spell_cat_check(cat) { @@ -246,8 +240,8 @@ function tab_change(tab) { set_byond_tab(tab); if (document.getElementById(tab)) document.getElementById(tab).className = "button active"; // make current button active - var spell_tabs_thingy = (spell_tabs.includes(tab)); - var verb_tabs_thingy = (verb_tabs.includes(tab)); + var spell_tabs_thingy = spell_tabs.includes(tab); + var verb_tabs_thingy = verb_tabs.includes(tab); if (tab == "Status") { draw_status(); } else if (tab == "MC") { @@ -269,25 +263,29 @@ function tab_change(tab) { statcontentdiv.textContext = "Loading..."; } Byond.winset(Byond.windowId, { - 'is-visible': true, + "is-visible": true, }); } function set_byond_tab(tab) { - Byond.sendMessage("Set-Tab", {tab: tab}); + Byond.sendMessage("Set-Tab", { tab: tab }); } function draw_debug() { statcontentdiv.textContent = ""; var wipeverbstabs = document.createElement("div"); var link = document.createElement("a"); - link.onclick = function () { wipe_verbs() }; + link.onclick = function () { + wipe_verbs(); + }; link.textContent = "Wipe All Verbs"; wipeverbstabs.appendChild(link); document.getElementById("statcontent").appendChild(wipeverbstabs); var wipeUpdateVerbsTabs = document.createElement("div"); var updateLink = document.createElement("a"); - updateLink.onclick = function () { update_verbs() }; + updateLink.onclick = function () { + update_verbs(); + }; updateLink.textContent = "Wipe and Update All Verbs"; wipeUpdateVerbsTabs.appendChild(updateLink); document.getElementById("statcontent").appendChild(wipeUpdateVerbsTabs); @@ -302,16 +300,17 @@ function draw_debug() { var splitName = verb_tabs[i].split("."); if (split_admin_tabs && splitName[0] === "Admin") part = splitName[1]; - else - continue; + else continue; } var tr = document.createElement("tr"); var td1 = document.createElement("td"); td1.textContent = part; var a = document.createElement("a"); - a.onclick = function (part) { - return function () { removeStatusTab(part) }; - }(part); + a.onclick = (function (part) { + return function () { + removeStatusTab(part); + }; + })(part); a.textContent = " Delete Tab " + part; td1.appendChild(a); tr.appendChild(td1); @@ -347,17 +346,18 @@ function draw_debug() { table3.appendChild(trrr); } document.getElementById("statcontent").appendChild(table3); - } function draw_status() { if (!document.getElementById("Status")) { createStatusTab("Status"); current_tab = "Status"; } - statcontentdiv.textContent = ''; + statcontentdiv.textContent = ""; for (var i = 0; i < status_tab_parts.length; i++) { if (status_tab_parts[i].trim() == "") { - document.getElementById("statcontent").appendChild(document.createElement("br")); + document + .getElementById("statcontent") + .appendChild(document.createElement("br")); } else { var div = document.createElement("div"); div.textContent = status_tab_parts[i]; @@ -380,7 +380,8 @@ function draw_mc() { var td2 = document.createElement("td"); if (part[2]) { var a = document.createElement("a"); - a.href = "?_src_=vars;admin_token=" + href_token + ";Vars=" + part[2]; + a.href = + "?_src_=vars;admin_token=" + href_token + ";Vars=" + part[2]; a.textContent = part[1]; td2.appendChild(a); } else { @@ -397,8 +398,7 @@ function remove_tickets() { if (tickets) { tickets = []; removePermanentTab("Tickets"); - if (current_tab == "Tickets") - tab_change("Status"); + if (current_tab == "Tickets") tab_change("Status"); } checkStatusTab(); } @@ -407,8 +407,7 @@ function remove_sdql2() { if (sdql2) { sdql2 = []; removePermanentTab("SDQL2"); - if (current_tab == "SDQL2") - tab_change("Status"); + if (current_tab == "SDQL2") tab_change("Status"); } checkStatusTab(); } @@ -421,19 +420,19 @@ function remove_interviews() { } function iconError(e) { - if(current_tab != turfname) { + if (current_tab != turfname) { return; } setTimeout(function () { var node = e.target; - var current_attempts = Number(node.getAttribute("data-attempts")) || 0 + var current_attempts = Number(node.getAttribute("data-attempts")) || 0; if (current_attempts > imageRetryLimit) { return; } var src = node.src; node.src = null; - node.src = src + '#' + current_attempts; - node.setAttribute("data-attempts", current_attempts + 1) + node.src = src + "#" + current_attempts; + node.setAttribute("data-attempts", current_attempts + 1); draw_listedturf(); }, imageRetryDelay); } @@ -460,7 +459,7 @@ function draw_listedturf() { var b = document.createElement("div"); var clickcatcher = ""; b.className = "link"; - b.onmousedown = function (part) { + b.onmousedown = (function (part) { // The outer function is used to close over a fresh "part" variable, // rather than every onmousedown getting the "part" of the last entry. return function (e) { @@ -468,13 +467,13 @@ function draw_listedturf() { clickcatcher = "?src=" + part[1]; switch (e.button) { case 1: - clickcatcher += ";statpanel_item_click=middle" + clickcatcher += ";statpanel_item_click=middle"; break; case 2: - clickcatcher += ";statpanel_item_click=right" + clickcatcher += ";statpanel_item_click=right"; break; default: - clickcatcher += ";statpanel_item_click=left" + clickcatcher += ";statpanel_item_click=left"; } if (e.shiftKey) { clickcatcher += ";statpanel_item_shiftclick=1"; @@ -486,8 +485,8 @@ function draw_listedturf() { clickcatcher += ";statpanel_item_altclick=1"; } window.location.href = clickcatcher; - } - }(part); + }; + })(part); b.textContent = part[0]; table.appendChild(b); table.appendChild(document.createElement("br")); @@ -508,7 +507,7 @@ function remove_mc() { if (current_tab == "MC") { tab_change("Status"); } -}; +} function draw_sdql2() { statcontentdiv.textContent = ""; @@ -548,7 +547,12 @@ function draw_tickets() { var td2 = document.createElement("td"); if (part[2]) { var a = document.createElement("a"); - a.href = "?_src_=holder;admin_token=" + href_token + ";ahelp=" + part[2] + ";ahelp_action=ticket;statpanel_item_click=left;action=ticket"; + a.href = + "?_src_=holder;admin_token=" + + href_token + + ";ahelp=" + + part[2] + + ";ahelp_action=ticket;statpanel_item_click=left;action=ticket"; a.textContent = part[1]; td2.appendChild(a); } else if (part[3]) { @@ -572,10 +576,13 @@ function draw_interviews() { header.textContent = "Interviews"; body.appendChild(header); var manDiv = document.createElement("div"); - manDiv.className = "interview_panel_controls" + manDiv.className = "interview_panel_controls"; var manLink = document.createElement("a"); manLink.textContent = "Open Interview Manager Panel"; - manLink.href = "?_src_=holder;admin_token=" + href_token + ";interview_man=1;statpanel_item_click=left"; + manLink.href = + "?_src_=holder;admin_token=" + + href_token + + ";interview_man=1;statpanel_item_click=left"; manDiv.appendChild(manLink); body.appendChild(manDiv); @@ -608,7 +615,12 @@ function draw_interviews() { var td = document.createElement("td"); var a = document.createElement("a"); a.textContent = part["status"]; - a.href = "?_src_=holder;admin_token=" + href_token + ";interview=" + part["ref"] + ";statpanel_item_click=left"; + a.href = + "?_src_=holder;admin_token=" + + href_token + + ";interview=" + + part["ref"] + + ";statpanel_item_click=left"; td.appendChild(a); tr.appendChild(td); table.appendChild(tr); @@ -657,8 +669,7 @@ function draw_verbs(cat) { sortVerbs(); if (split_admin_tabs && cat.lastIndexOf(".") != -1) { var splitName = cat.split("."); - if (splitName[0] === "Admin") - cat = splitName[1]; + if (splitName[0] === "Admin") cat = splitName[1]; } verbs.reverse(); // sort verbs backwards before we draw for (var i = 0; i < verbs.length; ++i) { @@ -666,13 +677,17 @@ function draw_verbs(cat) { var name = part[0]; if (split_admin_tabs && name.lastIndexOf(".") != -1) { var splitName = name.split("."); - if (splitName[0] === "Admin") - name = splitName[1]; + if (splitName[0] === "Admin") name = splitName[1]; } var command = part[1]; - if (command && name.lastIndexOf(cat, 0) != -1 && (name.length == cat.length || name.charAt(cat.length) == ".")) { - var subCat = name.lastIndexOf(".") != -1 ? name.split(".")[1] : null; + if ( + command && + name.lastIndexOf(cat, 0) != -1 && + (name.length == cat.length || name.charAt(cat.length) == ".") + ) { + var subCat = + name.lastIndexOf(".") != -1 ? name.split(".")[1] : null; if (subCat && !additions[subCat]) { var newTable = document.createElement("div"); newTable.className = "grid-container"; @@ -710,9 +725,11 @@ function draw_verbs(cat) { function set_theme(which) { if (which == "light") { document.body.className = ""; + document.documentElement.className = "light"; set_style_sheet("browserOutput_white"); } else if (which == "dark") { document.body.className = "dark"; + document.documentElement.className = "dark"; set_style_sheet("browserOutput"); } } @@ -722,35 +739,35 @@ function set_style_sheet(sheet) { var currentSheet = document.getElementById("goonStyle"); currentSheet.parentElement.removeChild(currentSheet); } - var head = document.getElementsByTagName('head')[0]; + var head = document.getElementsByTagName("head")[0]; var sheetElement = document.createElement("link"); sheetElement.id = "goonStyle"; sheetElement.rel = "stylesheet"; sheetElement.type = "text/css"; sheetElement.href = sheet + ".css"; - sheetElement.media = 'all'; + sheetElement.media = "all"; head.appendChild(sheetElement); } function restoreFocus() { run_after_focus(function () { - Byond.winset('map', { + Byond.winset("map", { focus: true, }); }); } function getCookie(cname) { - var name = cname + '='; - var ca = document.cookie.split(';'); + var name = cname + "="; + var ca = document.cookie.split(";"); for (var i = 0; i < ca.length; i++) { var c = ca[i]; - while (c.charAt(0) == ' ') c = c.substring(1); + while (c.charAt(0) == " ") c = c.substring(1); if (c.indexOf(name) === 0) { return decoder(c.substring(name.length, c.length)); } } - return ''; + return ""; } function add_verb_list(payload) { @@ -758,18 +775,15 @@ function add_verb_list(payload) { to_add.sort(); // sort what we're adding for (var i = 0; i < to_add.length; i++) { var part = to_add[i]; - if (!part[0]) - continue; + if (!part[0]) continue; var category = part[0]; if (category.indexOf(".") != -1) { var splitName = category.split("."); if (split_admin_tabs && splitName[0] === "Admin") category = splitName[1]; - else - category = splitName[0]; + else category = splitName[0]; } - if (findVerbindex(part[1], verbs)) - continue; + if (findVerbindex(part[1], verbs)) continue; if (verb_tabs.includes(category)) { verbs.push(part); if (current_tab == category) { @@ -781,7 +795,7 @@ function add_verb_list(payload) { createStatusTab(category); } } -}; +} function init_spells() { var cat = ""; @@ -806,7 +820,7 @@ window.onload = function () { Byond.sendMessage("Update-Verbs"); }; -Byond.subscribeTo('update_spells', function (payload) { +Byond.subscribeTo("update_spells", function (payload) { spell_tabs = payload.spell_tabs; var do_update = false; if (spell_tabs.includes(current_tab)) { @@ -823,20 +837,19 @@ Byond.subscribeTo('update_spells', function (payload) { } }); -Byond.subscribeTo('remove_verb_list', function (v) { +Byond.subscribeTo("remove_verb_list", function (v) { var to_remove = v; for (var i = 0; i < to_remove.length; i++) { remove_verb(to_remove[i]); } check_verbs(); sortVerbs(); - if (verb_tabs.includes(current_tab)) - draw_verbs(current_tab); + if (verb_tabs.includes(current_tab)) draw_verbs(current_tab); }); // passes a 2D list of (verbcategory, verbname) creates tabs and adds verbs to respective list // example (IC, Say) -Byond.subscribeTo('init_verbs', function (payload) { +Byond.subscribeTo("init_verbs", function (payload) { wipe_verbs(); // remove all verb categories so we can replace them checkStatusTab(); // remove all status tabs verb_tabs = payload.panel_tabs; @@ -860,15 +873,17 @@ Byond.subscribeTo('init_verbs', function (payload) { SendTabsToByond(); }); -Byond.subscribeTo('update_stat', function (payload) { +Byond.subscribeTo("update_stat", function (payload) { status_tab_parts = [payload.ping_str]; var parsed = payload.global_data; - for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]); + for (var i = 0; i < parsed.length; i++) + if (parsed[i] != null) status_tab_parts.push(parsed[i]); parsed = payload.other_str; - for (var i = 0; i < parsed.length; i++) if (parsed[i] != null) status_tab_parts.push(parsed[i]); + for (var i = 0; i < parsed.length; i++) + if (parsed[i] != null) status_tab_parts.push(parsed[i]); if (current_tab == "Status") { draw_status(); @@ -877,7 +892,7 @@ Byond.subscribeTo('update_stat', function (payload) { } }); -Byond.subscribeTo('update_mc', function (payload) { +Byond.subscribeTo("update_mc", function (payload) { mc_tab_parts = payload.mc_data; mc_tab_parts.splice(0, 0, ["Location:", payload.coord_entry]); @@ -892,13 +907,13 @@ Byond.subscribeTo('update_mc', function (payload) { } }); -Byond.subscribeTo('remove_spells', function () { +Byond.subscribeTo("remove_spells", function () { for (var s = 0; s < spell_tabs.length; s++) { removeStatusTab(spell_tabs[s]); } }); -Byond.subscribeTo('init_spells', function () { +Byond.subscribeTo("init_spells", function () { var cat = ""; for (var i = 0; i < spell_tabs.length; i++) { cat = spell_tabs[i]; @@ -909,13 +924,13 @@ Byond.subscribeTo('init_spells', function () { } }); -Byond.subscribeTo('check_spells', function () { +Byond.subscribeTo("check_spells", function () { for (var v = 0; v < spell_tabs.length; v++) { spell_cat_check(spell_tabs[v]); } }); -Byond.subscribeTo('create_debug', function () { +Byond.subscribeTo("create_debug", function () { if (!document.getElementById("Debug Stat Panel")) { addPermanentTab("Debug Stat Panel"); } else { @@ -923,14 +938,14 @@ Byond.subscribeTo('create_debug', function () { } }); -Byond.subscribeTo('create_listedturf', function (TN) { +Byond.subscribeTo("create_listedturf", function (TN) { remove_listedturf(); // remove the last one if we had one turfname = TN; addPermanentTab(turfname); tab_change(turfname); }); -Byond.subscribeTo('remove_admin_tabs', function () { +Byond.subscribeTo("remove_admin_tabs", function () { href_token = null; remove_mc(); remove_tickets(); @@ -938,22 +953,22 @@ Byond.subscribeTo('remove_admin_tabs', function () { remove_interviews(); }); -Byond.subscribeTo('update_listedturf', function (TC) { +Byond.subscribeTo("update_listedturf", function (TC) { turfcontents = TC; if (current_tab == turfname) { draw_listedturf(); } }); -Byond.subscribeTo('update_interviews', function (I) { +Byond.subscribeTo("update_interviews", function (I) { interviewManager = I; if (current_tab == "Tickets") { draw_interviews(); } }); -Byond.subscribeTo('update_split_admin_tabs', function (status) { - status = (status == true); +Byond.subscribeTo("update_split_admin_tabs", function (status) { + status = status == true; if (split_admin_tabs !== status) { if (split_admin_tabs === true) { @@ -966,13 +981,13 @@ Byond.subscribeTo('update_split_admin_tabs', function (status) { split_admin_tabs = status; }); -Byond.subscribeTo('add_admin_tabs', function (ht) { +Byond.subscribeTo("add_admin_tabs", function (ht) { href_token = ht; addPermanentTab("MC"); addPermanentTab("Tickets"); }); -Byond.subscribeTo('update_sdql2', function (S) { +Byond.subscribeTo("update_sdql2", function (S) { sdql2 = S; if (sdql2.length > 0 && !verb_tabs.includes("SDQL2")) { verb_tabs.push("SDQL2"); @@ -983,7 +998,7 @@ Byond.subscribeTo('update_sdql2', function (S) { } }); -Byond.subscribeTo('update_tickets', function (T) { +Byond.subscribeTo("update_tickets", function (T) { tickets = T; if (!verb_tabs.includes("Tickets")) { verb_tabs.push("Tickets"); @@ -994,10 +1009,10 @@ Byond.subscribeTo('update_tickets', function (T) { } }); -Byond.subscribeTo('remove_listedturf', remove_listedturf); +Byond.subscribeTo("remove_listedturf", remove_listedturf); -Byond.subscribeTo('remove_sdql2', remove_sdql2); +Byond.subscribeTo("remove_sdql2", remove_sdql2); -Byond.subscribeTo('remove_mc', remove_mc); +Byond.subscribeTo("remove_mc", remove_mc); -Byond.subscribeTo('add_verb_list', add_verb_list); +Byond.subscribeTo("add_verb_list", add_verb_list); diff --git a/interface/skin.dmf b/interface/skin.dmf index 40292d8ee5e3..e48bcce58e10 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -286,16 +286,25 @@ window "outputwindow" text = "Me" command = ".winset \"mebutton.is-checked=true ? input.command=\"!me \\\"\" : input.command=\"\"mebutton.is-checked=true ? saybutton.is-checked=false\"\"mebutton.is-checked=true ? oocbutton.is-checked=false\"" button-type = pushbox - elem "browseroutput" - type = BROWSER + elem "legacy_output_selector" + type = CHILD pos = 0,0 size = 640x456 anchor1 = 0,0 anchor2 = 100,100 - background-color = #ffffff - is-visible = false - is-disabled = true - saved-params = "" + saved-params = "splitter" + left = "output_legacy" + is-vert = false + +window "output_legacy" + elem "output_legacy" + type = MAIN + pos = 0,0 + size = 640x456 + anchor1 = -1,-1 + anchor2 = -1,-1 + saved-params = "pos;size;is-minimized;is-maximized" + is-pane = true elem "output" type = OUTPUT pos = 0,0 @@ -305,6 +314,23 @@ window "outputwindow" is-default = true saved-params = "" +window "output_browser" + elem "output_browser" + type = MAIN + pos = 0,0 + size = 640x456 + anchor1 = -1,-1 + anchor2 = -1,-1 + saved-params = "pos;size;is-minimized;is-maximized" + is-pane = true + elem "browseroutput" + type = BROWSER + pos = 0,0 + size = 640x456 + anchor1 = 0,0 + anchor2 = 100,100 + saved-params = "" + window "popupwindow" elem "popupwindow" type = MAIN diff --git a/mod_celadon/fixes/code/newscaster.dm b/mod_celadon/fixes/code/newscaster.dm index 232102f8e536..288aec40425b 100644 --- a/mod_celadon/fixes/code/newscaster.dm +++ b/mod_celadon/fixes/code/newscaster.dm @@ -275,21 +275,21 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat += "Добро пожаловать в новостной монитор #[unit_no].
    Системы успешно загружены и доступны." dat += "
    Разработано корпорацией: Griffon Inc" if(GLOB.news_network.wanted_issue.active) - dat+= "


    Межсекторный розыск" - dat+= "

    Создать новостной канал" - dat+= "
    Ознакомиться с лентой каналов" - dat+= "
    > Создать новый пост" - dat+= "
    > Напечатать газету" - dat+= "
    Обновить пользователя" - dat+= "

    Выйти" + dat+= "
    Межсекторный розыск" + dat+= "

    Создать новостной канал" + dat+= "
    Ознакомиться с лентой каналов" + dat+= "
    > Создать новый пост" + dat+= "
    > Напечатать газету" + dat+= "
    Обновить пользователя" + dat+= "

    Выйти" if(securityCaster) var/wanted_already = 0 if(GLOB.news_network.wanted_issue.active) wanted_already = 1 dat+="
    Модуль безопасности системы Griffon:
    " - dat+="
    [(wanted_already) ? ("Настроить") : ("Объявить")] \"Розыск\"" - dat+="
    Цензура новостей" - dat+="
    Установить метку D-Notice" + dat+="
    [(wanted_already) ? ("Настроить") : ("Объявить")] \"Розыск\"" + dat+="
    Цензура новостей" + dat+="
    Установить метку D-Notice" dat+="

    Вы были успешно зарегистрированы под именем:
    [scanned_user]" if(1) dat+= "Доступные каналы в вашем секторе:
    " @@ -298,31 +298,31 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) else for(var/datum/newscaster/feed_channel/CHANNEL in GLOB.news_network.network_channels) if(CHANNEL.is_admin_channel) - dat+="[CHANNEL.channel_name]
    " + dat+="[CHANNEL.channel_name]
    " else - dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ""]
    " - dat+="

    Обновить" - dat+="
    Вернуться" + dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ""]
    " + dat+="

    Обновить" + dat+="
    Вернуться" if(2) dat+="Регистрация нового канала:" - dat+="
    Название канала: [channel_name]
    " + dat+="
    Название канала: [channel_name]
    " dat+="Автор канала: [scanned_user]
    " - dat+="Будет ли канал являться публичным?: [(c_locked) ? ("Нет") : ("Да")]

    " - dat+="
    Подтвердить

    Отмена
    " + dat+="Будет ли канал являться публичным?: [(c_locked) ? ("Нет") : ("Да")]

    " + dat+="
    Подтвердить

    Отмена
    " if(3) dat+="Публикация нового поста:" - dat+="
    Выбранный канал: [channel_name]
    " + dat+="
    Выбранный канал: [channel_name]
    " dat+="Автор сообщения: [scanned_user]
    " - dat+="Сообщение:
    [parsemarkdown(msg, user)]
    " - dat+="Прикреплённые сообщения: [(picture ? "Фото прикреплено" : "Фото отсутствует")]
    " - dat+="Комментарии [allow_comments ? "разрешены" : "отключены"]
    " - dat+="
    Подтвердить

    Отмена
    " + dat+="Сообщение:
    [parsemarkdown(msg, user)]
    " + dat+="Прикреплённые сообщения: [(picture ? "Фото прикреплено" : "Фото отсутствует")]
    " + dat+="Комментарии [allow_comments ? "разрешены" : "отключены"]
    " + dat+="
    Подтвердить

    Отмена
    " if(4) dat+="Новый пост успешно загружен ​​на канал '[channel_name]'.

    " - dat+="
    Вернуться
    " + dat+="
    Вернуться
    " if(5) dat+="Канал '[channel_name]' успешно создан.

    " - dat+="
    Вернуться
    " + dat+="
    Вернуться
    " if(6) dat+="ОШИБКА: Не удалось отправить новый пост в сеть.

    " if(channel_name=="") @@ -331,7 +331,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="Автор канала не определён.
    " if(msg == "" || msg == "\[REDACTED\]") dat+="Недопустимое сообщение.
    " - dat+="
    Return
    " + dat+="
    Return
    " if(7) dat+="ОШИБКА: Не удалось создать канал в сети Griffon.

    " var/list/existing_authors = list() @@ -353,7 +353,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="Название канала уже используется.
    " if(scanned_user=="Unknown") dat+="Автор канала не определён.
    " - dat+="
    Вернуться
    " + dat+="
    Вернуться
    " if(8) var/total_num=length(GLOB.news_network.network_channels) var/active_num=total_num @@ -365,8 +365,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) active_num-- dat+="В настоящее время сеть обслуживает в общей сложности [total_num] новостных каналов, [active_num] из них активны, и всего [message_num] постов." dat+="

    Остаток жидкой бумаги: [(paper_remaining) *100 ] cm^3" - dat+="

    Распечатать газету" - dat+="
    Отмена" + dat+="

    Распечатать газету" + dat+="
    Отмена" if(9) dat+="[viewing_channel.channel_name]:
    \[Автор: [viewing_channel.returnAuthor(-1)]\]
    " if(viewing_channel.censored) @@ -393,9 +393,9 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) if(MESSAGE.locked) dat+="Комментарии заблокированы
    " else - dat+="Прокомментировать
    " - dat+="

    Обновить" - dat+="
    Вернуться" + dat+="Прокомментировать
    " + dat+="

    Обновить" + dat+="
    Вернуться" if(10) dat+="Инструмент цензуры каналов сети Griffon
    " dat+="ПРИМЕЧАНИЕ: Из-за технических работ полное удаление каналов невозможно.
    " @@ -405,8 +405,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="Активные каналы не найдены...
    " else for(var/datum/newscaster/feed_channel/CHANNEL in GLOB.news_network.network_channels) - dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ""]
    " - dat+="
    Отмена" + dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ""]
    " + dat+="
    Отмена" if(11) dat+="Модуль D-Notice:
    " dat+="Метка D-Notice должна быть прикреплена к каналу, если администрация сочтет его запрещённым для сектора. " @@ -416,24 +416,24 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="Активные каналы не найдены...
    " else for(var/datum/newscaster/feed_channel/CHANNEL in GLOB.news_network.network_channels) - dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ""]
    " - dat+="
    Вернуться" + dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ""]
    " + dat+="
    Вернуться" if(12) dat+="[viewing_channel.channel_name]:
    \[ Автор: [viewing_channel.returnAuthor(-1)] \]
    " - dat+="[(viewing_channel.authorCensor) ? ("Отменить цензуру автора") : ("Скрыть имя автора")]
    " + dat+="[(viewing_channel.authorCensor) ? ("Отменить цензуру автора") : ("Скрыть имя автора")]
    " if(!length(viewing_channel.messages)) dat+="В канале не найдено постов...
    " else for(var/datum/newscaster/feed_message/MESSAGE in viewing_channel.messages) dat+="-[MESSAGE.returnBody(-1)]
    \[Автор [MESSAGE.returnAuthor(-1)]\]
    " - dat+="[(MESSAGE.bodyCensor) ? ("Отменить цензуру сообщения") : ("Заблокировать сообщение")] - [(MESSAGE.authorCensor) ? ("Отменить цензуру имени автора") : ("Скрыть имя автора")]
    " - dat+="[MESSAGE.comments.len] comment[MESSAGE.comments.len > 1 ? "s" : ""]: [MESSAGE.locked ? "Разблокированы" : "Заблокированы"]
    " + dat+="[(MESSAGE.bodyCensor) ? ("Отменить цензуру сообщения") : ("Заблокировать сообщение")] - [(MESSAGE.authorCensor) ? ("Отменить цензуру имени автора") : ("Скрыть имя автора")]
    " + dat+="[MESSAGE.comments.len] comment[MESSAGE.comments.len > 1 ? "s" : ""]: [MESSAGE.locked ? "Разблокированы" : "Заблокированы"]
    " for(var/datum/newscaster/feed_comment/comment in MESSAGE.comments) - dat+="[comment.body] X
    [comment.author] [comment.time_stamp]
    " - dat+="
    Вернуться" + dat+="[comment.body] X
    [comment.author] [comment.time_stamp]
    " + dat+="
    Вернуться" if(13) dat+="[viewing_channel.channel_name]:
    \[ Автор: [viewing_channel.returnAuthor(-1)] \]
    " - dat+="Если вы считаете cообщения канала, что перечислены ниже, опасными для сектора, вы можете редактировать метку D-Notice для этого канала.
    " + dat+="Если вы считаете cообщения канала, что перечислены ниже, опасными для сектора, вы можете редактировать метку D-Notice для этого канала.
    " if(viewing_channel.censored) dat+="ВНИМАНИЕ: Этот канал был признан угрожающим благополучию сектора и отмечен меткой D-Notice администрацией Griffon.
    " dat+="Под действием метки D-Notice дальнейшее добавление новых постов запрещено.


    " @@ -443,7 +443,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) else for(var/datum/newscaster/feed_message/MESSAGE in viewing_channel.messages) dat+="[MESSAGE.returnBody(-1)]
    \[Автор [MESSAGE.returnAuthor(-1)]\]
    " - dat+="
    Вернуться" + dat+="
    Вернуться" if(14) dat+="Модуль безопасности:" var/wanted_already = 0 @@ -454,20 +454,20 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) if(wanted_already) dat+="
    Преступник уже объявлен в новостных каналах. Вы можете отредактировать или отменить ориентировку ниже.
    " dat+="
    " - dat+="Имя цели: [channel_name]
    " - dat+="Описание: [msg]
    " - dat+="Прикреплённые материалы: [(picture ? "Фото прикрепленно" : "Фото отсутствует")]
    " + dat+="Имя цели: [channel_name]
    " + dat+="Описание: [msg]
    " + dat+="Прикреплённые материалы: [(picture ? "Фото прикрепленно" : "Фото отсутствует")]
    " if(wanted_already) dat+="Розыск был объявлен пользователем: [GLOB.news_network.wanted_issue.scannedUser]
    " else dat+="Розыск будет объявлен пользователем:[scanned_user]
    " - dat+="
    [(wanted_already) ? ("Редактировать цель") : ("Подтвердить")]" + dat+="
    [(wanted_already) ? ("Редактировать цель") : ("Подтвердить")]" if(wanted_already) - dat+="
    Отменить розыск" - dat+="
    Отмена" + dat+="
    Отменить розыск" + dat+="
    Отмена" if(15) dat+="Розыск на '[channel_name]' был успешно опубликован на первых строчках сети Griffon.

    " - dat+="
    Вернуться
    " + dat+="
    Вернуться
    " if(16) dat+="ERROR: Розыск был отменён сетью Griffon.

    " if(channel_name=="" || channel_name == "\[REDACTED\]") @@ -476,10 +476,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="Автор не идентифицирован.
    " if(msg == "" || msg == "\[REDACTED\]") dat+="Неверное описание.
    " - dat+="
    Вернуться
    " + dat+="
    Вернуться
    " if(17) dat+="Розыск успешно был удалён с сети Griffon
    " - dat+="
    Вернуться
    " + dat+="
    Вернуться
    " if(18) if(GLOB.news_network.wanted_issue.active) dat+="-- ОБЪЯВЛЕН РОЗЫСК --
    \[Подтверждено: [GLOB.news_network.wanted_issue.scannedUser]\]
    " @@ -493,16 +493,16 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="Отсутствуют" else dat+="Объявленных розысков не найдено

    " - dat+="

    Вернуться
    " + dat+="

    Вернуться
    " if(19) dat+="Розыск для '[channel_name]' успешно отредактирован

    " - dat+="
    Вернуться
    " + dat+="
    Вернуться
    " if(20) dat+="Файл успешно был распечатан. Пожалуйста, возьмите газету из нижней части машины.

    " - dat+="Вернуться" + dat+="Вернуться" if(21) dat+="Невозможно напечатать газету. Недостаточно бумаги. Пожалуйста, сообщите обслуживающему персоналу о необходимости пополнить запасы машины.

    " - dat+="Вернуться" + dat+="Вернуться" var/datum/browser/popup = new(human_or_robot_user, "newscaster_main", "Новостной монитор #[unit_no]", 400, 600) popup.set_content(dat) popup.open() @@ -917,7 +917,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="" if(scribble_page==curr_page) dat+="
    В конце этой страницы есть небольшие каракули... \"[scribble]\"" - dat+= "
    Next Page
    Close
    " + dat+= "
    Next Page
    Close
    " if(1) // X channel pages inbetween. for(var/datum/newscaster/feed_channel/NP in news_content) pages++ @@ -946,7 +946,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="" if(scribble_page==curr_page) dat+="
    В конце этой страницы есть небольшая каракуля... \"[scribble]\"" - dat+= "

    Previous Page
    Next Page
    " + dat+= "

    Previous Page
    Next Page
    " if(2) //Last page for(var/datum/newscaster/feed_channel/NP in news_content) pages++ @@ -964,7 +964,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster/security_unit, 30) dat+="Кроме неинтересных объявлений на этой странице ничего нет..." if(scribble_page==curr_page) dat+="
    В конце этой страницы есть небольшая каракуля... \"[scribble]\"" - dat+= "
    Previous Page
    " + dat+= "
    Previous Page
    " dat+="

    [curr_page+1]
    " human_user << browse(dat, "window=newspaper_main;size=300x400") onclose(human_user, "newspaper_main") diff --git a/mod_celadon/mobs/code/pets/rouge.dm b/mod_celadon/mobs/code/pets/rouge.dm index fbba7766fb9c..520ec5548a3e 100644 --- a/mod_celadon/mobs/code/pets/rouge.dm +++ b/mod_celadon/mobs/code/pets/rouge.dm @@ -236,8 +236,8 @@ // user.set_machine(src) // var/dat = {"
    Inventory of [name]

    "} -// dat += "
    Head: [inventory_head]" : "add_inv=head'>Nothing"]" -// // dat += "
    Collar: [pcollar]" : "add_inv=collar'>Nothing"]" +// dat += "
    Head: [inventory_head]" : "add_inv=head'>Nothing"]" +// // dat += "
    Collar: [pcollar]" : "add_inv=collar'>Nothing"]" // var/datum/browser/popup = new(user, "mob[UID()]", "[src]", 440, 250) // popup.set_content(dat) diff --git a/mod_celadon/return_content_clowns/code/honkbot.dm b/mod_celadon/return_content_clowns/code/honkbot.dm index 76c48dea1207..4ec8fdd8990d 100644 --- a/mod_celadon/return_content_clowns/code/honkbot.dm +++ b/mod_celadon/return_content_clowns/code/honkbot.dm @@ -87,12 +87,12 @@ Status: []
    Behaviour controls are [locked ? "locked" : "unlocked"]
    Maintenance panel panel is [open ? "opened" : "closed"]"}, -"[on ? "On" : "Off"]" ) +"[on ? "On" : "Off"]" ) if(!locked || issilicon(user) || isAdminGhostAI(user)) dat += text({"
    Auto Patrol: []"}, -"[auto_patrol ? "On" : "Off"]" ) +"[auto_patrol ? "On" : "Off"]" ) return dat /mob/living/simple_animal/bot/honkbot/proc/judgement_criteria() diff --git a/shiptest.dme b/shiptest.dme index 0c6c3c3f9e83..f59bc6e21aaa 100644 --- a/shiptest.dme +++ b/shiptest.dme @@ -73,6 +73,7 @@ #include "code\__DEFINES\fov.dm" #include "code\__DEFINES\generators.dm" #include "code\__DEFINES\guns.dm" +#include "code\__DEFINES\html_assistant.dm" #include "code\__DEFINES\hud.dm" #include "code\__DEFINES\icon_smoothing.dm" #include "code\__DEFINES\important_recursive_contents.dm" diff --git a/tgui/global.d.ts b/tgui/global.d.ts index 9cb8e37c2e8c..f54583294073 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -41,6 +41,21 @@ type ByondType = { */ windowId: string; + /** + * True if javascript is running in BYOND. + */ + IS_BYOND: boolean; + + /** + * Version of Trident engine of Internet Explorer. Null if N/A. + */ + TRIDENT: number | null; + + /** + * Version of Blink engine of WebView2. Null if N/A. + */ + BLINK: number | null; + /** * If `true`, unhandled errors and common mistakes result in a blue screen * of death, which stops this window from handling incoming messages and @@ -132,6 +147,11 @@ type ByondType = { */ parseJson(text: string): any; + /** + * Downloads a blob, platform-agnostic + */ + saveBlob(blob: Blob, filename: string, ext: string): void; + /** * Sends a message to `/datum/tgui_window` which hosts this window instance. */ @@ -158,6 +178,16 @@ type ByondType = { * Loads a script into the document. */ loadJs(url: string): void; + + /** + * Maps icons to their ref + */ + iconRefMap: Record; + + /** + * Downloads a blob, platform-agnostic + */ + saveBlob(blob: Blob, filename: string, ext: string): void; }; /** @@ -170,4 +200,13 @@ interface Window { Byond: ByondType; __store__: Store; __augmentStack__: (store: Store) => StackAugmentor; + + // IE IndexedDB stuff. + msIndexedDB: IDBFactory; + msIDBTransaction: IDBTransaction; + + // 516 byondstorage API. + hubStorage: Storage; + domainStorage: Storage; + serverStorage: Storage; } diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.ts similarity index 54% rename from tgui/packages/common/storage.js rename to tgui/packages/common/storage.ts index acf842f64083..a1c20effa2df 100644 --- a/tgui/packages/common/storage.js +++ b/tgui/packages/common/storage.ts @@ -7,9 +7,14 @@ */ export const IMPL_MEMORY = 0; -export const IMPL_LOCAL_STORAGE = 1; +export const IMPL_HUB_STORAGE = 1; export const IMPL_INDEXED_DB = 2; +type StorageImplementation = + | typeof IMPL_MEMORY + | typeof IMPL_HUB_STORAGE + | typeof IMPL_INDEXED_DB; + const INDEXED_DB_VERSION = 1; const INDEXED_DB_NAME = 'tgui'; const INDEXED_DB_STORE_NAME = 'storage-v1'; @@ -17,6 +22,14 @@ const INDEXED_DB_STORE_NAME = 'storage-v1'; const READ_ONLY = 'readonly'; const READ_WRITE = 'readwrite'; +type StorageBackend = { + impl: StorageImplementation, + get(key: string): Promise, + set(key: string, value: any): Promise, + remove(key: string): Promise, + clear(): Promise, +}; + const testGeneric = (testFn) => () => { try { return Boolean(testFn()); @@ -25,69 +38,72 @@ const testGeneric = (testFn) => () => { } }; -// Localstorage can sometimes throw an error, even if DOM storage is not -// disabled in IE11 settings. -// See: https://superuser.com/questions/1080011 -// prettier-ignore -const testLocalStorage = testGeneric(() => ( - window.localStorage && window.localStorage.getItem -)); +const testHubStorage = testGeneric( + () => window.hubStorage && !!window.hubStorage.getItem +); // prettier-ignore const testIndexedDb = testGeneric(() => ( (window.indexedDB || window.msIndexedDB) - && (window.IDBTransaction || window.msIDBTransaction) + && !!(window.IDBTransaction || window.msIDBTransaction) )); -class MemoryBackend { +class MemoryBackend implements StorageBackend { + private store: Record; + public impl: StorageImplementation; constructor() { this.impl = IMPL_MEMORY; this.store = {}; } - get(key) { + async get(key: string): Promise { return this.store[key]; } - set(key, value) { + async set(key: string, value: any): Promise { this.store[key] = value; } - remove(key) { + async remove(key: string): Promise { this.store[key] = undefined; } - clear() { + async clear(): Promise { this.store = {}; } } -class LocalStorageBackend { +class HubStorageBackend implements StorageBackend { + public impl: StorageImplementation; constructor() { - this.impl = IMPL_LOCAL_STORAGE; + this.impl = IMPL_HUB_STORAGE; } - get(key) { - const value = localStorage.getItem(key); + async get(key: string): Promise { + const value = await window.hubStorage.getItem(key); if (typeof value === 'string') { return JSON.parse(value); } + return undefined; } - set(key, value) { - localStorage.setItem(key, JSON.stringify(value)); + async set(key: string, value: any): Promise { + window.hubStorage.setItem(key, JSON.stringify(value)); } - remove(key) { - localStorage.removeItem(key); + async remove(key: string): Promise { + window.hubStorage.removeItem(key); } - clear() { - localStorage.clear(); + async clear(): Promise { + window.hubStorage.clear(); } } -class IndexedDbBackend { +class IndexedDbBackend implements StorageBackend { + public impl: StorageImplementation; + public dbPromise: Promise; + constructor() { this.impl = IMPL_INDEXED_DB; /** @type {Promise} */ @@ -98,7 +114,12 @@ class IndexedDbBackend { try { req.result.createObjectStore(INDEXED_DB_STORE_NAME); } catch (err) { - reject(new Error('Failed to upgrade IDB: ' + req.error)); + reject( + new Error( + 'Failed to upgrade IDB: ' + + (err instanceof Error ? err.message : String(err)), + ), + ); } }; req.onsuccess = () => resolve(req.result); @@ -108,14 +129,14 @@ class IndexedDbBackend { }); } - getStore(mode) { - // prettier-ignore - return this.dbPromise.then((db) => db + private async getStore(mode: IDBTransactionMode): Promise { + const db = await this.dbPromise; + return db .transaction(INDEXED_DB_STORE_NAME, mode) - .objectStore(INDEXED_DB_STORE_NAME)); + .objectStore(INDEXED_DB_STORE_NAME); } - async get(key) { + async get(key: string): Promise { const store = await this.getStore(READ_ONLY); return new Promise((resolve, reject) => { const req = store.get(key); @@ -124,26 +145,19 @@ class IndexedDbBackend { }); } - async set(key, value) { - // The reason we don't _save_ null is because IE 10 does - // not support saving the `null` type in IndexedDB. How - // ironic, given the bug below! - // See: https://github.com/mozilla/localForage/issues/161 - if (value === null) { - value = undefined; - } + async set(key: string, value: any): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.put(value, key); } - async remove(key) { + async remove(key: string): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.delete(key); } - async clear() { + async clear(): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.clear(); @@ -154,9 +168,16 @@ class IndexedDbBackend { * Web Storage Proxy object, which selects the best backend available * depending on the environment. */ -class StorageProxy { +class StorageProxy implements StorageBackend { + private backendPromise: Promise; + public impl: StorageImplementation = IMPL_MEMORY; + constructor() { this.backendPromise = (async () => { + if (!Byond.TRIDENT && testHubStorage()) { + return new HubStorageBackend(); + } + // TODO: Remove with 516 if (testIndexedDb()) { try { const backend = new IndexedDbBackend(); @@ -164,29 +185,29 @@ class StorageProxy { return backend; } catch {} } - if (testLocalStorage()) { - return new LocalStorageBackend(); - } + console.warn( + 'No supported storage backend found. Using in-memory storage.', + ); return new MemoryBackend(); })(); } - async get(key) { + async get(key: string): Promise { const backend = await this.backendPromise; return backend.get(key); } - async set(key, value) { + async set(key: string, value: any): Promise { const backend = await this.backendPromise; return backend.set(key, value); } - async remove(key) { + async remove(key: string): Promise { const backend = await this.backendPromise; return backend.remove(key); } - async clear() { + async clear(): Promise { const backend = await this.backendPromise; return backend.clear(); } diff --git a/tgui/packages/tgui-panel/chat/renderer.js b/tgui/packages/tgui-panel/chat/renderer.js index 1f595bffc7a9..d5bbc6562744 100644 --- a/tgui/packages/tgui-panel/chat/renderer.js +++ b/tgui/packages/tgui-panel/chat/renderer.js @@ -603,7 +603,7 @@ class ChatRenderer { + '\n' + '\n'; // Create and send a nice blob - const blob = new Blob([pageHtml]); + const blob = new Blob([pageHtml], { type: 'text/plain' }); const timestamp = new Date() .toISOString() .substring(0, 19) diff --git a/tgui/packages/tgui-panel/chat/renderer.jsx b/tgui/packages/tgui-panel/chat/renderer.jsx new file mode 100644 index 000000000000..f588cac84ac0 --- /dev/null +++ b/tgui/packages/tgui-panel/chat/renderer.jsx @@ -0,0 +1,643 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { EventEmitter } from 'common/events'; +import { classes } from 'common/react'; +import { createRoot } from 'react-dom/client'; +import { Tooltip } from 'tgui/components'; +import { createLogger } from 'tgui/logging'; + +import { + COMBINE_MAX_MESSAGES, + COMBINE_MAX_TIME_WINDOW, + IMAGE_RETRY_DELAY, + IMAGE_RETRY_LIMIT, + IMAGE_RETRY_MESSAGE_AGE, + MAX_PERSISTED_MESSAGES, + MAX_VISIBLE_MESSAGES, + MESSAGE_PRUNE_INTERVAL, + MESSAGE_TYPE_INTERNAL, + MESSAGE_TYPE_UNKNOWN, + MESSAGE_TYPES, +} from './constants'; +import { canPageAcceptType, createMessage, isSameMessage } from './model'; +import { highlightNode, linkifyNode } from './replaceInTextNode'; + +const logger = createLogger('chatRenderer'); + +// We consider this as the smallest possible scroll offset +// that is still trackable. +const SCROLL_TRACKING_TOLERANCE = 24; + +// List of injectable component names to the actual type +export const TGUI_CHAT_COMPONENTS = { + Tooltip, +}; + +// List of injectable attibute names mapped to their proper prop +// We need this because attibutes don't support lowercase names +export const TGUI_CHAT_ATTRIBUTES_TO_PROPS = { + position: 'position', + content: 'content', +}; + +const findNearestScrollableParent = (startingNode) => { + const body = document.body; + let node = startingNode; + while (node && node !== body) { + // This definitely has a vertical scrollbar, because it reduces + // scrollWidth of the element. Might not work if element uses + // overflow: hidden. + if (node.scrollWidth < node.offsetWidth) { + return node; + } + node = node.parentNode; + } + return window; +}; + +const createHighlightNode = (text, color) => { + const node = document.createElement('span'); + node.className = 'Chat__highlight'; + node.setAttribute('style', 'background-color:' + color); + node.textContent = text; + return node; +}; + +const createMessageNode = () => { + const node = document.createElement('div'); + node.className = 'ChatMessage'; + return node; +}; + +const createReconnectedNode = () => { + const node = document.createElement('div'); + node.className = 'Chat__reconnected'; + return node; +}; + +const handleImageError = (e) => { + setTimeout(() => { + /** @type {HTMLImageElement} */ + const node = e.target; + if (!node) { + return; + } + const attempts = parseInt(node.getAttribute('data-reload-n'), 10) || 0; + if (attempts >= IMAGE_RETRY_LIMIT) { + logger.error(`failed to load an image after ${attempts} attempts`); + return; + } + const src = node.src; + node.src = null; + node.src = src + '#' + attempts; + node.setAttribute('data-reload-n', attempts + 1); + }, IMAGE_RETRY_DELAY); +}; + +/** + * Assigns a "times-repeated" badge to the message. + */ +const updateMessageBadge = (message) => { + const { node, times } = message; + if (!node || !times) { + // Nothing to update + return; + } + const foundBadge = node.querySelector('.Chat__badge'); + const badge = foundBadge || document.createElement('div'); + badge.textContent = times; + badge.className = classes(['Chat__badge', 'Chat__badge--animate']); + requestAnimationFrame(() => { + badge.className = 'Chat__badge'; + }); + if (!foundBadge) { + node.appendChild(badge); + } +}; + +class ChatRenderer { + constructor() { + /** @type {HTMLElement} */ + this.loaded = false; + /** @type {HTMLElement} */ + this.rootNode = null; + this.queue = []; + this.messages = []; + this.visibleMessages = []; + this.page = null; + this.events = new EventEmitter(); + // Scroll handler + /** @type {HTMLElement} */ + this.scrollNode = null; + this.scrollTracking = true; + this.handleScroll = (type) => { + const node = this.scrollNode; + const height = node.scrollHeight; + const bottom = node.scrollTop + node.offsetHeight; + const scrollTracking = + Math.abs(height - bottom) < SCROLL_TRACKING_TOLERANCE; + if (scrollTracking !== this.scrollTracking) { + this.scrollTracking = scrollTracking; + this.events.emit('scrollTrackingChanged', scrollTracking); + logger.debug('tracking', this.scrollTracking); + } + }; + this.ensureScrollTracking = () => { + if (this.scrollTracking) { + this.scrollToBottom(); + } + }; + // Periodic message pruning + setInterval(() => this.pruneMessages(), MESSAGE_PRUNE_INTERVAL); + } + + isReady() { + return this.loaded && this.rootNode && this.page; + } + + mount(node) { + // Mount existing root node on top of the new node + if (this.rootNode) { + node.appendChild(this.rootNode); + } + // Initialize the root node + else { + this.rootNode = node; + } + // Find scrollable parent + this.scrollNode = findNearestScrollableParent(this.rootNode); + this.scrollNode.addEventListener('scroll', this.handleScroll); + setTimeout(() => { + this.scrollToBottom(); + }); + // Flush the queue + this.tryFlushQueue(); + } + + onStateLoaded() { + this.loaded = true; + this.tryFlushQueue(); + } + + tryFlushQueue() { + if (this.isReady() && this.queue.length > 0) { + this.processBatch(this.queue); + this.queue = []; + } + } + + assignStyle(style = {}) { + for (let key of Object.keys(style)) { + this.rootNode.style.setProperty(key, style[key]); + } + } + + setHighlight(highlightSettings, highlightSettingById) { + this.highlightParsers = null; + if (!highlightSettings) { + return; + } + highlightSettings.map((id) => { + const setting = highlightSettingById[id]; + const text = setting.highlightText; + const highlightColor = setting.highlightColor; + const highlightWholeMessage = setting.highlightWholeMessage; + const matchWord = setting.matchWord; + const matchCase = setting.matchCase; + const allowedRegex = /^[a-zа-яё0-9_\-$/^[\s\]\\]+$/gi; + const regexEscapeCharacters = /[!#$%^&*)(+=.<>{}[\]:;'"|~`_\-\\/]/g; + const lines = String(text) + .split(',') + .map((str) => str.trim()) + .filter( + (str) => + // Must be longer than one character + str && + str.length > 1 && + // Must be alphanumeric (with some punctuation) + allowedRegex.test(str) && + // Reset lastIndex so it does not mess up the next word + ((allowedRegex.lastIndex = 0) || true), + ); + let highlightWords; + let highlightRegex; + // Nothing to match, reset highlighting + if (lines.length === 0) { + return; + } + let regexExpressions = []; + // Organize each highlight entry into regex expressions and words + for (let line of lines) { + // Regex expression syntax is /[exp]/ + if (line.charAt(0) === '/' && line.charAt(line.length - 1) === '/') { + const expr = line.substring(1, line.length - 1); + // Check if this is more than one character + if (/^(\[.*\]|\\.|.)$/.test(expr)) { + continue; + } + regexExpressions.push(expr); + } else { + // Lazy init + if (!highlightWords) { + highlightWords = []; + } + // We're not going to let regex characters fuck up our RegEx operation. + line = line.replace(regexEscapeCharacters, '\\$&'); + + highlightWords.push(line); + } + } + const regexStr = regexExpressions.join('|'); + const flags = 'g' + (matchCase ? '' : 'i'); + // We wrap this in a try-catch to ensure that broken regex doesn't break + // the entire chat. + try { + // setting regex overrides matchword + if (regexStr) { + highlightRegex = new RegExp('(' + regexStr + ')', flags); + } else { + const pattern = `${matchWord ? '\\b' : ''}(${highlightWords.join( + '|', + )})${matchWord ? '\\b' : ''}`; + highlightRegex = new RegExp(pattern, flags); + } + } catch { + // We just reset it if it's invalid. + highlightRegex = null; + } + // Lazy init + if (!this.highlightParsers) { + this.highlightParsers = []; + } + this.highlightParsers.push({ + highlightWords, + highlightRegex, + highlightColor, + highlightWholeMessage, + }); + }); + } + + scrollToBottom() { + // scrollHeight is always bigger than scrollTop and is + // automatically clamped to the valid range. + this.scrollNode.scrollTop = this.scrollNode.scrollHeight; + } + + changePage(page) { + if (!this.isReady()) { + this.page = page; + this.tryFlushQueue(); + return; + } + this.page = page; + // Fast clear of the root node + this.rootNode.textContent = ''; + this.visibleMessages = []; + // Re-add message nodes + const fragment = document.createDocumentFragment(); + let node; + for (let message of this.messages) { + if (canPageAcceptType(page, message.type)) { + node = message.node; + fragment.appendChild(node); + this.visibleMessages.push(message); + } + } + if (node) { + this.rootNode.appendChild(fragment); + node.scrollIntoView(); + } + } + + getCombinableMessage(predicate) { + const now = Date.now(); + const len = this.visibleMessages.length; + const from = len - 1; + const to = Math.max(0, len - COMBINE_MAX_MESSAGES); + for (let i = from; i >= to; i--) { + const message = this.visibleMessages[i]; + + const matches = + // Is not an internal message + !message.type.startsWith(MESSAGE_TYPE_INTERNAL) && + // Text payload must fully match + isSameMessage(message, predicate) && + // Must land within the specified time window + now < message.createdAt + COMBINE_MAX_TIME_WINDOW; + if (matches) { + return message; + } + } + return null; + } + + processBatch(batch, options = {}) { + const { prepend, notifyListeners = true } = options; + const now = Date.now(); + // Queue up messages until chat is ready + if (!this.isReady()) { + if (prepend) { + this.queue = [...batch, ...this.queue]; + } else { + this.queue = [...this.queue, ...batch]; + } + return; + } + // Insert messages + const fragment = document.createDocumentFragment(); + const countByType = {}; + let node; + for (let payload of batch) { + const message = createMessage(payload); + // Combine messages + const combinable = this.getCombinableMessage(message); + if (combinable) { + combinable.times = (combinable.times || 1) + 1; + updateMessageBadge(combinable); + continue; + } + // Reuse message node + if (message.node) { + node = message.node; + } + // Reconnected + else if (message.type === 'internal/reconnected') { + node = createReconnectedNode(); + } + // Create message node + else { + node = createMessageNode(); + // Payload is plain text + if (message.text) { + node.textContent = message.text; + } + // Payload is HTML + else if (message.html) { + node.innerHTML = message.html; + } else { + logger.error('Error: message is missing text payload', message); + } + // Get all nodes in this message that want to be rendered like jsx + const nodes = node.querySelectorAll('[data-component]'); + for (let i = 0; i < nodes.length; i++) { + const childNode = nodes[i]; + const targetName = childNode.getAttribute('data-component'); + // Let's pull out the attibute info we need + let outputProps = {}; + for (let j = 0; j < childNode.attributes.length; j++) { + const attribute = childNode.attributes[j]; + + let working_value = attribute.nodeValue; + // We can't do the "if it has no value it's truthy" trick + // Because getAttribute returns "", not null. Hate IE + if (working_value === '$true') { + working_value = true; + } else if (working_value === '$false') { + working_value = false; + } else if (!isNaN(working_value)) { + const parsed_float = parseFloat(working_value); + if (!isNaN(parsed_float)) { + working_value = parsed_float; + } + } + + let canon_name = attribute.nodeName.replace('data-', ''); + // html attributes don't support upper case chars, so we need to map + canon_name = TGUI_CHAT_ATTRIBUTES_TO_PROPS[canon_name]; + outputProps[canon_name] = working_value; + } + const oldHtml = { __html: childNode.innerHTML }; + while (childNode.firstChild) { + childNode.removeChild(childNode.firstChild); + } + const Element = TGUI_CHAT_COMPONENTS[targetName]; + + const reactRoot = createRoot(childNode); + + /* eslint-disable react/no-danger */ + reactRoot.render( + + + , + childNode, + ); + } + + // Highlight text + if (!message.avoidHighlighting && this.highlightParsers) { + this.highlightParsers.map((parser) => { + const highlighted = highlightNode( + node, + parser.highlightRegex, + parser.highlightWords, + (text) => createHighlightNode(text, parser.highlightColor), + ); + if (highlighted && parser.highlightWholeMessage) { + node.className += ' ChatMessage--highlighted'; + } + }); + } + // Linkify text + const linkifyNodes = node.querySelectorAll('.linkify'); + for (let i = 0; i < linkifyNodes.length; ++i) { + linkifyNode(linkifyNodes[i]); + } + // Assign an image error handler + if (now < message.createdAt + IMAGE_RETRY_MESSAGE_AGE) { + const imgNodes = node.querySelectorAll('img'); + for (let i = 0; i < imgNodes.length; i++) { + const imgNode = imgNodes[i]; + imgNode.addEventListener('error', handleImageError); + } + } + } + // Store the node in the message + message.node = node; + // Query all possible selectors to find out the message type + if (!message.type) { + const typeDef = MESSAGE_TYPES.find( + (typeDef) => typeDef.selector && node.querySelector(typeDef.selector), + ); + message.type = typeDef?.type || MESSAGE_TYPE_UNKNOWN; + } + updateMessageBadge(message); + if (!countByType[message.type]) { + countByType[message.type] = 0; + } + countByType[message.type] += 1; + // TODO: Detect duplicates + this.messages.push(message); + if (canPageAcceptType(this.page, message.type)) { + fragment.appendChild(node); + this.visibleMessages.push(message); + } + } + if (node) { + const firstChild = this.rootNode.childNodes[0]; + if (prepend && firstChild) { + this.rootNode.insertBefore(fragment, firstChild); + } else { + this.rootNode.appendChild(fragment); + } + if (this.scrollTracking) { + setTimeout(() => this.scrollToBottom()); + } + } + // Notify listeners that we have processed the batch + if (notifyListeners) { + this.events.emit('batchProcessed', countByType); + } + } + + pruneMessages() { + if (!this.isReady()) { + return; + } + // Delay pruning because user is currently interacting + // with chat history + if (!this.scrollTracking) { + logger.debug('pruning delayed'); + return; + } + // Visible messages + { + const messages = this.visibleMessages; + const fromIndex = Math.max(0, messages.length - MAX_VISIBLE_MESSAGES); + if (fromIndex > 0) { + this.visibleMessages = messages.slice(fromIndex); + for (let i = 0; i < fromIndex; i++) { + const message = messages[i]; + this.rootNode.removeChild(message.node); + // Mark this message as pruned + message.node = 'pruned'; + } + // Remove pruned messages from the message array + + this.messages = this.messages.filter( + (message) => message.node !== 'pruned', + ); + logger.log(`pruned ${fromIndex} visible messages`); + } + } + // All messages + { + const fromIndex = Math.max( + 0, + this.messages.length - MAX_PERSISTED_MESSAGES, + ); + if (fromIndex > 0) { + this.messages = this.messages.slice(fromIndex); + logger.log(`pruned ${fromIndex} stored messages`); + } + } + } + + rebuildChat() { + if (!this.isReady()) { + return; + } + // Make a copy of messages + const fromIndex = Math.max( + 0, + this.messages.length - MAX_PERSISTED_MESSAGES, + ); + const messages = this.messages.slice(fromIndex); + // Remove existing nodes + for (let message of messages) { + message.node = undefined; + } + // Fast clear of the root node + this.rootNode.textContent = ''; + this.messages = []; + this.visibleMessages = []; + // Repopulate the chat log + this.processBatch(messages, { + notifyListeners: false, + }); + } + + /** + * @clearChat + * @copyright 2023 + * @author Cheffie + * @link https://github.com/CheffieGithub + * @license MIT + */ + clearChat() { + const messages = this.visibleMessages; + this.visibleMessages = []; + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + this.rootNode.removeChild(message.node); + // Mark this message as pruned + message.node = 'pruned'; + } + // Remove pruned messages from the message array + this.messages = this.messages.filter( + (message) => message.node !== 'pruned', + ); + logger.log(`Cleared chat`); + } + + saveToDisk() { + // Compile currently loaded stylesheets as CSS text + let cssText = ''; + const styleSheets = document.styleSheets; + for (let i = 0; i < styleSheets.length; i++) { + const cssRules = styleSheets[i].cssRules; + for (let i = 0; i < cssRules.length; i++) { + const rule = cssRules[i]; + if (rule && typeof rule.cssText === 'string') { + cssText += rule.cssText + '\n'; + } + } + } + cssText += 'body, html { background-color: #141414 }\n'; + // Compile chat log as HTML text + let messagesHtml = ''; + for (let message of this.visibleMessages) { + if (message.node) { + messagesHtml += message.node.outerHTML + '\n'; + } + } + // Create a page + + const pageHtml = + '\n' + + '\n' + + '\n' + + 'SS13 Chat Log\n' + + '\n' + + '\n' + + '\n' + + '

    \n' + + messagesHtml + + '
    \n' + + '\n' + + '\n'; + // Create and send a nice blob + const blob = new Blob([pageHtml], { type: 'text/plain' }); + const timestamp = new Date() + .toISOString() + .substring(0, 19) + .replace(/[-:]/g, '') + .replace('T', '-'); + Byond.saveBlob(blob, `ss13-chatlog-${timestamp}.html`, '.html'); + } +} + +// Make chat renderer global so that we can continue using the same +// instance after hot code replacement. +if (!window.__chatRenderer__) { + window.__chatRenderer__ = new ChatRenderer(); +} + +/** @type {ChatRenderer} */ +export const chatRenderer = window.__chatRenderer__; diff --git a/tgui/packages/tgui-panel/index.js b/tgui/packages/tgui-panel/index.js index 6bc6b32c4622..297ecd85852c 100644 --- a/tgui/packages/tgui-panel/index.js +++ b/tgui/packages/tgui-panel/index.js @@ -75,14 +75,8 @@ const setupApp = () => { Byond.subscribe((type, payload) => store.dispatch({ type, payload })); // Unhide the panel - Byond.winset('output', { - 'is-visible': false, - }); - Byond.winset('browseroutput', { - 'is-visible': true, - 'is-disabled': false, - 'pos': '0x0', - 'size': '0x0', + Byond.winset('legacy_output_selector', { + left: 'output_browser', }); // Resize the panel to match the non-browser output diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss index d463b780a028..dc5cd0d34894 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss @@ -19,6 +19,7 @@ img { img.icon { height: 1em; min-height: 16px; + min-width: 16px; width: auto; vertical-align: bottom; } diff --git a/tgui/packages/tgui-say/index.tsx b/tgui/packages/tgui-say/index.tsx index ed512b525ff5..f7b65f610668 100644 --- a/tgui/packages/tgui-say/index.tsx +++ b/tgui/packages/tgui-say/index.tsx @@ -1,19 +1,15 @@ -import './styles/main.scss'; import { createRenderer } from 'tgui/renderer'; import { TguiSay } from './TguiSay'; -const renderApp = createRenderer(() => { - return ; -}); +let reactRoot: Root | null = null; -const setupApp = () => { - // Delay setup - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', setupApp); - return; +document.onreadystatechange = function () { + if (document.readyState !== 'complete') return; + + if (!reactRoot) { + const root = document.getElementById('react-root'); + reactRoot = createRoot(root!); } - renderApp(); + reactRoot.render(); }; - -setupApp(); diff --git a/tgui/packages/tgui-say/package.json b/tgui/packages/tgui-say/package.json index 5ee9e432606f..31ef0a759c82 100644 --- a/tgui/packages/tgui-say/package.json +++ b/tgui/packages/tgui-say/package.json @@ -3,8 +3,10 @@ "name": "tgui-say", "version": "1.0.0", "dependencies": { + "@types/react-dom": "^18.2.24", "common": "workspace:*", "inferno": "^7.4.8", + "react-dom": "^18.2.0", "tgui": "workspace:*", "tgui-polyfill": "workspace:*" } diff --git a/tgui/packages/tgui/backend.ts b/tgui/packages/tgui/backend.ts index 79dc7dd6ac8f..5535198233f1 100644 --- a/tgui/packages/tgui/backend.ts +++ b/tgui/packages/tgui/backend.ts @@ -21,9 +21,25 @@ import { resumeRenderer, suspendRenderer } from './renderer'; const logger = createLogger('backend'); +export let globalStore; + +export const setGlobalStore = (store) => { + globalStore = store; +}; + export const backendUpdate = createAction('backend/update'); export const backendSetSharedState = createAction('backend/setSharedState'); export const backendSuspendStart = createAction('backend/suspendStart'); +export const backendCreatePayloadQueue = createAction( + 'backend/createPayloadQueue' +); +export const backendDequeuePayloadQueue = createAction( + 'backend/dequeuePayloadQueue' +); +export const backendRemovePayloadQueue = createAction( + 'backend/removePayloadQueue' +); +export const nextPayloadChunk = createAction('nextPayloadChunk'); export const backendSuspendSuccess = () => ({ type: 'backend/suspendSuccess', @@ -36,6 +52,7 @@ const initialState = { config: {}, data: {}, shared: {}, + outgoingPayloadQueues: {} as Record, // Start as suspended suspended: Date.now(), suspending: false, @@ -112,6 +129,44 @@ export const backendReducer = (state = initialState, action) => { }; } + if (type === 'backend/createPayloadQueue') { + const { id, chunks } = payload; + const { outgoingPayloadQueues } = state; + return { + ...state, + outgoingPayloadQueues: { + ...outgoingPayloadQueues, + [id]: chunks, + }, + }; + } + + if (type === 'backend/dequeuePayloadQueue') { + const { id } = payload; + const { outgoingPayloadQueues } = state; + const { [id]: targetQueue, ...otherQueues } = outgoingPayloadQueues; + const [_, ...rest] = targetQueue; + return { + ...state, + outgoingPayloadQueues: rest.length + ? { + ...otherQueues, + [id]: rest, + } + : otherQueues, + }; + } + + if (type === 'backend/removePayloadQueue') { + const { id } = payload; + const { outgoingPayloadQueues } = state; + const { [id]: _, ...otherQueues } = outgoingPayloadQueues; + return { + ...state, + outgoingPayloadQueues: otherQueues, + }; + } + return state; }; @@ -120,7 +175,9 @@ export const backendMiddleware = (store) => { let suspendInterval; return (next) => (action) => { - const { suspended } = selectBackend(store.getState()); + const { suspended, outgoingPayloadQueues } = selectBackend( + store.getState() + ); const { type, payload } = action; if (type === 'update') { @@ -195,10 +252,9 @@ export const backendMiddleware = (store) => { setTimeout(() => { perf.mark('resume/start'); // Doublecheck if we are not re-suspended. - const { suspended } = selectBackend(store.getState()); - if (suspended) { - return; - } + const { suspended, outgoingPayloadQueues } = selectBackend( + store.getState() + ); Byond.winset(Byond.windowId, { 'is-visible': true, }); @@ -212,10 +268,86 @@ export const backendMiddleware = (store) => { }); } + if (type === 'oversizePayloadResponse') { + const { allow } = payload; + if (allow) { + store.dispatch(nextPayloadChunk(payload)); + } else { + store.dispatch(backendRemovePayloadQueue(payload)); + } + } + + if (type === 'acknowlegePayloadChunk') { + store.dispatch(backendDequeuePayloadQueue(payload)); + store.dispatch(nextPayloadChunk(payload)); + } + + if (type === 'nextPayloadChunk') { + const { id } = payload; + const chunk = outgoingPayloadQueues[id][0]; + Byond.sendMessage('payloadChunk', { + id, + chunk, + }); + } + return next(action); }; }; +const encodedLengthBinarySearch = (haystack: string[], length: number) => { + const haystackLength = haystack.length; + let high = haystackLength - 1; + let low = 0; + let mid = 0; + while (low < high) { + mid = Math.round((low + high) / 2); + const substringLength = encodeURIComponent( + haystack.slice(0, mid).join('') + ).length; + if (substringLength === length) { + break; + } + if (substringLength < length) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return mid; +}; + +const chunkSplitter = { + [Symbol.split]: (string: string) => { + const charSeq = string[Symbol.iterator]().toArray(); + const length = charSeq.length; + let chunks: string[] = []; + let startIndex = 0; + let endIndex = 1024; + while (startIndex < length) { + const cut = charSeq.slice( + startIndex, + endIndex < length ? endIndex : undefined + ); + const cutString = cut.join(''); + if (encodeURIComponent(cutString).length > 1024) { + const splitIndex = startIndex + encodedLengthBinarySearch(cut, 1024); + chunks.push( + charSeq + .slice(startIndex, splitIndex < length ? splitIndex : undefined) + .join('') + ); + startIndex = splitIndex; + } else { + chunks.push(cutString); + startIndex = endIndex; + } + endIndex = startIndex + 1024; + } + return chunks; + }, +}; + /** * Sends an action to `ui_act` on `src_object` that this tgui window * is associated with. @@ -230,6 +362,33 @@ export const sendAct = (action: string, payload: object = {}) => { logger.error(`Payload for act() must be an object, got this:`, payload); return; } + if (!Byond.TRIDENT) { + const stringifiedPayload = JSON.stringify(payload); + const urlSize = Object.entries({ + type: 'act/' + action, + payload: stringifiedPayload, + tgui: 1, + windowId: Byond.windowId, + }).reduce( + (url, [key, value], i) => + url + + `${i > 0 ? '&' : '?'}${encodeURIComponent(key)}=${encodeURIComponent( + value + )}`, + '' + ).length; + if (urlSize > 2048) { + let chunks: string[] = stringifiedPayload.split(chunkSplitter); + const id = `${Date.now()}`; + globalStore?.dispatch(backendCreatePayloadQueue({ id, chunks })); + Byond.sendMessage('oversizedPayloadRequest', { + type: 'act/' + action, + id, + chunkCount: chunks.length, + }); + return; + } + } Byond.sendMessage('act/' + action, payload); }; @@ -257,6 +416,7 @@ type BackendState = { }; data: TData; shared: Record; + outgoingPayloadQueues: Record; suspending: boolean; suspended: boolean; }; diff --git a/tgui/packages/tgui/layouts/Layout.js b/tgui/packages/tgui/layouts/Layout.js index 40c25bfc3839..563756758e65 100644 --- a/tgui/packages/tgui/layouts/Layout.js +++ b/tgui/packages/tgui/layouts/Layout.js @@ -10,6 +10,7 @@ import { addScrollableNode, removeScrollableNode } from '../events'; export const Layout = (props) => { const { className, theme = 'nanotrasen', children, ...rest } = props; + document.documentElement.className = `theme-${theme}`; return (
    , }; -const Story = (props, context) => { - const [code, setCode] = useLocalState( - context, - 'byondUiEvalCode', - `Byond.winset('${Byond.windowId}', {\n 'is-visible': true,\n})` +const Story = (props) => { + const [code, setCode] = useState( + `Byond.winset('${Byond.windowId}', {\n 'is-visible': true,\n})`, ); return ( <> @@ -66,3 +65,5 @@ const Story = (props, context) => { ); }; + 1 change: 1 addition & 0 deletions1 +tgui/packages/tgui/styles/interfaces/IntegratedCircuit.scss diff --git a/tgui/packages/tgui/styles/interfaces/IntegratedCircuit.scss b/tgui/packages/tgui/styles/interfaces/IntegratedCircuit.scss index 7cfecb250acc..2c39ce04ad4d 100644 --- a/tgui/packages/tgui/styles/interfaces/IntegratedCircuit.scss +++ b/tgui/packages/tgui/styles/interfaces/IntegratedCircuit.scss @@ -1,3 +1,4 @@ +@use 'sass:map'; @use '../colors.scss'; @use '../base.scss'; diff --git a/tgui/packages/tgui/styles/layouts/Layout.scss b/tgui/packages/tgui/styles/layouts/Layout.scss index eaadcd9a6978..a6c01b73bd86 100644 --- a/tgui/packages/tgui/styles/layouts/Layout.scss +++ b/tgui/packages/tgui/styles/layouts/Layout.scss @@ -5,12 +5,28 @@ @use 'sass:color'; @use '../base.scss'; +@use '../functions.scss' as *; +$luminance: luminance(base.$color-bg); $scrollbar-color-multiplier: 1 !default; +$scrollbar-base: color.scale( + base.$color-bg, + $lightness: -33% * $scrollbar-color-multiplier +); +$scrollbar-face: color.scale( + base.$color-bg, + $lightness: if($luminance > 0.05, 30%, 10%) * $scrollbar-color-multiplier +); +// Fancy scrollbar +html, +body { + scrollbar-color: $scrollbar-face $scrollbar-base; +} + +// Remove with 516, IE legacy code .Layout, .Layout * { - // Fancy scrollbar scrollbar-base-color: color.scale( base.$color-bg, $lightness: -25% * $scrollbar-color-multiplier diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index d0fea4281710..f026597c9574 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -89,7 +89,7 @@ // NT Theme .Layout__content { background-image: url('../assets/bg-shiptest.svg'); - background-size: 70%; + background-size: 70% 70%; background-position: center; background-repeat: no-repeat; } diff --git a/tgui/yarn.lock b/tgui/yarn.lock index b5219c532e60..539639dd9485 100644 --- a/tgui/yarn.lock +++ b/tgui/yarn.lock @@ -2187,6 +2187,32 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.14 + resolution: "@types/prop-types@npm:15.7.14" + checksum: d0c5407b9ccc3dd5fae0ccf9b1007e7622ba5e6f1c18399b4f24dff33619d469da4b9fa918a374f19dc0d9fe6a013362aab0b844b606cfc10676efba3f5f736d + languageName: node + linkType: hard + +"@types/react-dom@npm:^18.2.24": + version: 18.2.24 + resolution: "@types/react-dom@npm:18.2.24" + dependencies: + "@types/react": "npm:*" + checksum: 7fb0dd0c88c5219bcc27f1f9bf14d9e1b1593014ee7938dd46ee9782c77c39d1ccc79d2b8364a6113019d7f65c94e4dc3c37425c3972910bb2674cce98d3f3ca + languageName: node + linkType: hard + +"@types/react@npm:*": + version: 18.2.74 + resolution: "@types/react@npm:18.2.74" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.0.2" + checksum: 093c0e350552e61393e2ba30169aa620e2e64c1e2d0ff38efd2a7549ded689b6ab6bffb65fe0f7ef9e143174de54442d942bd70c014649f464c52465701208d8 + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.0 resolution: "@types/stack-utils@npm:2.0.0" @@ -3835,6 +3861,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.0.2": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 + languageName: node + linkType: hard + "cubic2quad@npm:^1.0.0": version: 1.1.1 resolution: "cubic2quad@npm:1.1.1" @@ -6725,7 +6758,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -7848,6 +7881,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^18.2.0": + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.2 + peerDependencies: + react: ^18.3.1 + checksum: 298954ecd8f78288dcaece05e88b570014d8f6dce5db6f66e6ee91448debeb59dcd31561dddb354eee47e6c1bb234669459060deb238ed0213497146e555a0b9 + languageName: node + linkType: hard + "react-is@npm:^16.8.1": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -8245,6 +8290,15 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"scheduler@npm:^0.23.2": + version: 0.23.2 + resolution: "scheduler@npm:0.23.2" + dependencies: + loose-envify: ^1.1.0 + checksum: 3e82d1f419e240ef6219d794ff29c7ee415fbdc19e038f680a10c067108e06284f1847450a210b29bbaf97b9d8a97ced5f624c31c681248ac84c80d56ad5a2c4 + languageName: node + linkType: hard + "schema-utils@npm:^2.6.5": version: 2.7.1 resolution: "schema-utils@npm:2.7.1" @@ -9098,8 +9152,10 @@ resolve@^2.0.0-next.3: version: 0.0.0-use.local resolution: "tgui-say@workspace:packages/tgui-say" dependencies: + "@types/react-dom": ^18.2.24 common: "workspace:*" inferno: ^7.4.8 + react-dom: ^18.2.0 tgui: "workspace:*" tgui-polyfill: "workspace:*" languageName: unknown diff --git a/tools/build/build.js b/tools/build/build.js index 65a4dea5b63b..d1a9666cb05e 100755 --- a/tools/build/build.js +++ b/tools/build/build.js @@ -90,6 +90,7 @@ export const DmTarget = new Juke.Target({ "icons/**", "interface/**", "mod_celadon/**", // CELADON + "tgui/public/tgui.html", `${DME_NAME}.dme`, NamedVersionFile, ], diff --git a/tools/ci/check_grep.sh b/tools/ci/check_grep.sh index ccc11b81317e..432ee8948674 100755 --- a/tools/ci/check_grep.sh +++ b/tools/ci/check_grep.sh @@ -127,6 +127,14 @@ fi; # st=1 #fi; +section "516 Href Styles" +part "byond href styles" +if $grep "href[\s='\"\\\\]*\?" $code_files ; then + echo + echo -e "${RED}ERROR: BYOND requires internal href links to begin with \"byond://\".${NC}" + st=1 +fi; + section "common mistakes" part "global vars" if $grep '^/*var/' $code_files; then