From 72cb4fb920141c4b03daf3c74103090e368733e6 Mon Sep 17 00:00:00 2001 From: kappa-sama <44128284+kappa-sama@users.noreply.github.com> Date: Tue, 28 Jul 2020 21:56:39 -0400 Subject: [PATCH 001/136] honk --- code/modules/mining/lavaland/necropolis_chests.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index bfdb731869..02df00f1c4 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -79,9 +79,9 @@ new /obj/item/clothing/neck/necklace/memento_mori(src) if(29) if(prob(50)) - new /obj/item/malf_upgrade(src) + new /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom(loc) else - new /obj/item/disk/tech_disk/illegal(src) + new /mob/living/simple_animal/hostile/retaliate/clown/clownhulk(loc) //KA modkit design discs /obj/item/disk/design_disk/modkit_disc From cc34c4b1a204af4b959ff6141456674c4db634b3 Mon Sep 17 00:00:00 2001 From: kappa-sama <44128284+kappa-sama@users.noreply.github.com> Date: Fri, 31 Jul 2020 17:09:54 -0400 Subject: [PATCH 002/136] no empty slot :pensive: --- code/modules/mining/lavaland/necropolis_chests.dm | 3 --- 1 file changed, 3 deletions(-) diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index 02df00f1c4..2b4a3902ff 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -78,10 +78,7 @@ if(28) new /obj/item/clothing/neck/necklace/memento_mori(src) if(29) - if(prob(50)) new /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom(loc) - else - new /mob/living/simple_animal/hostile/retaliate/clown/clownhulk(loc) //KA modkit design discs /obj/item/disk/design_disk/modkit_disc From adf4a8a2e3beafa738aae1478df455e02e3b9f20 Mon Sep 17 00:00:00 2001 From: kappa-sama <44128284+kappa-sama@users.noreply.github.com> Date: Fri, 31 Jul 2020 17:10:06 -0400 Subject: [PATCH 003/136] indent --- code/modules/mining/lavaland/necropolis_chests.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index 2b4a3902ff..ce9b1b239e 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -78,7 +78,7 @@ if(28) new /obj/item/clothing/neck/necklace/memento_mori(src) if(29) - new /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom(loc) + new /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom(loc) //KA modkit design discs /obj/item/disk/design_disk/modkit_disc From 94ee8bec08212cbcd38628cfc3961c36ddc31e0f Mon Sep 17 00:00:00 2001 From: kappa-sama <44128284+kappa-sama@users.noreply.github.com> Date: Sat, 1 Aug 2020 19:19:59 -0400 Subject: [PATCH 004/136] ok --- code/modules/mining/lavaland/necropolis_chests.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm index ce9b1b239e..1a908cade3 100644 --- a/code/modules/mining/lavaland/necropolis_chests.dm +++ b/code/modules/mining/lavaland/necropolis_chests.dm @@ -78,7 +78,7 @@ if(28) new /obj/item/clothing/neck/necklace/memento_mori(src) if(29) - new /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom(loc) + new /obj/item/disk/tech_disk/illegal(src) //KA modkit design discs /obj/item/disk/design_disk/modkit_disc From 97438eb9b8f42be0f042004fe1aa3209048b84ac Mon Sep 17 00:00:00 2001 From: zerothebigboy Date: Tue, 4 Aug 2020 05:53:28 -0400 Subject: [PATCH 005/136] a --- code/modules/antagonists/traitor/classes/freeform.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/antagonists/traitor/classes/freeform.dm b/code/modules/antagonists/traitor/classes/freeform.dm index 998373a77c..05551433fd 100644 --- a/code/modules/antagonists/traitor/classes/freeform.dm +++ b/code/modules/antagonists/traitor/classes/freeform.dm @@ -1,7 +1,7 @@ /datum/traitor_class/human/freeform name = "Waffle Co Agent" employer = "Waffle Company" - weight = 16 + weight = 0 // should not spawn in unless admins bus something in the traitor panel with setting traitor classe chaos = 0 /datum/traitor_class/human/freeform/forge_objectives(datum/antagonist/traitor/T) From 25d2b1a3f10b244b7255fa1ff5cd1cd5cd25c38c Mon Sep 17 00:00:00 2001 From: zerothebigboy Date: Tue, 4 Aug 2020 05:53:36 -0400 Subject: [PATCH 006/136] a --- code/modules/antagonists/traitor/classes/freeform.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/antagonists/traitor/classes/freeform.dm b/code/modules/antagonists/traitor/classes/freeform.dm index 05551433fd..1ca385ef19 100644 --- a/code/modules/antagonists/traitor/classes/freeform.dm +++ b/code/modules/antagonists/traitor/classes/freeform.dm @@ -1,7 +1,7 @@ /datum/traitor_class/human/freeform name = "Waffle Co Agent" employer = "Waffle Company" - weight = 0 // should not spawn in unless admins bus something in the traitor panel with setting traitor classe + weight = 0 // should not spawn in unless admins bus something in the traitor panel with setting traitor classes chaos = 0 /datum/traitor_class/human/freeform/forge_objectives(datum/antagonist/traitor/T) From b44a329db1fd7522a7ade2a159e4a0549977f291 Mon Sep 17 00:00:00 2001 From: zerothebigboy Date: Tue, 4 Aug 2020 07:25:18 -0400 Subject: [PATCH 007/136] a --- code/modules/antagonists/traitor/classes/hijack.dm | 2 +- code/modules/antagonists/traitor/classes/martyr.dm | 2 +- code/modules/antagonists/traitor/classes/subterfuge.dm | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/modules/antagonists/traitor/classes/hijack.dm b/code/modules/antagonists/traitor/classes/hijack.dm index bbfb0cb11d..2f0154e369 100644 --- a/code/modules/antagonists/traitor/classes/hijack.dm +++ b/code/modules/antagonists/traitor/classes/hijack.dm @@ -4,7 +4,7 @@ weight = 3 chaos = 5 threat = 3 - min_players = 25 + min_players = 0 uplink_filters = list(/datum/uplink_item/stealthy_weapons/romerol_kit) /datum/traitor_class/human/hijack/forge_objectives(datum/antagonist/traitor/T) diff --git a/code/modules/antagonists/traitor/classes/martyr.dm b/code/modules/antagonists/traitor/classes/martyr.dm index 5c407b70fd..94cfde4f45 100644 --- a/code/modules/antagonists/traitor/classes/martyr.dm +++ b/code/modules/antagonists/traitor/classes/martyr.dm @@ -4,7 +4,7 @@ weight = 2 chaos = 5 threat = 5 - min_players = 20 + min_players = 0 uplink_filters = list(/datum/uplink_item/stealthy_weapons/romerol_kit,/datum/uplink_item/bundles_TC/contract_kit) /datum/traitor_class/human/martyr/forge_objectives(datum/antagonist/traitor/T) diff --git a/code/modules/antagonists/traitor/classes/subterfuge.dm b/code/modules/antagonists/traitor/classes/subterfuge.dm index ae43f11fe7..aced32d0c9 100644 --- a/code/modules/antagonists/traitor/classes/subterfuge.dm +++ b/code/modules/antagonists/traitor/classes/subterfuge.dm @@ -1,7 +1,7 @@ /datum/traitor_class/human/subterfuge name = "MI13 Operative" employer = "MI13" - weight = 20 + weight = 40 chaos = -5 /datum/traitor_class/human/subterfuge/forge_single_objective(datum/antagonist/traitor/T) From 3b34c75b37b79d0cb9cc4c1d2bc7f373210d9867 Mon Sep 17 00:00:00 2001 From: zerothebigboy Date: Tue, 4 Aug 2020 07:25:30 -0400 Subject: [PATCH 008/136] a --- code/modules/antagonists/traitor/classes/hijack.dm | 2 +- code/modules/antagonists/traitor/classes/martyr.dm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/antagonists/traitor/classes/hijack.dm b/code/modules/antagonists/traitor/classes/hijack.dm index 2f0154e369..bbfb0cb11d 100644 --- a/code/modules/antagonists/traitor/classes/hijack.dm +++ b/code/modules/antagonists/traitor/classes/hijack.dm @@ -4,7 +4,7 @@ weight = 3 chaos = 5 threat = 3 - min_players = 0 + min_players = 25 uplink_filters = list(/datum/uplink_item/stealthy_weapons/romerol_kit) /datum/traitor_class/human/hijack/forge_objectives(datum/antagonist/traitor/T) diff --git a/code/modules/antagonists/traitor/classes/martyr.dm b/code/modules/antagonists/traitor/classes/martyr.dm index 94cfde4f45..5c407b70fd 100644 --- a/code/modules/antagonists/traitor/classes/martyr.dm +++ b/code/modules/antagonists/traitor/classes/martyr.dm @@ -4,7 +4,7 @@ weight = 2 chaos = 5 threat = 5 - min_players = 0 + min_players = 20 uplink_filters = list(/datum/uplink_item/stealthy_weapons/romerol_kit,/datum/uplink_item/bundles_TC/contract_kit) /datum/traitor_class/human/martyr/forge_objectives(datum/antagonist/traitor/T) From ff5efc766086ea8ccc7d0bca722a1853112162b6 Mon Sep 17 00:00:00 2001 From: zerothebigboy Date: Tue, 4 Aug 2020 20:48:08 -0400 Subject: [PATCH 009/136] a --- code/modules/antagonists/traitor/classes/subterfuge.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/antagonists/traitor/classes/subterfuge.dm b/code/modules/antagonists/traitor/classes/subterfuge.dm index aced32d0c9..0d4060fb7b 100644 --- a/code/modules/antagonists/traitor/classes/subterfuge.dm +++ b/code/modules/antagonists/traitor/classes/subterfuge.dm @@ -1,7 +1,7 @@ /datum/traitor_class/human/subterfuge name = "MI13 Operative" employer = "MI13" - weight = 40 + weight = 3 chaos = -5 /datum/traitor_class/human/subterfuge/forge_single_objective(datum/antagonist/traitor/T) From 1b367994b132054d3061e9038a8db2f5e3450fe2 Mon Sep 17 00:00:00 2001 From: zerothebigboy Date: Tue, 4 Aug 2020 20:48:17 -0400 Subject: [PATCH 010/136] a --- code/modules/antagonists/traitor/classes/subterfuge.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/antagonists/traitor/classes/subterfuge.dm b/code/modules/antagonists/traitor/classes/subterfuge.dm index 0d4060fb7b..ad049f3264 100644 --- a/code/modules/antagonists/traitor/classes/subterfuge.dm +++ b/code/modules/antagonists/traitor/classes/subterfuge.dm @@ -1,7 +1,7 @@ /datum/traitor_class/human/subterfuge name = "MI13 Operative" employer = "MI13" - weight = 3 + weight = 36 chaos = -5 /datum/traitor_class/human/subterfuge/forge_single_objective(datum/antagonist/traitor/T) From 18e2f922855985e850a2b270220b7feec3792fc2 Mon Sep 17 00:00:00 2001 From: Changelogs Date: Mon, 10 Aug 2020 00:42:25 +0000 Subject: [PATCH 011/136] Automatic changelog compile [ci skip] --- html/changelog.html | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index b87a49f7f2..2140b4b7aa 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -1159,30 +1159,6 @@
  • plantpeople should stop dying in the halls now
  • Modifier-independent hotkey bindings have been added.
  • - -

    08 June 2020

    -

    DeltaFire15 updated:

    -
      -
    • Delinging now properly removes their special role
    • -
    • Keycard auth devices now require two seperate IDs with sufficient access to auth.
    • -
    -

    Linzolle updated:

    -
      -
    • shotguns no longer delete chambered shells while firing
    • -
    -

    kevinz000 updated:

    -
      -
    • test
    • -
    -

    shellspeed1 updated:

    -
      -
    • Internal tanks are now printable at the engineering lathe.
    • -
    -

    timothyteakettle updated:

    -
      -
    • newly created areas using blueprints now maintain the previous areas noteleport value
    • -
    • kudzu seeds now actually spawn vines
    • -
    GoonStation 13 Development Team From 2d7746aedf549c34672021f6c8c2db13a402ae3e Mon Sep 17 00:00:00 2001 From: Changelogs Date: Tue, 11 Aug 2020 00:41:37 +0000 Subject: [PATCH 012/136] Automatic changelog compile [ci skip] --- html/changelog.html | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index 2140b4b7aa..0b2babbebc 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -1134,31 +1134,6 @@
  • Mice can now breed using cheese wedges
  • Royal cheese can be crafted to convert a mouse into king rat
  • - -

    09 June 2020

    -

    Anonymous updated:

    -
      -
    • Added Orville-inspired clothing as a worthy alternative to Trek stuff.
    • -
    • Adds chaplain role allowance to the TMP Service Uniform loadout.
    • -
    • Adds paramedic in every medsci mentioned uniform loadout.
    • -
    -

    DeltaFire15 updated:

    -
      -
    • Offstation AIs can once again only interact with their z-level
    • -
    -

    Ghommie updated:

    -
      -
    • Fixing IC material containers interaction with stacks, for real.
    • -
    -

    Trilbyspaceclone updated:

    -
      -
    • Gasses like BZ and Masiam seem to just sell for less in cargo, markets seem to change it seems
    • -
    -

    kevinz000 updated:

    -
      -
    • plantpeople should stop dying in the halls now
    • -
    • Modifier-independent hotkey bindings have been added.
    • -
    GoonStation 13 Development Team From cad7fee64564209b9dd28ade2eb8a564845b8f74 Mon Sep 17 00:00:00 2001 From: Chiirno Date: Tue, 11 Aug 2020 07:45:14 -0300 Subject: [PATCH 013/136] Moved dice from pill_bottles to boxes on cogStation --- _maps/map_files/CogStation/CogStation.dmm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_maps/map_files/CogStation/CogStation.dmm b/_maps/map_files/CogStation/CogStation.dmm index 182f1a4f27..19fc31a406 100644 --- a/_maps/map_files/CogStation/CogStation.dmm +++ b/_maps/map_files/CogStation/CogStation.dmm @@ -8607,7 +8607,7 @@ /obj/effect/turf_decal/tile/bar{ dir = 1 }, -/obj/item/storage/pill_bottle/dice, +/obj/item/storage/box/dice, /obj/item/toy/cards/deck, /turf/open/floor/plasteel, /area/crew_quarters/bar) @@ -16804,7 +16804,7 @@ /area/bridge) "aKw" = ( /obj/structure/table, -/obj/item/storage/pill_bottle/dice, +/obj/item/storage/box/dice, /obj/item/toy/cards/deck, /obj/structure/disposalpipe/segment{ dir = 4 @@ -31910,7 +31910,7 @@ /obj/effect/turf_decal/tile/green{ dir = 4 }, -/obj/item/storage/pill_bottle/dice, +/obj/item/storage/box/dice, /obj/item/toy/cards/deck, /turf/open/floor/plasteel/white/corner{ dir = 1 From a26bae29023dea7da23c1c35bf1799f96322aa9e Mon Sep 17 00:00:00 2001 From: Changelogs Date: Wed, 12 Aug 2020 00:41:57 +0000 Subject: [PATCH 014/136] Automatic changelog compile [ci skip] --- html/changelog.html | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index 0b2babbebc..c847a909a8 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -1102,38 +1102,6 @@
    • Balanced vending machine prices to be generally more affordable, a minority has been priced up though.
    - -

    10 June 2020

    -

    DeltaFire15 updated:

    -
      -
    • Golems / simillar now inherit the neutered antag datum if the creator is neutered.
    • -
    -

    Ghommie updated:

    -
      -
    • Fixing missing pill type buttons from the chem master UI.
    • -
    -

    Naksu updated:

    -
      -
    • Lighting corner updates are ever so slightly faster.
    • -
    -

    Putnam for helping me code the contamination clearing on people updated:

    -
      -
    • Lab made Zeolites have been remade anew and more affective now that they refined the best possable way to mix and make a supper Zeolite capable of clearing contamination form not only people but items!
    • -
    -

    Trilbyspaceclone updated:

    -
      -
    • Tank Dispender has been moved into toxin storage from toxins
    • -
    -

    kevinz000 updated:

    -
      -
    • traitor classes can now be poplocked. hijack/glorious death are now locked to 25/20 respectively.
    • -
    -

    timothyteakettle updated:

    -
      -
    • adds a new fermichem, used for creating sentient plushies!
    • -
    • Mice can now breed using cheese wedges
    • -
    • Royal cheese can be crafted to convert a mouse into king rat
    • -
    GoonStation 13 Development Team From 538624ea0967ec8268907186ded70963699d9a43 Mon Sep 17 00:00:00 2001 From: Changelogs Date: Thu, 13 Aug 2020 00:41:40 +0000 Subject: [PATCH 015/136] Automatic changelog compile [ci skip] --- html/changelog.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index c847a909a8..716cb2ff30 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -1096,12 +1096,6 @@
    • Crabs, cockroaches, slimes and crabs are now small enough to fit in pet carriers
    - -

    11 June 2020

    -

    Ghommie updated:

    -
      -
    • Balanced vending machine prices to be generally more affordable, a minority has been priced up though.
    • -
    GoonStation 13 Development Team From e660a0d065437d1394b7e62f24845ed043c8e4ce Mon Sep 17 00:00:00 2001 From: Changelogs Date: Fri, 14 Aug 2020 00:42:22 +0000 Subject: [PATCH 016/136] Automatic changelog compile [ci skip] --- html/changelog.html | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/html/changelog.html b/html/changelog.html index 716cb2ff30..f7630b2ef1 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -1083,19 +1083,6 @@
  • Parrying has been added, keybound to F. Timing-based counterattacks, effect heavily dependent on the item, WIP.
  • Disks are now smaller.
  • - -

    12 June 2020

    -

    EmeraldSundisk updated:

    -
      -
    • The Detective's Office has been commandeered in order to serve a Head of Security. Detectives will find their new office within starboard maintenance.
    • -
    • Adds the Head of Security's standard equipment to their new office.
    • -
    • Slight adjustment/expansion to the main security department
    • -
    • Additional maintenance work
    • -
    -

    timothyteakettle updated:

    -
      -
    • Crabs, cockroaches, slimes and crabs are now small enough to fit in pet carriers
    • -
    GoonStation 13 Development Team From 15e659fc9cb7b863303fe4cd1d1c100d507df29c Mon Sep 17 00:00:00 2001 From: Letter N <24603524+LetterN@users.noreply.github.com> Date: Sat, 15 Aug 2020 16:08:10 +0800 Subject: [PATCH 017/136] tguichat and tgui4.1? --- tgui/.eslintrc.yml | 3 +- tgui/.gitattributes | 1 + tgui/bin/tgui | 2 +- tgui/docs/component-reference.md | 28 +- tgui/docs/converting-old-tgui-interfaces.md | 17 +- tgui/package.json | 2 +- tgui/packages/common/color.js | 62 ++ tgui/packages/common/events.js | 42 + tgui/packages/common/keycodes.js | 86 ++ tgui/packages/common/package.json | 2 +- tgui/packages/common/perf.js | 40 +- tgui/packages/common/react.js | 10 +- tgui/packages/common/redux.js | 59 +- tgui/packages/common/storage.js | 193 +++- tgui/packages/common/timer.js | 9 + tgui/packages/common/uuid.js | 19 + tgui/packages/tgui-dev-server/dreamseeker.js | 87 ++ tgui/packages/tgui-dev-server/index.js | 3 + tgui/packages/tgui-dev-server/link/client.js | 15 +- tgui/packages/tgui-dev-server/link/server.js | 202 ++-- tgui/packages/tgui-dev-server/package.json | 3 +- tgui/packages/tgui-dev-server/reloader.js | 32 +- tgui/packages/tgui-dev-server/webpack.js | 9 +- tgui/packages/tgui-panel/Notifications.js | 41 + tgui/packages/tgui-panel/Panel.js | 142 +++ .../tgui-panel/audio/NowPlayingWidget.js | 69 ++ tgui/packages/tgui-panel/audio/hooks.js | 17 + tgui/packages/tgui-panel/audio/index.js | 10 + tgui/packages/tgui-panel/audio/middleware.js | 37 + tgui/packages/tgui-panel/audio/player.js | 117 +++ tgui/packages/tgui-panel/audio/reducer.js | 50 + tgui/packages/tgui-panel/audio/selectors.js | 7 + .../tgui-panel/chat/ChatPageSettings.js | 75 ++ tgui/packages/tgui-panel/chat/ChatPanel.js | 71 ++ tgui/packages/tgui-panel/chat/ChatTabs.js | 61 ++ tgui/packages/tgui-panel/chat/actions.js | 21 + tgui/packages/tgui-panel/chat/constants.js | 123 +++ tgui/packages/tgui-panel/chat/index.js | 11 + tgui/packages/tgui-panel/chat/middleware.js | 114 ++ tgui/packages/tgui-panel/chat/model.js | 45 + tgui/packages/tgui-panel/chat/reducer.js | 168 +++ tgui/packages/tgui-panel/chat/renderer.js | 475 +++++++++ .../tgui-panel/chat/replaceInTextNode.js | 128 +++ tgui/packages/tgui-panel/chat/selectors.js | 21 + tgui/packages/tgui-panel/game/actions.js | 11 + tgui/packages/tgui-panel/game/constants.js | 7 + tgui/packages/tgui-panel/game/hooks.js | 12 + tgui/packages/tgui-panel/game/index.js | 9 + tgui/packages/tgui-panel/game/middleware.js | 48 + tgui/packages/tgui-panel/game/reducer.js | 39 + tgui/packages/tgui-panel/game/selectors.js | 7 + tgui/packages/tgui-panel/index.js | 127 +++ tgui/packages/tgui-panel/package.json | 6 + tgui/packages/tgui-panel/panelFocus.js | 47 + .../packages/tgui-panel/ping/PingIndicator.js | 31 + tgui/packages/tgui-panel/ping/actions.js | 25 + tgui/packages/tgui-panel/ping/constants.js | 12 + tgui/packages/tgui-panel/ping/index.js | 9 + tgui/packages/tgui-panel/ping/middleware.js | 50 + tgui/packages/tgui-panel/ping/reducer.js | 42 + tgui/packages/tgui-panel/ping/selectors.js | 7 + .../tgui-panel/settings/SettingsPanel.js | 142 +++ tgui/packages/tgui-panel/settings/actions.js | 13 + .../packages/tgui-panel/settings/constants.js | 16 + tgui/packages/tgui-panel/settings/hooks.js | 20 + tgui/packages/tgui-panel/settings/index.js | 10 + .../tgui-panel/settings/middleware.js | 46 + tgui/packages/tgui-panel/settings/reducer.js | 73 ++ .../packages/tgui-panel/settings/selectors.js | 8 + .../tgui-panel/styles/components/Chat.scss | 97 ++ .../styles/components/Notifications.scss | 26 + .../tgui-panel/styles/components/Ping.scss | 26 + .../tgui-panel/styles/goon/chat-dark.scss | 851 +++++++++++++++ .../tgui-panel/styles/goon/chat-light.scss | 873 ++++++++++++++++ tgui/packages/tgui-panel/styles/main.scss | 60 ++ .../tgui-panel/styles/themes/light.scss | 82 ++ tgui/packages/tgui-panel/telemetry.js | 95 ++ tgui/packages/tgui-panel/themes.js | 124 +++ tgui/packages/tgui-polyfill/css-om.js | 44 + tgui/packages/tgui-polyfill/dom4.js | 970 ++++++++++++++++++ tgui/packages/tgui-polyfill/html5shiv.js | 330 ++++++ tgui/packages/tgui-polyfill/ie8.js | 836 +++++++++++++++ tgui/packages/tgui-polyfill/inferno.js | 10 + tgui/packages/tgui-polyfill/package.json | 6 + tgui/packages/tgui/assets.js | 56 +- tgui/packages/tgui/backend.js | 56 +- tgui/packages/tgui/components/Box.js | 24 +- tgui/packages/tgui/components/Button.js | 13 +- tgui/packages/tgui/components/ByondUi.js | 3 +- .../tgui/components/DraggableControl.js | 26 +- tgui/packages/tgui/components/Dropdown.js | 4 +- tgui/packages/tgui/components/Flex.js | 4 +- tgui/packages/tgui/components/Input.js | 21 +- tgui/packages/tgui/components/Knob.js | 4 +- tgui/packages/tgui/components/NumberInput.js | 26 +- tgui/packages/tgui/components/Section.js | 111 +- tgui/packages/tgui/components/Tabs.js | 48 +- tgui/packages/tgui/components/TextArea.js | 56 +- tgui/packages/tgui/debug/KitchenSink.js | 142 ++- tgui/packages/tgui/debug/actions.js | 11 + tgui/packages/tgui/debug/hooks.js | 10 + tgui/packages/tgui/debug/index.js | 47 +- tgui/packages/tgui/debug/middleware.js | 80 ++ tgui/packages/tgui/debug/reducer.js | 22 + tgui/packages/tgui/debug/selectors.js | 7 + tgui/packages/tgui/drag.js | 12 +- tgui/packages/tgui/events.js | 229 +++++ tgui/packages/tgui/focus.js | 25 + tgui/packages/tgui/format.js | 16 + tgui/packages/tgui/global.d.ts | 12 + tgui/packages/tgui/hotkeys.js | 334 ++---- tgui/packages/tgui/index.js | 200 +--- tgui/packages/tgui/layouts/NtosWindow.js | 7 +- tgui/packages/tgui/layouts/Pane.js | 62 ++ tgui/packages/tgui/layouts/Window.js | 39 +- tgui/packages/tgui/links.js | 38 + tgui/packages/tgui/package.json | 2 +- tgui/packages/tgui/public/tgui.html | 68 +- tgui/packages/tgui/renderer.js | 57 + tgui/packages/tgui/routes.js | 7 +- tgui/packages/tgui/store.js | 64 +- tgui/packages/tgui/styles/base.scss | 1 + tgui/packages/tgui/styles/colors.scss | 2 +- .../tgui/styles/components/Button.scss | 15 +- .../tgui/styles/components/Dropdown.scss | 5 +- .../packages/tgui/styles/components/Flex.scss | 31 +- .../tgui/styles/components/Input.scss | 13 +- .../packages/tgui/styles/components/Knob.scss | 5 +- .../tgui/styles/components/Modal.scss | 4 +- .../tgui/styles/components/NoticeBox.scss | 6 +- .../tgui/styles/components/NumberInput.scss | 9 +- .../tgui/styles/components/ProgressBar.scss | 10 +- .../tgui/styles/components/Section.scss | 42 +- .../tgui/styles/components/Slider.scss | 11 +- .../packages/tgui/styles/components/Tabs.scss | 128 ++- .../tgui/styles/components/TextArea.scss | 69 +- .../tgui/styles/components/Tooltip.scss | 4 +- tgui/packages/tgui/styles/functions.scss | 9 + .../tgui/styles/interfaces/CameraConsole.scss | 4 +- tgui/packages/tgui/styles/layouts/Layout.scss | 16 +- .../tgui/styles/layouts/TitleBar.scss | 22 +- tgui/packages/tgui/styles/layouts/Window.scss | 2 + .../packages/tgui/styles/themes/abductor.scss | 8 +- .../tgui/styles/themes/cardtable.scss | 4 +- .../tgui/styles/themes/hackerman.scss | 2 +- .../tgui/styles/themes/malfunction.scss | 8 +- tgui/packages/tgui/styles/themes/neutral.scss | 6 +- tgui/packages/tgui/styles/themes/ntos.scss | 6 +- tgui/packages/tgui/styles/themes/paper.scss | 10 +- tgui/packages/tgui/styles/themes/retro.scss | 4 +- .../tgui/styles/themes/syndicate.scss | 8 +- tgui/packages/tgui/webpack.config.js | 37 +- tgui/yarn.lock | 21 + tools/deploy.sh | 3 +- 154 files changed, 9353 insertions(+), 1091 deletions(-) create mode 100644 tgui/packages/common/color.js create mode 100644 tgui/packages/common/events.js create mode 100644 tgui/packages/common/keycodes.js create mode 100644 tgui/packages/common/uuid.js create mode 100644 tgui/packages/tgui-dev-server/dreamseeker.js create mode 100644 tgui/packages/tgui-panel/Notifications.js create mode 100644 tgui/packages/tgui-panel/Panel.js create mode 100644 tgui/packages/tgui-panel/audio/NowPlayingWidget.js create mode 100644 tgui/packages/tgui-panel/audio/hooks.js create mode 100644 tgui/packages/tgui-panel/audio/index.js create mode 100644 tgui/packages/tgui-panel/audio/middleware.js create mode 100644 tgui/packages/tgui-panel/audio/player.js create mode 100644 tgui/packages/tgui-panel/audio/reducer.js create mode 100644 tgui/packages/tgui-panel/audio/selectors.js create mode 100644 tgui/packages/tgui-panel/chat/ChatPageSettings.js create mode 100644 tgui/packages/tgui-panel/chat/ChatPanel.js create mode 100644 tgui/packages/tgui-panel/chat/ChatTabs.js create mode 100644 tgui/packages/tgui-panel/chat/actions.js create mode 100644 tgui/packages/tgui-panel/chat/constants.js create mode 100644 tgui/packages/tgui-panel/chat/index.js create mode 100644 tgui/packages/tgui-panel/chat/middleware.js create mode 100644 tgui/packages/tgui-panel/chat/model.js create mode 100644 tgui/packages/tgui-panel/chat/reducer.js create mode 100644 tgui/packages/tgui-panel/chat/renderer.js create mode 100644 tgui/packages/tgui-panel/chat/replaceInTextNode.js create mode 100644 tgui/packages/tgui-panel/chat/selectors.js create mode 100644 tgui/packages/tgui-panel/game/actions.js create mode 100644 tgui/packages/tgui-panel/game/constants.js create mode 100644 tgui/packages/tgui-panel/game/hooks.js create mode 100644 tgui/packages/tgui-panel/game/index.js create mode 100644 tgui/packages/tgui-panel/game/middleware.js create mode 100644 tgui/packages/tgui-panel/game/reducer.js create mode 100644 tgui/packages/tgui-panel/game/selectors.js create mode 100644 tgui/packages/tgui-panel/index.js create mode 100644 tgui/packages/tgui-panel/package.json create mode 100644 tgui/packages/tgui-panel/panelFocus.js create mode 100644 tgui/packages/tgui-panel/ping/PingIndicator.js create mode 100644 tgui/packages/tgui-panel/ping/actions.js create mode 100644 tgui/packages/tgui-panel/ping/constants.js create mode 100644 tgui/packages/tgui-panel/ping/index.js create mode 100644 tgui/packages/tgui-panel/ping/middleware.js create mode 100644 tgui/packages/tgui-panel/ping/reducer.js create mode 100644 tgui/packages/tgui-panel/ping/selectors.js create mode 100644 tgui/packages/tgui-panel/settings/SettingsPanel.js create mode 100644 tgui/packages/tgui-panel/settings/actions.js create mode 100644 tgui/packages/tgui-panel/settings/constants.js create mode 100644 tgui/packages/tgui-panel/settings/hooks.js create mode 100644 tgui/packages/tgui-panel/settings/index.js create mode 100644 tgui/packages/tgui-panel/settings/middleware.js create mode 100644 tgui/packages/tgui-panel/settings/reducer.js create mode 100644 tgui/packages/tgui-panel/settings/selectors.js create mode 100644 tgui/packages/tgui-panel/styles/components/Chat.scss create mode 100644 tgui/packages/tgui-panel/styles/components/Notifications.scss create mode 100644 tgui/packages/tgui-panel/styles/components/Ping.scss create mode 100644 tgui/packages/tgui-panel/styles/goon/chat-dark.scss create mode 100644 tgui/packages/tgui-panel/styles/goon/chat-light.scss create mode 100644 tgui/packages/tgui-panel/styles/main.scss create mode 100644 tgui/packages/tgui-panel/styles/themes/light.scss create mode 100644 tgui/packages/tgui-panel/telemetry.js create mode 100644 tgui/packages/tgui-panel/themes.js create mode 100644 tgui/packages/tgui-polyfill/css-om.js create mode 100644 tgui/packages/tgui-polyfill/dom4.js create mode 100644 tgui/packages/tgui-polyfill/html5shiv.js create mode 100644 tgui/packages/tgui-polyfill/ie8.js create mode 100644 tgui/packages/tgui-polyfill/inferno.js create mode 100644 tgui/packages/tgui-polyfill/package.json create mode 100644 tgui/packages/tgui/debug/actions.js create mode 100644 tgui/packages/tgui/debug/hooks.js create mode 100644 tgui/packages/tgui/debug/middleware.js create mode 100644 tgui/packages/tgui/debug/reducer.js create mode 100644 tgui/packages/tgui/debug/selectors.js create mode 100644 tgui/packages/tgui/events.js create mode 100644 tgui/packages/tgui/focus.js create mode 100644 tgui/packages/tgui/layouts/Pane.js create mode 100644 tgui/packages/tgui/links.js create mode 100644 tgui/packages/tgui/renderer.js diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml index 9fd4db9fd2..e3c3144e5b 100644 --- a/tgui/.eslintrc.yml +++ b/tgui/.eslintrc.yml @@ -114,7 +114,7 @@ rules: ## Require return statements to either always or never specify values # consistent-return: error ## Enforce consistent brace style for all control statements - curly: [error, all] + curly: [error, multi-line] ## Require default cases in switch statements # default-case: error ## Enforce default parameters to be last @@ -374,6 +374,7 @@ rules: ignorePattern: '^(import\s.+\sfrom\s|.*require\()', ignoreUrls: true, ignoreRegExpLiterals: true, + ignoreStrings: true, }] ## Enforce a maximum number of lines per file # max-lines: error diff --git a/tgui/.gitattributes b/tgui/.gitattributes index 9382416e69..d9cdc20fbd 100644 --- a/tgui/.gitattributes +++ b/tgui/.gitattributes @@ -17,3 +17,4 @@ bin/tgui text eol=lf ## Treat bundles as binary and ignore them during conflicts *.bundle.* binary merge=tgui-merge-bundle +*.chunk.* binary merge=tgui-merge-bundle diff --git a/tgui/bin/tgui b/tgui/bin/tgui index 97a86159e6..5627b40413 100755 --- a/tgui/bin/tgui +++ b/tgui/bin/tgui @@ -67,7 +67,7 @@ task-clean() { task-validate-build() { cd "${base_dir}" local diff - diff="$(git diff packages/tgui/public/tgui.bundle.*)" + diff="$(git diff packages/tgui/public/*)" if [[ -n ${diff} ]]; then echo "Error: our build differs from the build committed into git." echo "Please rebuild tgui." diff --git a/tgui/docs/component-reference.md b/tgui/docs/component-reference.md index ff1b4e7dfd..23909c639f 100644 --- a/tgui/docs/component-reference.md +++ b/tgui/docs/component-reference.md @@ -227,7 +227,7 @@ be truncated with an ellipsis. Be careful however, because this prop breaks the baseline alignment. - `title: string` - A native browser tooltip, which appears when hovering over the button. -- `content/children: any` - Content to render inside the button. +- `children: any` - Content to render inside the button. - `onClick: function` - Called when element is clicked. ### `Button.Checkbox` @@ -389,7 +389,9 @@ to the left, and certain elements to the right: - ``` @@ -625,7 +627,9 @@ to perform some sort of action), there is a way to do that: + )}> Content @@ -643,7 +647,7 @@ to perform some sort of action), there is a way to do that: - `label: string` - Item label. - `color: string` - Sets the color of the text. - `buttons: any` - Buttons to render aside the content. -- `content/children: any` - Content of this labeled item. +- `children: any` - Content of this labeled item. ### `LabeledList.Divider` @@ -748,7 +752,7 @@ percentage and how filled the bar is. - `ranges: { color: [from, to] }` - Applies a `color` to the progress bar based on whether the value lands in the range between `from` and `to`. - `color: string` - Color of the progress bar. -- `content/children: any` - Content to render inside the progress bar. +- `children: any` - Content to render inside the progress bar. ### `Section` @@ -773,7 +777,9 @@ If you want to have a button on the right side of an section title
    + )}> Here you can order supply crates.
    @@ -784,7 +790,10 @@ If you want to have a button on the right side of an section title - `level: number` - Section level in hierarchy. Default is 1, higher number means deeper level of nesting. Must be an integer number. - `buttons: any` - Buttons to render aside the section title. -- `content/children: any` - Content of this section. +- `fill: boolean` - If true, fills all available vertical space. +- `fitted: boolean` - If true, removes all section padding. +- `scrollable: boolean` - Shows or hides the scrollbar. +- `children: any` - Content of this section. ### `Slider` @@ -953,7 +962,7 @@ Usage: **Props:** - `position: string` - Tooltip position. -- `content/children: string` - Content of the tooltip. Must be a plain string. +- `content: string` - Content of the tooltip. Must be a plain string. Fragments or other elements are **not** supported. ## `tgui/layouts` @@ -978,6 +987,7 @@ Example: **Props:** +- See inherited props: [Box](#box) - `className: string` - Applies a CSS class to the element. - `theme: string` - A name of the theme. - For a list of themes, see `packages/tgui/styles/themes`. @@ -995,6 +1005,8 @@ Can be scrollable. **Props:** +- See inherited props: [Box](#box) - `className: string` - Applies a CSS class to the element. +- `fitted: boolean` - If true, removes all padding. - `scrollable: boolean` - Shows or hides the scrollbar. - `children: any` - Main content of your window. diff --git a/tgui/docs/converting-old-tgui-interfaces.md b/tgui/docs/converting-old-tgui-interfaces.md index fe2feebfee..a42724e05c 100644 --- a/tgui/docs/converting-old-tgui-interfaces.md +++ b/tgui/docs/converting-old-tgui-interfaces.md @@ -73,7 +73,6 @@ This might look a bit intimidating compared to the reactive part but it's not as You don't really need to know all this to understand how to use it, but I find it helps with understanding when things go wrong. Ractive conditionals can have an `else` as well - ```ractive {{#if data.condition}} value @@ -117,7 +116,7 @@ and you can mix string literals, values, and tags as well. Ractive has loops for iterating over data and inserting something for each member of an array or object -```ractive +``` {{#each data.list_of_foo}} foo {{number}} is here. {{/each}} @@ -136,7 +135,6 @@ Objects are represented by `{}`, arrays by `[]` `list("bla", "blo")` would become `["bla", "blo"]` and `list("foo" = 1, "bar" = 2)` would become `{"foo": 1, "bar": 2}` First things first, above the `return` of the function you're making the interface in, you're going to want to add something like this - ```jsx const things = data.things || []; ``` @@ -144,7 +142,6 @@ const things = data.things || []; This ensures that you'll never be reading a null entry by mistake. Substitute `{}` for objects as appropriate. If it's an array, you'll want to do this in the template - ```jsx {things.map(thing => ( @@ -190,7 +187,7 @@ const fooArray = toArray(fooObject); Also occasionally you'd see an else: -```ractive +``` {{#each data.potentially_empty_list}} Thing "{{name}}" is in this list! {{else}} @@ -223,7 +220,7 @@ This will be a reference of tgui components and the tgui-next equivalent. Equivalent of `` is `
    ` -```ractive +``` Contents @@ -239,7 +236,7 @@ becomes A feature sometimes used is if `ui-display` has the `button` property, it will contain a `partial` command. This becomes the `buttons` property on `Section`: -```ractive +``` {{#partial button}} // lots more button bullshit here @@ -266,7 +263,7 @@ Very important to note `ui-section` is NOT the equivalent of `Section` `` does not have a direct equivalent, but the closest equivalent is `` -```ractive +``` No Power @@ -296,7 +293,7 @@ Also good to know that if you need the contents of a `LabeledList.Item` to be co `` has a direct equivalent in `` -```ractive +``` Notice stuff! @@ -314,7 +311,7 @@ becomes The equivalent of `ui-button` is `Button` but it works quite a bit differently. -```ractive +``` ( + new Color( + parseInt(hex.substr(1, 2), 16), + parseInt(hex.substr(3, 2), 16), + parseInt(hex.substr(5, 2), 16)) +); + +/** + * Linear interpolation of two colors. + */ +Color.lerp = (c1, c2, n) => ( + new Color( + (c2.r - c1.r) * n + c1.r, + (c2.g - c1.g) * n + c1.g, + (c2.b - c1.b) * n + c1.b, + (c2.a - c1.a) * n + c1.a) +); + +/** + * Loops up the color in the provided list of colors + * with linear interpolation. + */ +Color.lookup = (value, colors = []) => { + const len = colors.length; + if (len < 2) { + throw new Error('Needs at least two colors!'); + } + const scaled = value * (len - 1); + if (value < EPSILON) { + return colors[0]; + } + if (value >= 1 - EPSILON) { + return colors[len - 1]; + } + const ratio = scaled % 1; + const index = scaled | 0; + return Color.lerp(colors[index], colors[index + 1], ratio); +}; diff --git a/tgui/packages/common/events.js b/tgui/packages/common/events.js new file mode 100644 index 0000000000..6d590a3445 --- /dev/null +++ b/tgui/packages/common/events.js @@ -0,0 +1,42 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +export class EventEmitter { + constructor() { + this.listeners = {}; + } + + on(name, listener) { + this.listeners[name] = this.listeners[name] || []; + this.listeners[name].push(listener); + } + + off(name, listener) { + const listeners = this.listeners[name]; + if (!listeners) { + throw new Error(`There is no listeners for "${name}"`); + } + this.listeners[name] = listeners + .filter(existingListener => { + return existingListener !== listener; + }); + } + + emit(name, ...params) { + const listeners = this.listeners[name]; + if (!listeners) { + return; + } + for (let i = 0, len = listeners.length; i < len; i += 1) { + const listener = listeners[i]; + listener(...params); + } + } + + clear() { + this.listeners = {}; + } +} diff --git a/tgui/packages/common/keycodes.js b/tgui/packages/common/keycodes.js new file mode 100644 index 0000000000..8f18b154b5 --- /dev/null +++ b/tgui/packages/common/keycodes.js @@ -0,0 +1,86 @@ +/** + * All possible browser keycodes, in one file. + * + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +export const KEY_BACKSPACE = 8; +export const KEY_TAB = 9; +export const KEY_ENTER = 13; +export const KEY_SHIFT = 16; +export const KEY_CTRL = 17; +export const KEY_ALT = 18; +export const KEY_PAUSE = 19; +export const KEY_CAPSLOCK = 20; +export const KEY_ESCAPE = 27; +export const KEY_SPACE = 32; +export const KEY_PAGEUP = 33; +export const KEY_PAGEDOWN = 34; +export const KEY_END = 35; +export const KEY_HOME = 36; +export const KEY_LEFT = 37; +export const KEY_UP = 38; +export const KEY_RIGHT = 39; +export const KEY_DOWN = 40; +export const KEY_INSERT = 45; +export const KEY_DELETE = 46; +export const KEY_0 = 48; +export const KEY_1 = 49; +export const KEY_2 = 50; +export const KEY_3 = 51; +export const KEY_4 = 52; +export const KEY_5 = 53; +export const KEY_6 = 54; +export const KEY_7 = 55; +export const KEY_8 = 56; +export const KEY_9 = 57; +export const KEY_A = 65; +export const KEY_B = 66; +export const KEY_C = 67; +export const KEY_D = 68; +export const KEY_E = 69; +export const KEY_F = 70; +export const KEY_G = 71; +export const KEY_H = 72; +export const KEY_I = 73; +export const KEY_J = 74; +export const KEY_K = 75; +export const KEY_L = 76; +export const KEY_M = 77; +export const KEY_N = 78; +export const KEY_O = 79; +export const KEY_P = 80; +export const KEY_Q = 81; +export const KEY_R = 82; +export const KEY_S = 83; +export const KEY_T = 84; +export const KEY_U = 85; +export const KEY_V = 86; +export const KEY_W = 87; +export const KEY_X = 88; +export const KEY_Y = 89; +export const KEY_Z = 90; +export const KEY_F1 = 112; +export const KEY_F2 = 113; +export const KEY_F3 = 114; +export const KEY_F4 = 115; +export const KEY_F5 = 116; +export const KEY_F6 = 117; +export const KEY_F7 = 118; +export const KEY_F8 = 119; +export const KEY_F9 = 120; +export const KEY_F10 = 121; +export const KEY_F11 = 122; +export const KEY_F12 = 123; +export const KEY_SEMICOLON = 186; +export const KEY_EQUAL = 187; +export const KEY_COMMA = 188; +export const KEY_MINUS = 189; +export const KEY_PERIOD = 190; +export const KEY_SLASH = 191; +export const KEY_LEFT_BRACKET = 219; +export const KEY_BACKSLASH = 220; +export const KEY_RIGHT_BRACKET = 221; +export const KEY_QUOTE = 222; diff --git a/tgui/packages/common/package.json b/tgui/packages/common/package.json index fbd255abab..22560d92f9 100644 --- a/tgui/packages/common/package.json +++ b/tgui/packages/common/package.json @@ -1,6 +1,6 @@ { "private": true, "name": "common", - "version": "3.0.0", + "version": "4.1.0", "type": "module" } diff --git a/tgui/packages/common/perf.js b/tgui/packages/common/perf.js index 319b77cea3..8414971f93 100644 --- a/tgui/packages/common/perf.js +++ b/tgui/packages/common/perf.js @@ -1,21 +1,32 @@ /** * Ghetto performance measurement tools. * - * Uses NODE_ENV to redact itself from production bundles. + * Uses NODE_ENV to remove itself from production builds. * * @file * @copyright 2020 Aleksej Komarov * @license MIT */ -let markersByLabel = {}; +const FPS = 60; +const FRAME_DURATION = 1000 / FPS; + +// True if Performance API is supported +const supportsPerf = !!window.performance?.now; +// High precision markers +let hpMarkersByName = {}; +// Low precision markers +let lpMarkersByName = {}; /** * Marks a certain spot in the code for later measurements. */ -const mark = (label, timestamp) => { +const mark = (name, timestamp) => { if (process.env.NODE_ENV !== 'production') { - markersByLabel[label] = timestamp || Date.now(); + if (supportsPerf && !timestamp) { + hpMarkersByName[name] = performance.now(); + } + lpMarkersByName[name] = timestamp || Date.now(); } }; @@ -24,18 +35,23 @@ const mark = (label, timestamp) => { * * Use logger.log() to print the measurement. */ -const measure = (markerA, markerB) => { +const measure = (markerNameA, markerNameB) => { if (process.env.NODE_ENV !== 'production') { - return timeDiff( - markersByLabel[markerA], - markersByLabel[markerB]); + let markerA = hpMarkersByName[markerNameA]; + let markerB = hpMarkersByName[markerNameB]; + if (!markerA || !markerB) { + markerA = lpMarkersByName[markerNameA]; + markerB = lpMarkersByName[markerNameB]; + } + const duration = Math.abs(markerB - markerA); + return formatDuration(duration); } }; -const timeDiff = (startedAt, finishedAt) => { - const diff = Math.abs(finishedAt - startedAt); - const diffFrames = (diff / 16.6667).toFixed(2); - return `${diff}ms (${diffFrames} frames)`; +const formatDuration = duration => { + const durationInFrames = duration / FRAME_DURATION; + return duration.toFixed(duration < 10 ? 1 : 0) + 'ms ' + + '(' + durationInFrames.toFixed(2) + ' frames)'; }; export const perf = { diff --git a/tgui/packages/common/react.js b/tgui/packages/common/react.js index dba84b7b10..c0b24563f1 100644 --- a/tgui/packages/common/react.js +++ b/tgui/packages/common/react.js @@ -64,10 +64,10 @@ export const pureComponentHooks = { }; /** - * A helper to determine whether to render an item. + * A helper to determine whether the object is renderable by React. */ -export const isFalsy = value => { - return value === undefined - || value === null - || value === false; +export const canRender = value => { + return value !== undefined + && value !== null + && typeof value !== 'boolean'; }; diff --git a/tgui/packages/common/redux.js b/tgui/packages/common/redux.js index dc486ff7b8..12aacadb5f 100644 --- a/tgui/packages/common/redux.js +++ b/tgui/packages/common/redux.js @@ -4,6 +4,7 @@ * @license MIT */ +import { Component } from 'inferno'; import { compose } from './fp'; /** @@ -26,7 +27,9 @@ export const createStore = (reducer, enhancer) => { const dispatch = action => { currentState = reducer(currentState, action); - listeners.forEach(fn => fn()); + for (let i = 0; i < listeners.length; i++) { + listeners[i](); + } }; // This creates the initial store by causing each reducer to be called @@ -81,7 +84,7 @@ export const applyMiddleware = (...middlewares) => { export const combineReducers = reducersObj => { const keys = Object.keys(reducersObj); let hasChanged = false; - return (prevState, action) => { + return (prevState = {}, action) => { const nextState = { ...prevState }; for (let key of keys) { const reducer = reducersObj[key]; @@ -97,3 +100,55 @@ export const combineReducers = reducersObj => { : prevState; }; }; + +/** + * A utility function to create an action creator for the given action + * type string. The action creator accepts a single argument, which will + * be included in the action object as a field called payload. The action + * creator function will also have its toString() overriden so that it + * returns the action type, allowing it to be used in reducer logic that + * is looking for that action type. + * + * @param type The action type to use for created actions. + * @param prepare (optional) a method that takes any number of arguments + * and returns { payload } or { payload, meta }. If this is given, the + * resulting action creator will pass it's arguments to this method to + * calculate payload & meta. + * + * @public + */ +export const createAction = (type, prepare) => { + const actionCreator = (...args) => { + if (!prepare) { + return { type, payload: args[0] }; + } + const prepared = prepare(...args); + if (!prepared) { + throw new Error('prepare function did not return an object'); + } + const action = { type }; + if ('payload' in prepared) { + action.payload = prepared.payload; + } + if ('meta' in prepared) { + action.meta = prepared.meta; + } + return action; + }; + actionCreator.toString = () => '' + type; + actionCreator.type = type; + actionCreator.match = action => action.type === type; + return actionCreator; +}; + + +// Implementation specific +// -------------------------------------------------------- + +export const useDispatch = context => { + return context.store.dispatch; +}; + +export const useSelector = (context, selector) => { + return selector(context.store.getState()); +}; diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.js index 8e1d2183e4..33397f33a1 100644 --- a/tgui/packages/common/storage.js +++ b/tgui/packages/common/storage.js @@ -6,71 +6,152 @@ * @license MIT */ -export const STORAGE_NONE = 0; -export const STORAGE_LOCAL_STORAGE = 1; -export const STORAGE_INDEXED_DB = 2; +export const IMPL_MEMORY = 0; +export const IMPL_LOCAL_STORAGE = 1; +export const IMPL_INDEXED_DB = 2; -const createMock = () => { - let storage = {}; - const get = key => storage[key]; - const set = (key, value) => { - storage[key] = value; - }; - const remove = key => { - storage[key] = undefined; - }; - const clear = () => { - // NOTE: On IE8, this will probably leak memory if used often. - storage = {}; - }; - return { - get, - set, - remove, - clear, - engine: STORAGE_NONE, - }; -}; +const INDEXED_DB_VERSION = 1; +const INDEXED_DB_NAME = 'tgui'; +const INDEXED_DB_STORE_NAME = 'storage-v1'; -const createLocalStorage = () => { - const get = key => { - const value = localStorage.getItem(key); - if (typeof value !== 'string') { - return; - } - return JSON.parse(value); - }; - const set = (key, value) => { - localStorage.setItem(key, JSON.stringify(value)); - }; - const remove = key => { - localStorage.removeItem(key); - }; - const clear = () => { - localStorage.clear(); - }; - return { - get, - set, - remove, - clear, - engine: STORAGE_LOCAL_STORAGE, - }; -}; +const READ_ONLY = 'readonly'; +const READ_WRITE = 'readwrite'; -const testLocalStorage = () => { - // Localstorage can sometimes throw an error, even if DOM storage is not - // disabled in IE11 settings. - // See: https://superuser.com/questions/1080011 +const testGeneric = testFn => () => { try { - return Boolean(window.localStorage && window.localStorage.getItem); + return Boolean(testFn()); } catch { return false; } }; +// Localstorage can sometimes throw an error, even if DOM storage is not +// disabled in IE11 settings. +// See: https://superuser.com/questions/1080011 +const testLocalStorage = testGeneric(() => ( + window.localStorage && window.localStorage.getItem +)); + +const testIndexedDb = testGeneric(() => ( + (window.indexedDB || window.msIndexedDB) + && (window.IDBTransaction || window.msIDBTransaction) +)); + +class MemoryBackend { + constructor() { + this.impl = IMPL_MEMORY; + this.store = {}; + } + + async get(key) { + return this.store[key]; + } + + async set(key, value) { + this.store[key] = value; + } + + async remove(key) { + this.store[key] = undefined; + } + + async clear() { + this.store = {}; + } +} + +class LocalStorageBackend { + constructor() { + this.impl = IMPL_LOCAL_STORAGE; + this.store = {}; + } + + async get(key) { + const value = localStorage.getItem(key); + if (typeof value === 'string') { + return JSON.parse(value); + } + } + + async set(key, value) { + localStorage.setItem(key, JSON.stringify(value)); + } + + async remove(key) { + localStorage.removeItem(key); + } + + async clear() { + localStorage.clear(); + } +} + +class IndexedDbBackend { + constructor() { + this.impl = IMPL_INDEXED_DB; + /** @type {Promise} */ + this.dbPromise = new Promise((resolve, reject) => { + const indexedDB = window.indexedDB || window.msIndexedDB; + const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION); + req.onupgradeneeded = () => { + try { + req.result.createObjectStore(INDEXED_DB_STORE_NAME); + } + catch (err) { + reject(new Error('Failed to upgrade IDB: ' + req.error)); + } + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => { + reject(new Error('Failed to open IDB: ' + req.error)); + }; + }); + } + + getStore(mode) { + return this.dbPromise.then(db => db + .transaction(INDEXED_DB_STORE_NAME, mode) + .objectStore(INDEXED_DB_STORE_NAME)); + } + + async get(key) { + const store = await this.getStore(READ_ONLY); + return new Promise((resolve, reject) => { + const req = store.get(key); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + } + + 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; + } + // NOTE: We deliberately make this operation transactionless + const store = await this.getStore(READ_WRITE); + store.put(value, key); + } + + async remove(key) { + // NOTE: We deliberately make this operation transactionless + const store = await this.getStore(READ_WRITE); + store.delete(key); + } + + async clear() { + // NOTE: We deliberately make this operation transactionless + const store = await this.getStore(READ_WRITE); + store.clear(); + } +} + export const storage = ( - testLocalStorage() && createLocalStorage() - || createMock() + testIndexedDb() && new IndexedDbBackend() + || testLocalStorage() && new LocalStorageBackend() + || new MemoryBackend() ); diff --git a/tgui/packages/common/timer.js b/tgui/packages/common/timer.js index f4e26fa5aa..1177071b9c 100644 --- a/tgui/packages/common/timer.js +++ b/tgui/packages/common/timer.js @@ -27,3 +27,12 @@ export const debounce = (fn, time, immediate = false) => { } }; }; + +/** + * Suspends an asynchronous function for N milliseconds. + * + * @param {number} time + */ +export const sleep = time => ( + new Promise(resolve => setTimeout(resolve, time)) +); diff --git a/tgui/packages/common/uuid.js b/tgui/packages/common/uuid.js new file mode 100644 index 0000000000..7721af6494 --- /dev/null +++ b/tgui/packages/common/uuid.js @@ -0,0 +1,19 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +/** + * Creates a UUID v4 string + * + * @return {string} + */ +export const createUuid = () => { + let d = new Date().getTime(); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +}; diff --git a/tgui/packages/tgui-dev-server/dreamseeker.js b/tgui/packages/tgui-dev-server/dreamseeker.js new file mode 100644 index 0000000000..c36fe55f28 --- /dev/null +++ b/tgui/packages/tgui-dev-server/dreamseeker.js @@ -0,0 +1,87 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import axios from 'axios'; +import { exec } from 'child_process'; +import { createLogger } from 'common/logging.js'; +import { promisify } from 'util'; + +const logger = createLogger('dreamseeker'); + +const instanceByPid = new Map(); + +export class DreamSeeker { + constructor(pid, addr) { + this.pid = pid; + this.addr = addr; + this.client = axios.create({ + baseURL: `http://${addr}/`, + }); + } + + topic(params = {}) { + const query = Object.keys(params) + .map(key => encodeURIComponent(key) + + '=' + encodeURIComponent(params[key])) + .join('&'); + return this.client.get('/dummy?' + query); + } +} + +/** + * @param {number[]} pids + * @returns {DreamSeeker[]} + */ +DreamSeeker.getInstancesByPids = async pids => { + if (process.platform !== 'win32') { + return []; + } + const instances = []; + const pidsToResolve = []; + for (let pid of pids) { + const instance = instanceByPid.get(pid); + if (instance) { + instances.push(instance); + } + else { + pidsToResolve.push(pid); + } + } + if (pidsToResolve.length > 0) { + try { + const command = 'netstat -a -n -o'; + const { stdout } = await promisify(exec)(command); + // Line format: + // proto addr mask mode pid + const entries = stdout + .split('\r\n') + .filter(line => line.includes('LISTENING')) + .map(line => { + const words = line.match(/\S+/g); + return { + addr: words[1], + pid: parseInt(words[4], 10), + }; + }) + .filter(entry => pidsToResolve.includes(entry.pid)); + const len = entries.length; + logger.log('found', len, plural('instance', len)); + for (let entry of entries) { + const { pid, addr } = entry; + const instance = new DreamSeeker(pid, addr); + instances.push(instance); + instanceByPid.set(pid, instance); + } + } + catch (err) { + logger.error(err); + return []; + } + } + return instances; +}; + +const plural = (word, n) => n !== 1 ? word + 's' : word; diff --git a/tgui/packages/tgui-dev-server/index.js b/tgui/packages/tgui-dev-server/index.js index f4e8155d29..43acd87474 100644 --- a/tgui/packages/tgui-dev-server/index.js +++ b/tgui/packages/tgui-dev-server/index.js @@ -8,12 +8,15 @@ import { setupWebpack, getWebpackConfig } from './webpack.js'; import { reloadByondCache } from './reloader.js'; const noHot = process.argv.includes('--no-hot'); +const noTmp = process.argv.includes('--no-tmp'); const reloadOnce = process.argv.includes('--reload'); const setupServer = async () => { const config = await getWebpackConfig({ mode: 'development', hot: !noHot, + devServer: true, + useTmpFolder: !noTmp, }); // Reload cache once if (reloadOnce) { diff --git a/tgui/packages/tgui-dev-server/link/client.js b/tgui/packages/tgui-dev-server/link/client.js index 4671b340c5..5a4d2f6112 100644 --- a/tgui/packages/tgui-dev-server/link/client.js +++ b/tgui/packages/tgui-dev-server/link/client.js @@ -37,7 +37,7 @@ if (process.env.NODE_ENV !== 'production') { window.onunload = () => socket && socket.close(); } -const subscribe = fn => subscribers.push(fn); +export const subscribe = fn => subscribers.push(fn); /** * A json serializer which handles circular references and other junk. @@ -68,7 +68,10 @@ const serializeObject = obj => { } refs.push(value); // Error object - if (value instanceof Error) { + const isError = value instanceof Error || ( + value.code && value.message && value.message.includes('Error') + ); + if (isError) { return { __error__: true, string: String(value), @@ -88,7 +91,7 @@ const serializeObject = obj => { return json; }; -const sendRawMessage = msg => { +export const sendMessage = msg => { if (process.env.NODE_ENV !== 'production') { const json = serializeObject(msg); // Send message using WebSocket @@ -109,8 +112,8 @@ const sendRawMessage = msg => { else { const DEV_SERVER_IP = process.env.DEV_SERVER_IP || '127.0.0.1'; const req = new XMLHttpRequest(); - req.open('POST', `http://${DEV_SERVER_IP}:3001`); - req.timeout = 500; + req.open('POST', `http://${DEV_SERVER_IP}:3001`, true); + req.timeout = 250; req.send(json); } } @@ -119,7 +122,7 @@ const sendRawMessage = msg => { export const sendLogEntry = (level, ns, ...args) => { if (process.env.NODE_ENV !== 'production') { try { - sendRawMessage({ + sendMessage({ type: 'log', payload: { level, diff --git a/tgui/packages/tgui-dev-server/link/server.js b/tgui/packages/tgui-dev-server/link/server.js index 94a79c9ad5..2586d77779 100644 --- a/tgui/packages/tgui-dev-server/link/server.js +++ b/tgui/packages/tgui-dev-server/link/server.js @@ -16,37 +16,108 @@ const DEBUG = process.argv.includes('--debug'); export { loadSourceMaps }; -export const setupLink = () => { - logger.log('setting up'); - const wss = setupWebSocketLink(); - setupHttpLink(); - return { - wss, - }; -}; +export const setupLink = () => new LinkServer(); -export const broadcastMessage = (link, msg) => { - const { wss } = link; - const clients = [...wss.clients]; - logger.log(`broadcasting ${msg.type} to ${clients.length} clients`); - for (let client of clients) { - const json = JSON.stringify(msg); - client.send(json); +class LinkServer { + constructor() { + logger.log('setting up'); + this.wss = null; + this.setupWebSocketLink(); + this.setupHttpLink(); } -}; + + // WebSocket-based client link + setupWebSocketLink() { + const port = 3000; + this.wss = new WebSocket.Server({ port }); + this.wss.on('connection', ws => { + logger.log('client connected'); + ws.on('message', json => { + const msg = deserializeObject(json); + this.handleLinkMessage(ws, msg); + }); + ws.on('close', () => { + logger.log('client disconnected'); + }); + }); + logger.log(`listening on port ${port} (WebSocket)`); + } + + // One way HTTP-based client link for IE8 + setupHttpLink() { + const port = 3001; + this.httpServer = http.createServer((req, res) => { + if (req.method === 'POST') { + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + }); + req.on('end', () => { + const msg = deserializeObject(body); + this.handleLinkMessage(null, msg); + res.end(); + }); + return; + } + res.write('Hello'); + res.end(); + }); + this.httpServer.listen(port); + logger.log(`listening on port ${port} (HTTP)`); + } + + handleLinkMessage(ws, msg) { + const { type, payload } = msg; + if (type === 'log') { + const { level, ns, args } = payload; + // Skip debug messages + if (level <= 0 && !DEBUG) { + return; + } + directLog(ns, ...args.map(arg => { + if (typeof arg === 'object') { + return inspect(arg, { + depth: Infinity, + colors: true, + compact: 8, + }); + } + return arg; + })); + return; + } + if (type === 'relay') { + for (let client of this.wss.clients) { + if (client === ws) { + continue; + } + this.sendMessage(client, msg); + } + return; + } + logger.log('unhandled message', msg); + } + + sendMessage(ws, msg) { + ws.send(JSON.stringify(msg)); + } + + broadcastMessage(msg) { + const clients = [...this.wss.clients]; + if (clients.length === 0) { + return; + } + logger.log(`broadcasting ${msg.type} to ${clients.length} clients`); + for (let client of clients) { + const json = JSON.stringify(msg); + client.send(json); + } + } +} const deserializeObject = str => { return JSON.parse(str, (key, value) => { if (typeof value === 'object' && value !== null) { - if (value.__error__) { - if (!value.stack) { - return value.string; - } - return retrace(value.stack); - } - if (value.__number__) { - return parseFloat(value.__number__); - } if (value.__undefined__) { // NOTE: You should not rely on deserialized object's undefined, // this is purely for inspection purposes. @@ -54,80 +125,17 @@ const deserializeObject = str => { [inspect.custom]: () => undefined, }; } + if (value.__number__) { + return parseFloat(value.__number__); + } + if (value.__error__) { + if (!value.stack) { + return value.string; + } + return retrace(value.stack); + } return value; } return value; }); }; - -const handleLinkMessage = msg => { - const { type, payload } = msg; - - if (type === 'log') { - const { level, ns, args } = payload; - // Skip debug messages - if (level <= 0 && !DEBUG) { - return; - } - directLog(ns, ...args.map(arg => { - if (typeof arg === 'object') { - return inspect(arg, { - depth: Infinity, - colors: true, - compact: 8, - }); - } - return arg; - })); - return; - } - - logger.log('unhandled message', msg); -}; - -// WebSocket-based client link -const setupWebSocketLink = () => { - const port = 3000; - const wss = new WebSocket.Server({ port }); - - wss.on('connection', ws => { - logger.log('client connected'); - - ws.on('message', json => { - const msg = deserializeObject(json); - handleLinkMessage(msg); - }); - - ws.on('close', () => { - logger.log('client disconnected'); - }); - }); - - logger.log(`listening on port ${port} (WebSocket)`); - return wss; -}; - -// One way HTTP-based client link for IE8 -const setupHttpLink = () => { - const port = 3001; - - const server = http.createServer((req, res) => { - if (req.method === 'POST') { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - req.on('end', () => { - const msg = deserializeObject(body); - handleLinkMessage(msg); - res.end(); - }); - return; - } - res.write('Hello'); - res.end(); - }); - - server.listen(port); - logger.log(`listening on port ${port} (HTTP)`); -}; diff --git a/tgui/packages/tgui-dev-server/package.json b/tgui/packages/tgui-dev-server/package.json index e397fe3e0b..3e50a97f0e 100644 --- a/tgui/packages/tgui-dev-server/package.json +++ b/tgui/packages/tgui-dev-server/package.json @@ -1,9 +1,10 @@ { "private": true, "name": "tgui-dev-server", - "version": "3.0.0", + "version": "4.1.0", "type": "module", "dependencies": { + "axios": "^0.19.2", "glob": "^7.1.4", "source-map": "^0.7.3", "stacktrace-parser": "^0.1.7", diff --git a/tgui/packages/tgui-dev-server/reloader.js b/tgui/packages/tgui-dev-server/reloader.js index e33f7226b9..1271e67d2a 100644 --- a/tgui/packages/tgui-dev-server/reloader.js +++ b/tgui/packages/tgui-dev-server/reloader.js @@ -11,6 +11,7 @@ import { basename } from 'path'; import { promisify } from 'util'; import { resolveGlob, resolvePath } from './util.js'; import { regQuery } from './winreg.js'; +import { DreamSeeker } from './dreamseeker.js'; const logger = createLogger('reloader'); @@ -43,7 +44,7 @@ export const findCacheRoot = async () => { const paths = await resolveGlob(pattern); if (paths.length > 0) { cacheRoot = paths[0]; - logger.log(`found cache at '${cacheRoot}'`); + onCacheRootFound(cacheRoot); return cacheRoot; } } @@ -58,13 +59,19 @@ export const findCacheRoot = async () => { .replace(/\\$/, '') .replace(/\\/g, '/') + '/cache'; - logger.log(`found cache at '${cacheRoot}'`); + onCacheRootFound(cacheRoot); return cacheRoot; } } logger.log('found no cache directories'); }; +const onCacheRootFound = cacheRoot => { + logger.log(`found cache at '${cacheRoot}'`); + // Plant dummy + fs.closeSync(fs.openSync(cacheRoot + '/dummy', 'w')); +}; + export const reloadByondCache = async bundleDir => { const cacheRoot = await findCacheRoot(); if (!cacheRoot) { @@ -76,10 +83,16 @@ export const reloadByondCache = async bundleDir => { logger.log('found no tmp folder in cache'); return; } - const assets = await resolveGlob(bundleDir, './*.+(bundle|hot-update).*'); + // Get dreamseeker instances + const pids = cacheDirs.map(cacheDir => ( + parseInt(cacheDir.split('/cache/tmp').pop(), 10) + )); + const dssPromise = DreamSeeker.getInstancesByPids(pids); + // Copy assets + const assets = await resolveGlob(bundleDir, './*.+(bundle|chunk|hot-update).*'); for (let cacheDir of cacheDirs) { // Clear garbage - const garbage = await resolveGlob(cacheDir, './*.+(bundle|hot-update).*'); + const garbage = await resolveGlob(cacheDir, './*.+(bundle|chunk|hot-update).*'); for (let file of garbage) { await promisify(fs.unlink)(file); } @@ -90,4 +103,15 @@ export const reloadByondCache = async bundleDir => { } logger.log(`copied ${assets.length} files to '${cacheDir}'`); } + // Notify dreamseeker + const dss = await dssPromise; + if (dss.length > 0) { + logger.log(`notifying dreamseeker`); + for (let dreamseeker of dss) { + dreamseeker.topic({ + tgui: 1, + type: 'cacheReloaded', + }); + } + } }; diff --git a/tgui/packages/tgui-dev-server/webpack.js b/tgui/packages/tgui-dev-server/webpack.js index c625827409..7a1f600253 100644 --- a/tgui/packages/tgui-dev-server/webpack.js +++ b/tgui/packages/tgui-dev-server/webpack.js @@ -9,7 +9,7 @@ import fs from 'fs'; import { createRequire } from 'module'; import { promisify } from 'util'; import webpack from 'webpack'; -import { broadcastMessage, loadSourceMaps, setupLink } from './link/server.js'; +import { loadSourceMaps, setupLink } from './link/server.js'; import { reloadByondCache } from './reloader.js'; import { resolveGlob } from './util.js'; @@ -44,7 +44,7 @@ export const setupWebpack = async config => { // Reload cache await reloadByondCache(bundleDir); // Notify all clients that update has happened - broadcastMessage(link, { + link.broadcastMessage({ type: 'hotUpdate', }); }); @@ -55,6 +55,9 @@ export const setupWebpack = async config => { logger.error('compilation error', err); return; } - logger.log(stats.toString(config.devServer.stats)); + stats + .toString(config.devServer.stats) + .split('\n') + .forEach(line => logger.log(line)); }); }; diff --git a/tgui/packages/tgui-panel/Notifications.js b/tgui/packages/tgui-panel/Notifications.js new file mode 100644 index 0000000000..a64ddd8e30 --- /dev/null +++ b/tgui/packages/tgui-panel/Notifications.js @@ -0,0 +1,41 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { Flex } from 'tgui/components'; + +export const Notifications = props => { + const { children } = props; + return ( +
    + {children} +
    + ); +}; + +const NotificationsItem = props => { + const { + rightSlot, + children, + } = props; + return ( + + + {children} + + {rightSlot && ( + + {rightSlot} + + )} + + ); +}; + +Notifications.Item = NotificationsItem; diff --git a/tgui/packages/tgui-panel/Panel.js b/tgui/packages/tgui-panel/Panel.js new file mode 100644 index 0000000000..d86a1fc8e8 --- /dev/null +++ b/tgui/packages/tgui-panel/Panel.js @@ -0,0 +1,142 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { Button, Flex, Section } from 'tgui/components'; +import { Pane } from 'tgui/layouts'; +import { NowPlayingWidget, useAudio } from './audio'; +import { ChatPanel, ChatTabs } from './chat'; +import { useGame } from './game'; +import { Notifications } from './Notifications'; +import { PingIndicator } from './ping'; +import { SettingsPanel, useSettings } from './settings'; + +export const Panel = (props, context) => { + if (Byond.IS_LTE_IE8) { + return ( + + ); + } + const audio = useAudio(context); + const settings = useSettings(context); + const game = useGame(context); + if (process.env.NODE_ENV !== 'production') { + const { useDebug, KitchenSink } = require('tgui/debug'); + const debug = useDebug(context); + if (debug.kitchenSink) { + return ( + + ); + } + } + return ( + + + +
    + + + + + + + + +
    +
    + {audio.visible && ( + +
    + +
    +
    + )} + {settings.visible && ( + + + + )} + +
    + + + + + {game.connectionLostAt && ( + Byond.command('.reconnect')}> + Reconnect + + )}> + You are either AFK, experiencing lag or the connection + has closed. + + )} + {game.roundRestartedAt && ( + + The connection has been closed because the server is + restarting. Please wait while you automatically reconnect. + + )} + +
    +
    +
    +
    + ); +}; + +// IE8: Needs special treatment +const HoboIE8Panel = (props, context) => { + const settings = useSettings(context); + return ( + + + + {settings.visible && ( + + + + ) || ( + + )} + + + ); +}; diff --git a/tgui/packages/tgui-panel/audio/NowPlayingWidget.js b/tgui/packages/tgui-panel/audio/NowPlayingWidget.js new file mode 100644 index 0000000000..654f2a6b7c --- /dev/null +++ b/tgui/packages/tgui-panel/audio/NowPlayingWidget.js @@ -0,0 +1,69 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { toFixed } from 'common/math'; +import { useDispatch, useSelector } from 'common/redux'; +import { Fragment } from 'inferno'; +import { Button, Flex, Knob } from 'tgui/components'; +import { useSettings } from '../settings'; +import { selectAudio } from './selectors'; + +export const NowPlayingWidget = (props, context) => { + const audio = useSelector(context, selectAudio); + const dispatch = useDispatch(context); + const settings = useSettings(context); + const title = audio.meta?.title; + return ( + + {audio.playing && ( + + + Now playing: + + + {title || 'Unknown Track'} + + + ) || ( + + Nothing to play. + + )} + {audio.playing && ( + + + + + +
    + {MESSAGE_TYPES + .filter(typeDef => !typeDef.important && !typeDef.admin) + .map(typeDef => ( + dispatch(toggleAcceptedType({ + pageId: page.id, + type: typeDef.type, + }))}> + {typeDef.name} + + ))} + + {MESSAGE_TYPES + .filter(typeDef => !typeDef.important && typeDef.admin) + .map(typeDef => ( + dispatch(toggleAcceptedType({ + pageId: page.id, + type: typeDef.type, + }))}> + {typeDef.name} + + ))} + +
    +
    + ); +}; diff --git a/tgui/packages/tgui-panel/chat/ChatPanel.js b/tgui/packages/tgui-panel/chat/ChatPanel.js new file mode 100644 index 0000000000..bf7eb90e6f --- /dev/null +++ b/tgui/packages/tgui-panel/chat/ChatPanel.js @@ -0,0 +1,71 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { shallowDiffers } from 'common/react'; +import { Component, createRef, Fragment } from 'inferno'; +import { Button } from 'tgui/components'; +import { chatRenderer } from './renderer'; + +export class ChatPanel extends Component { + constructor() { + super(); + this.ref = createRef(); + this.state = { + scrollTracking: true, + }; + this.handleScrollTrackingChange = value => this.setState({ + scrollTracking: value, + }); + } + + componentDidMount() { + chatRenderer.mount(this.ref.current); + chatRenderer.events.on('scrollTrackingChanged', + this.handleScrollTrackingChange); + this.componentDidUpdate(); + } + + componentWillUnmount() { + chatRenderer.events.off('scrollTrackingChanged', + this.handleScrollTrackingChange); + } + + componentDidUpdate(prevProps) { + requestAnimationFrame(() => { + chatRenderer.ensureScrollTracking(); + }); + const shouldUpdateStyle = ( + !prevProps || shallowDiffers(this.props, prevProps) + ); + if (shouldUpdateStyle) { + chatRenderer.assignStyle({ + 'width': '100%', + 'white-space': 'pre-wrap', + 'font-size': this.props.fontSize, + 'line-height': this.props.lineHeight, + }); + } + } + + render() { + const { + scrollTracking, + } = this.state; + return ( + +
    + {!scrollTracking && ( + + )} + + ); + } +} diff --git a/tgui/packages/tgui-panel/chat/ChatTabs.js b/tgui/packages/tgui-panel/chat/ChatTabs.js new file mode 100644 index 0000000000..a0e6cc59e5 --- /dev/null +++ b/tgui/packages/tgui-panel/chat/ChatTabs.js @@ -0,0 +1,61 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { useDispatch, useSelector } from 'common/redux'; +import { Box, Tabs, Flex, Button } from 'tgui/components'; +import { changeChatPage, addChatPage } from './actions'; +import { selectChatPages, selectCurrentChatPage } from './selectors'; +import { openChatSettings } from '../settings/actions'; + +const UnreadCountWidget = ({ value }) => ( + + {Math.min(value, 99)} + +); + +export const ChatTabs = (props, context) => { + const pages = useSelector(context, selectChatPages); + const currentPage = useSelector(context, selectCurrentChatPage); + const dispatch = useDispatch(context); + return ( + + + + {pages.map(page => ( + 0 && ( + + )} + onClick={() => dispatch(changeChatPage({ + pageId: page.id, + }))}> + {page.name} + + ))} + + + +