syncs TGS, tgui, and build tools to latest tgstation (#3039)

lol said the crab, lmao
This commit is contained in:
silicons
2021-04-29 14:08:52 -06:00
committed by GitHub
parent 58d131466c
commit a2a56a6790
133 changed files with 3245 additions and 2371 deletions

6
.gitattributes vendored
View File

@@ -3,7 +3,7 @@
## Enforce text mode and LF line breaks
*.bat text eol=lf
*.css text eol=lf
*.css text eol=lf
*.cjs text eol=lf
*.dm text eol=lf
*.dme text eol=lf
*.dmf text eol=lf
@@ -14,6 +14,7 @@
*.json text eol=lf
*.jsx text eol=lf
*.md text eol=lf
*.ps1 text eol=lf
*.py text eol=lf
*.scss text eol=lf
*.sh text eol=lf
@@ -41,3 +42,6 @@
## Force changelog merging to use union
html/changelog.html text eol=lf merge=union
## Force tab indents on dm files
*dm whitespace=indent-with-non-tab

108
.github/AUTODOC_GUIDE.md vendored Normal file
View File

@@ -0,0 +1,108 @@
# dmdoc
[DOCUMENTATION]: http://codedocs.tgstation13.org
[BYOND]: https://secure.byond.com/
[DMDOC]: https://github.com/SpaceManiac/SpacemanDMM/tree/master/src/dmdoc
[DMDOC] is a documentation generator for DreamMaker, the scripting language
of the [BYOND] game engine. It produces simple static HTML files based on
documented files, macros, types, procs, and vars.
We use **dmdoc** to generate [DOCUMENTATION] for our code, and that documentation
is automatically generated and built on every new commit to the master branch
This gives new developers a clickable reference [DOCUMENTATION] they can browse to better help
gain understanding of the /tg/station codebase structure and api reference.
## Documenting code on /tg/station
We use block comments to document procs and classes, and we use `///` line comments
when documenting individual variables.
It is required that all new code be covered with DMdoc code, according to the [Requirements](#Required)
We also require that when you touch older code, you must document the functions that you
have touched in the process of updating that code
### Required
A class *must* always be autodocumented, and all public functions *must* be documented
All class level defined variables *must* be documented
Internal functions *should* be documented, but may not be
A public function is any function that a developer might reasonably call while using
or interacting with your object. Internal functions are helper functions that your
public functions rely on to implement logic
### Documenting a proc
When documenting a proc, we give a short one line description (as this is shown
next to the proc definition in the list of all procs for a type or global
namespace), then a longer paragraph which will be shown when the user clicks on
the proc to jump to it's definition
```
/**
* Short description of the proc
*
* Longer detailed paragraph about the proc
* including any relevant detail
* Arguments:
* * arg1 - Relevance of this argument
* * arg2 - Relevance of this argument
*/
```
### Documenting a class
We first give the name of the class as a header, this can be omitted if the name is
just going to be the typepath of the class, as dmdoc uses that by default
Then we give a short oneline description of the class
Finally we give a longer multi paragraph description of the class and it's details
```
/**
* # Classname (Can be omitted if it's just going to be the typepath)
*
* The short overview
*
* A longer
* paragraph of functionality about the class
* including any assumptions/special cases
*
*/
```
### Documenting a variable/define
Give a short explanation of what the variable, in the context of the class, or define is.
```
/// Type path of item to go in suit slot
var/suit = null
```
## Module level description of code
Modules are the best way to describe the structure/intent of a package of code
where you don't want to be tied to the formal layout of the class structure.
On /tg/station we do this by adding markdown files inside the `code` directory
that will also be rendered and added to the modules tree. The structure for
these is deliberately not defined, so you can be as freeform and as wheeling as
you would like.
[Here is a representative example of what you might write](http://codedocs.tgstation13.org/code/modules/keybindings/readme.html)
## Special variables
You can use certain special template variables in DM DOC comments and they will be expanded
```
[DEFINE_NAME] - Expands to a link to the define definition if documented
[/mob] - Expands to a link to the docs for the /mob class
[/mob/proc/Dizzy] - Expands to a link that will take you to the /mob class and anchor you to the dizzy proc docs
[/mob/var/stat] - Expands to a link that will take you to the /mob class and anchor you to the stat var docs
```
You can customise the link name by using `[link name][link shorthand].`
eg. `[see more about dizzy here] [/mob/proc/Dizzy]`
This is very useful to quickly link to other parts of the autodoc code to expand
upon a comment made, or reasoning about code

View File

@@ -58,8 +58,9 @@ jobs:
bash tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
python3 tools/ci/template_dm_generator.py
tgui/bin/tgui --build
bash tools/ci/dm.sh -DCIBUILDING -DCITESTING -DALL_MAPS vorestation.dme
tools/build/build
env:
CBT_BUILD_MODE : ALL_MAPS
run_all_tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -85,8 +86,8 @@ jobs:
sudo systemctl start mysql
mysql -u root -proot -e 'CREATE DATABASE tg_ci;'
mysql -u root -proot tg_ci < SQL/tgstation_schema.sql
# mysql -u root -proot -e 'CREATE DATABASE tg_ci_prefixed;'
# mysql -u root -proot tg_ci_prefixed < SQL/tgstation_schema_prefixed.sql
# mysql -u root -proot -e 'CREATE DATABASE tg_ci_prefixed;'
# mysql -u root -proot tg_ci_prefixed < SQL/tgstation_schema_prefixed.sql
- name: Install rust-g
run: |
sudo dpkg --add-architecture i386
@@ -97,9 +98,10 @@ jobs:
run: |
bash tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
tgui/bin/tgui --build
bash tools/ci/dm.sh -DCIBUILDING vorestation.dme
tools/build/build
bash tools/ci/run_server.sh
env:
CBT_BUILD_MODE: TEST_RUN
test_windows:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
@@ -109,6 +111,8 @@ jobs:
- uses: actions/checkout@v2
- name: Compile
run: pwsh tools/ci/build.ps1
env:
DM_EXE: "C:\\byond\\bin\\dm.exe"
- name: Create artifact
run: |
md deploy

View File

@@ -47,6 +47,7 @@ jobs:
git config --local user.email "action@github.com"
git config --local user.name "Changelogs"
git pull origin master
git add html/changelogs
git commit -m "Automatic changelog compile [ci skip]" -a || true
- name: "Push"
if: steps.value_holder.outputs.CL_ENABLED

13
.github/workflows/conflicts.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: 'Check for merge conflicts'
on:
push:
branches:
- master
jobs:
triage:
runs-on: ubuntu-20.04
steps:
- uses: mschilde/auto-label-merge-conflicts@2e8fcc76c6430272ec8bb64fb74ec1592156aa6a
with:
CONFLICT_LABEL_NAME: 'Merge Conflict'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -20,4 +20,3 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
tags: "latest"
cache: true

68
.gitignore vendored
View File

@@ -7,6 +7,9 @@
#Ignore byond config folder.
/cfg/**/*
# Ignore compiled linux libs in the root folder, e.g. librust_g.so
/*.so
#Ignore compiled files and other files generated during compilation.
*.mdme
*.dmb
@@ -49,27 +52,6 @@ __pycache__/
*.py[cod]
*$py.class
# C extensions
#*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
@@ -77,8 +59,7 @@ var/
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-*.txt
# Unit test / coverage reports
htmlcov/
@@ -88,7 +69,6 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
@@ -97,10 +77,6 @@ coverage.xml
# Django stuff:
*.log
local_settings.py
# Flask instance folder
instance/
# Scrapy stuff:
.scrapy
@@ -108,9 +84,6 @@ instance/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
@@ -123,10 +96,6 @@ celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# IntelliJ IDEA / PyCharm (with plugin)
.idea
@@ -149,12 +118,6 @@ Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
#*.cab
#*.msi
#*.msm
#*.msp
# Windows shortcuts
*.lnk
@@ -195,11 +158,10 @@ Temporary Items
#Visual studio stuff
*.vscode/*
!/.vscode/extensions.json
tools/MapAtmosFixer/MapAtmosFixer/obj/*
tools/MapAtmosFixer/MapAtmosFixer/bin/*
tools/CreditsTool/bin/*
tools/CreditsTool/obj/*
/tools/MapAtmosFixer/MapAtmosFixer/obj/*
/tools/MapAtmosFixer/MapAtmosFixer/bin/*
/tools/CreditsTool/bin/*
/tools/CreditsTool/obj/*
#GitHub Atom
.atom-build.json
@@ -224,10 +186,10 @@ tools/CreditsTool/obj/*
!/config/title_screens/images/exclude
#Linux docker
tools/LinuxOneShot/SetupProgram/obj/*
tools/LinuxOneShot/SetupProgram/bin/*
tools/LinuxOneShot/SetupProgram/.vs
tools/LinuxOneShot/Database
tools/LinuxOneShot/TGS_Config
tools/LinuxOneShot/TGS_Instances
tools/LinuxOneShot/TGS_Logs
/tools/LinuxOneShot/SetupProgram/obj/*
/tools/LinuxOneShot/SetupProgram/bin/*
/tools/LinuxOneShot/SetupProgram/.vs
/tools/LinuxOneShot/Database
/tools/LinuxOneShot/TGS_Config
/tools/LinuxOneShot/TGS_Instances
/tools/LinuxOneShot/TGS_Logs

View File

@@ -1,8 +1,11 @@
{
"recommendations": [
"gbasood.byond-dm-language-support",
"platymuus.dm-langclient",
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint"
]
"recommendations": [
"gbasood.byond-dm-language-support",
"platymuus.dm-langclient",
"EditorConfig.EditorConfig",
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint",
"stylemistake.auto-comment-blocks",
"Donkie.vscode-tgstation-test-adapter"
]
}

12
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "byond",
"request": "launch",
"name": "Launch DreamSeeker",
"preLaunchTask": "Build All",
"dmb": "${workspaceFolder}/${command:CurrentDMB}"
}
]
}

23
.vscode/settings.json vendored
View File

@@ -1,11 +1,16 @@
{
"eslint.nodePath": "tgui/.yarn/sdks",
"eslint.nodePath": "./tgui/.yarn/sdks",
"eslint.workingDirectories": [
"./tgui"
],
"typescript.tsdk": "./tgui/.yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"search.exclude": {
"tgui/.yarn": true,
"tgui/.pnp.*": true
"**/.yarn": true,
"**/.pnp.*": true
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"workbench.editorAssociations": [
{
@@ -14,5 +19,15 @@
}
],
"files.eol": "\n",
"gitlens.advanced.blame.customArguments": ["-w"]
"gitlens.advanced.blame.customArguments": ["-w"],
"tgstationTestExplorer.project.resultsType": "json",
"[javascript]": {
"editor.rulers": [80]
},
"[typescript]": {
"editor.rulers": [80]
},
"[scss]": {
"editor.rulers": [80]
}
}

55
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "process",
"command": "tools/build/build",
"windows": {
"command": ".\\tools\\build\\build.bat"
},
"options": {
"env": {
"DM_EXE": "${config:dreammaker.byondPath}"
}
},
"problemMatcher": [
"$dreammaker",
"$tsc",
"$eslint-stylish"
],
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": "dm: reparse",
"label": "Build All"
},
{
"type": "dreammaker",
"dme": "vorestation.dme",
"problemMatcher": [
"$dreammaker"
],
"group": "build",
"label": "dm: build - vorestation.dme"
},
{
"type": "shell",
"command": "tgui/bin/tgui",
"windows": {
"command": ".\\tgui\\bin\\tgui.bat"
},
"problemMatcher": [
"$tsc",
"$eslint-stylish"
],
"group": "build",
"label": "tgui: build"
},
{
"command": "${command:dreammaker.reparse}",
"group": "build",
"label": "dm: reparse"
}
]
}

View File

@@ -1,6 +1,6 @@
// tgstation-server DMAPI
#define TGS_DMAPI_VERSION "5.2.7"
#define TGS_DMAPI_VERSION "6.0.3"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -67,7 +67,7 @@
#define TGS_EVENT_REPO_CHECKOUT 1
/// When the repository performs a fetch operation. No parameters
#define TGS_EVENT_REPO_FETCH 2
/// When the repository merges a pull request. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user
/// When the repository test merges. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user
#define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3
/// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path
#define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4
@@ -95,6 +95,13 @@
#define TGS_EVENT_WATCHDOG_SHUTDOWN 15
/// Before the watchdog detaches for a TGS update/restart. No parameters.
#define TGS_EVENT_WATCHDOG_DETACH 16
// We don't actually implement these 4 events as the DMAPI can never receive them.
// #define TGS_EVENT_WATCHDOG_LAUNCH 17
// #define TGS_EVENT_WATCHDOG_CRASH 18
// #define TGS_EVENT_WORLD_END_PROCESS 19
// #define TGS_EVENT_WORLD_REBOOT 20
/// Watchdog event when TgsInitializationComplete() is called. No parameters.
#define TGS_EVENT_WORLD_PRIME 21
// OTHER ENUMS
@@ -115,21 +122,21 @@
//REQUIRED HOOKS
/**
* Call this somewhere in [/world/proc/New] that is always run. This function may sleep!
*
* * event_handler - Optional user defined [/datum/tgs_event_handler].
* * minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated. Can be one of [TGS_SECURITY_ULTRASAFE], [TGS_SECURITY_SAFE], or [TGS_SECURITY_TRUSTED].
*/
* Call this somewhere in [/world/proc/New] that is always run. This function may sleep!
*
* * event_handler - Optional user defined [/datum/tgs_event_handler].
* * minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated. Can be one of [TGS_SECURITY_ULTRASAFE], [TGS_SECURITY_SAFE], or [TGS_SECURITY_TRUSTED].
*/
/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
return
/**
* Call this when your initializations are complete and your game is ready to play before any player interactions happen.
*
* This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running.
* Before this point, note that any static files or directories may be in use by another server. Your code should account for this.
* This function should not be called before ..() in [/world/proc/New].
*/
* Call this when your initializations are complete and your game is ready to play before any player interactions happen.
*
* This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running.
* Afterwards, consider explicitly setting it to what you want to avoid this BYOND bug: http://www.byond.com/forum/post/2575184
* This function should not be called before ..() in [/world/proc/New].
*/
/world/proc/TgsInitializationComplete()
return
@@ -137,8 +144,8 @@
#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return
/**
* Call this at the beginning of [world/proc/Reboot].
*/
* Call this as late as possible in [world/proc/Reboot].
*/
/world/proc/TgsReboot()
return
@@ -149,6 +156,8 @@
/datum/tgs_revision_information
/// Full SHA of the commit.
var/commit
/// ISO 8601 timestamp of when the commit was created
var/timestamp
/// Full sha of last known remote commit. This may be null if the TGS repository is not currently tracking a remote branch.
var/origin_commit
@@ -172,36 +181,34 @@
var/deprefixed_parameter
/**
* Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] contains wildcards.
*/
* Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] contains wildcards.
*/
/datum/tgs_version/proc/Wildcard()
return
/**
* Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] equals some other version.
*
* other_version - The [/datum/tgs_version] to compare against.
*/
* Returns [TRUE]/[FALSE] based on if the [/datum/tgs_version] equals some other version.
*
* other_version - The [/datum/tgs_version] to compare against.
*/
/datum/tgs_version/proc/Equals(datum/tgs_version/other_version)
return
/// Represents a merge of a GitHub pull request.
/datum/tgs_revision_information/test_merge
/// The pull request number.
/// The test merge number.
var/number
/// The pull request title when it was merged.
/// The test merge source's title when it was merged.
var/title
/// The pull request body when it was merged.
/// The test merge source's body when it was merged.
var/body
/// The GitHub username of the pull request's author.
/// The Username of the test merge source's author.
var/author
/// An http URL to the pull request.
/// An http URL to the test merge source.
var/url
/// The SHA of the pull request when that was merged.
var/pull_request_commit
/// ISO 8601 timestamp of when the pull request was merged.
var/time_merged
/// (Nullable) Comment left by the TGS user who initiated the merge..
/// The SHA of the test merge when that was merged.
var/head_commit
/// Optional comment left by the TGS user who initiated the merge.
var/comment
/// Represents a connected chat channel.
@@ -231,10 +238,10 @@
var/datum/tgs_chat_channel/channel
/**
* User definable callback for handling TGS events.
*
* event_code - One of the TGS_EVENT_ defines. Extra parameters will be documented in each
*/
* User definable callback for handling TGS events.
*
* event_code - One of the TGS_EVENT_ defines. Extra parameters will be documented in each
*/
/datum/tgs_event_handler/proc/HandleEvent(event_code, ...)
set waitfor = FALSE
return
@@ -249,67 +256,67 @@
var/admin_only = FALSE
/**
* Process command activation. Should return a string to respond to the issuer with.
*
* sender - The [/datum/tgs_chat_user] who issued the command.
* params - The trimmed string following the command `/datum/tgs_chat_command/var/name].
*/
* Process command activation. Should return a string to respond to the issuer with.
*
* sender - The [/datum/tgs_chat_user] who issued the command.
* params - The trimmed string following the command `/datum/tgs_chat_command/var/name].
*/
/datum/tgs_chat_command/proc/Run(datum/tgs_chat_user/sender, params)
CRASH("[type] has no implementation for Run()")
// API FUNCTIONS
/// Returns the maximum supported [/datum/tgs_version] of the DMAPI.
/world/proc/TgsMaximumAPIVersion()
/world/proc/TgsMaximumApiVersion()
return
/// Returns the minimum supported [/datum/tgs_version] of the DMAPI.
/world/proc/TgsMinimumAPIVersion()
/world/proc/TgsMinimumApiVersion()
return
/**
* Returns [TRUE] if DreamDaemon was launched under TGS, the API matches, and was properly initialized. [FALSE] will be returned otherwise.
*/
* Returns [TRUE] if DreamDaemon was launched under TGS, the API matches, and was properly initialized. [FALSE] will be returned otherwise.
*/
/world/proc/TgsAvailable()
return
// No function below this succeeds if it TgsAvailable() returns FALSE or if TgsNew() has yet to be called.
/**
* Forces a hard reboot of DreamDaemon by ending the process.
*
* Unlike del(world) clients will try to reconnect.
* If TGS has not requested a [TGS_REBOOT_MODE_SHUTDOWN] DreamDaemon will be launched again
*/
* Forces a hard reboot of DreamDaemon by ending the process.
*
* Unlike del(world) clients will try to reconnect.
* If TGS has not requested a [TGS_REBOOT_MODE_SHUTDOWN] DreamDaemon will be launched again
*/
/world/proc/TgsEndProcess()
return
/**
* Send a message to connected chats.
*
* message - The string to send.
* admin_only: If [TRUE], message will be sent to admin connected chats. Vice-versa applies.
*/
* Send a message to connected chats.
*
* message - The string to send.
* admin_only: If [TRUE], message will be sent to admin connected chats. Vice-versa applies.
*/
/world/proc/TgsTargetedChatBroadcast(message, admin_only = FALSE)
return
/**
* Send a private message to a specific user.
*
* message - The string to send.
* user: The [/datum/tgs_chat_user] to PM.
*/
* Send a private message to a specific user.
*
* message - The string to send.
* user: The [/datum/tgs_chat_user] to PM.
*/
/world/proc/TgsChatPrivateMessage(message, datum/tgs_chat_user/user)
return
// The following functions will sleep if a call to TgsNew() is sleeping
/**
* Send a message to connected chats that are flagged as game-related in TGS.
*
* message - The string to send.
* channels - Optional list of [/datum/tgs_chat_channel]s to restrict the message to.
*/
* Send a message to connected chats that are flagged as game-related in TGS.
*
* message - The string to send.
* channels - Optional list of [/datum/tgs_chat_channel]s to restrict the message to.
*/
/world/proc/TgsChatBroadcast(message, list/channels = null)
return

View File

@@ -0,0 +1,2 @@
/datum/config_entry/flag/emergency_tgui_logging
config_entry_value = FALSE

View File

@@ -16,7 +16,7 @@
if(revinfo)
commit = revinfo.commit
originmastercommit = revinfo.origin_commit
date = rustg_git_commit_date(commit)
date = revinfo.timestamp || rustg_git_commit_date(commit)
// goes to DD log and config_error.txt
log_world(get_log_message())
@@ -29,7 +29,8 @@
for(var/line in testmerge)
var/datum/tgs_revision_information/test_merge/tm = line
msg += "Test merge active of PR #[tm.number] commit [tm.pull_request_commit]"
msg += "Test merge active of PR #[tm.number] commit [tm.head_commit]"
// SSblackbox.record_feedback("associative", "testmerged_prs", 1, list("number" = "[tm.number]", "commit" = "[tm.head_commit]", "title" = "[tm.title]", "author" = "[tm.author]"))
if(commit && commit != originmastercommit)
msg += "HEAD: [commit]"
@@ -44,7 +45,7 @@
. = header ? "The following pull requests are currently test merged:<br>" : ""
for(var/line in testmerge)
var/datum/tgs_revision_information/test_merge/tm = line
var/cm = tm.pull_request_commit
var/cm = tm.head_commit
var/details = ": '" + html_encode(tm.title) + "' by " + html_encode(tm.author) + " at commit " + html_encode(copytext_char(cm, 1, 11))
if(details && findtext(details, "\[s\]") && (!usr || !usr.client.holder))
continue
@@ -78,7 +79,9 @@
msg += "No commit information"
if(world.TgsAvailable())
var/datum/tgs_version/version = world.TgsVersion()
msg += "Server tools version: [version.raw_parameter]"
msg += "TGS version: [version.raw_parameter]"
var/datum/tgs_version/api_version = world.TgsApiVersion()
msg += "DMAPI version: [api_version.raw_parameter]"
/*
// Game mode odds
@@ -123,5 +126,5 @@
if(probabilities[ctag] > 0)
var/percentage = round(probabilities[ctag] / sum * 100, 0.1)
msg += "[ctag] [percentage]%"
*/
to_chat(src, msg.Join("<br>"))
*/

View File

@@ -5,7 +5,7 @@ var/global/floorIsLava = 0
////////////////////////////////
/proc/message_admins(var/msg)
msg = "<span class='log_message'><span class='prefix'>ADMIN LOG:</span> <span class='message'>[msg]</span></span>"
msg = "<span class='admin log_message'><span class='prefix'>ADMIN LOG:</span> <span class='message'>[msg]</span></span>"
//log_adminwarn(msg) //log_and_message_admins is for this
for(var/client/C in admins)
@@ -13,7 +13,7 @@ var/global/floorIsLava = 0
to_chat(C, msg)
/proc/msg_admin_attack(var/text) //Toggleable Attack Messages
var/rendered = "<span class='log_message><span class='prefix'>ATTACK:</span> <span class='message'>[text]</span></span>"
var/rendered = "<span class='admin log_message><span class='prefix'>ATTACK:</span> <span class='message'>[text]</span></span>"
for(var/client/C in admins)
if((R_ADMIN|R_MOD) & C.holder.rights)
if(C.is_preference_enabled(/datum/client_preference/mod/show_attack_logs))

View File

@@ -6,7 +6,7 @@
/datum/asset/simple/tgui_common
keep_local_name = TRUE
assets = list(
"tgui-common.chunk.js" = 'tgui/public/tgui-common.chunk.js',
"tgui-common.chunk.js" = 'tgui/public/tgui-common.bundle.js',
)
/datum/asset/simple/tgui
@@ -203,6 +203,13 @@
)
parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css')
/datum/asset/simple/namespaced/tgfont
assets = list(
"tgfont.eot" = 'tgui/packages/tgfont/dist/tgfont.eot',
"tgfont.woff2" = 'tgui/packages/tgfont/dist/tgfont.woff2',
)
parents = list("tgfont.css" = 'tgui/packages/tgfont/dist/tgfont.css')
/datum/asset/spritesheet/chat
name = "chat"

View File

@@ -60,7 +60,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
topiclimiter[ADMINSWARNED_AT] = minute
msg += " Administrators have been informed."
log_game("[key_name(src)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
message_admins("[ADMIN_LOOKUPFLW(src)] [ADMIN_KICK(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
message_admins("[ADMIN_LOOKUPFLW(usr)] [ADMIN_KICK(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
to_chat(src, "<span class='danger'>[msg]</span>")
return
@@ -77,13 +77,15 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
to_chat(src, "<span class='danger'>Your previous action was ignored because you've done too many in a second</span>")
return
// Tgui Topic middleware.
// Tgui Topic middleware
if(tgui_Topic(href_list))
if(CONFIG_GET(flag/emergency_tgui_logging))
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
return
//Logs all hrefs, except chat pings
if(!(href_list["_src_"] == "chat" && href_list["proc"] == "ping" && LAZYLEN(href_list) == 2))
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
//Logs all hrefs
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
//byond bug ID:2256651
if (asset_cache_job && (asset_cache_job in completed_asset_jobs))

View File

@@ -40,7 +40,7 @@
if(5)
api_datum = /datum/tgs_api/v5
var/datum/tgs_version/max_api_version = TgsMaximumAPIVersion();
var/datum/tgs_version/max_api_version = TgsMaximumApiVersion();
if(version.suite != null && version.minor != null && version.patch != null && version.deprecated_patch != null && version.deprefixed_parameter > max_api_version.deprefixed_parameter)
TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
api_datum = /datum/tgs_api/latest
@@ -64,10 +64,10 @@
TGS_WRITE_GLOBAL(tgs, null)
TGS_ERROR_LOG("Failed to activate API!")
/world/TgsMaximumAPIVersion()
/world/TgsMaximumApiVersion()
return new /datum/tgs_version("5.x.x")
/world/TgsMinimumAPIVersion()
/world/TgsMinimumApiVersion()
return new /datum/tgs_version("3.2.x")
/world/TgsInitializationComplete()

View File

@@ -50,7 +50,7 @@ TGS_PROTECT_DATUM(/datum/tgs_api)
/datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only)
return TGS_UNIMPLEMENTED
/datum/tgs_api/proc/ChatPrivateMessage(message, admin_only)
/datum/tgs_api/proc/ChatPrivateMessage(message, datum/tgs_chat_user/user)
return TGS_UNIMPLEMENTED
/datum/tgs_api/proc/SecurityLevel()

View File

@@ -14,4 +14,4 @@
#include "v5\_defines.dm"
#include "v5\api.dm"
#include "v5\commands.dm"
#include "v5\undef.dm"
#include "v5\undefs.dm"

View File

@@ -62,7 +62,7 @@
comms_key = world.params[SERVICE_WORLD_PARAM]
instance_name = world.params[SERVICE_INSTANCE_PARAM]
if(!instance_name)
instance_name = "TG Station Server" //maybe just upgraded
instance_name = "TG Station Server" //maybe just upgraded
var/list/logs = file2list(".git/logs/HEAD")
if(logs.len)
@@ -92,14 +92,14 @@
if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL))
TGS_ERROR_LOG("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.")
return
call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval
call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval
return TRUE
/datum/tgs_api/v3210/OnTopic(T)
var/list/params = params2list(T)
var/their_sCK = params[SERVICE_CMD_PARAM_KEY]
if(!their_sCK)
return FALSE //continue world/Topic
return FALSE //continue world/Topic
if(their_sCK != comms_key)
return "Invalid comms key!";
@@ -160,7 +160,7 @@
var/datum/tgs_revision_information/test_merge/tm = new
tm.number = text2num(I)
var/list/entry = json[I]
tm.pull_request_commit = entry["commit"]
tm.head_commit = entry["commit"]
tm.author = entry["author"]
tm.title = entry["title"]
. += tm
@@ -176,11 +176,11 @@
return ri
/datum/tgs_api/v3210/EndProcess()
sleep(world.tick_lag) //flush the buffers
sleep(world.tick_lag) //flush the buffers
ExportService(SERVICE_REQUEST_KILL_PROCESS)
/datum/tgs_api/v3210/ChatChannelInfo()
return list()
return list() // :omegalul:
/datum/tgs_api/v3210/ChatBroadcast(message, list/channels)
if(channels)

View File

@@ -92,7 +92,7 @@
var/list/json = cached_json["testMerges"]
for(var/entry in json)
var/datum/tgs_revision_information/test_merge/tm = new
tm.time_merged = text2num(entry["timeMerged"])
tm.timestamp = text2num(entry["timeMerged"])
var/list/revInfo = entry["revision"]
if(revInfo)
@@ -104,7 +104,7 @@
tm.url = entry["url"]
tm.author = entry["author"]
tm.number = entry["number"]
tm.pull_request_commit = entry["pullRequestRevision"]
tm.head_commit = entry["pullRequestRevision"]
tm.comment = entry["comment"]
cached_test_merges += tm
@@ -114,18 +114,11 @@
/datum/tgs_api/v4/OnInitializationComplete()
Export(TGS4_COMM_SERVER_PRIMED)
var/tgs4_secret_sleep_offline_sauce = 24051994
var/old_sleep_offline = world.sleep_offline
world.sleep_offline = tgs4_secret_sleep_offline_sauce
sleep(1)
if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it
world.sleep_offline = old_sleep_offline
/datum/tgs_api/v4/OnTopic(T)
var/list/params = params2list(T)
var/their_sCK = params[TGS4_INTEROP_ACCESS_IDENTIFIER]
if(!their_sCK)
return FALSE //continue world/Topic
return FALSE //continue world/Topic
if(their_sCK != access_identifier)
return "Invalid comms key!";
@@ -199,7 +192,7 @@
//request a new port
export_lock = FALSE
var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose
var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose
if(!new_port_json)
TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]")
@@ -242,7 +235,7 @@
var/port = result[TGS4_PARAMETER_DATA]
if(!isnum(port))
return //this is valid, server may just want use to reboot
return //this is valid, server may just want use to reboot
if(port == 0)
//to byond 0 means any port and "none" means close vOv
@@ -255,7 +248,7 @@
return instance_name
/datum/tgs_api/v4/TestMerges()
return cached_test_merges
return cached_test_merges.Copy()
/datum/tgs_api/v4/EndProcess()
Export(TGS4_COMM_END_PROCESS)

View File

@@ -0,0 +1,8 @@
# DMAPI V5
This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no security restrictions.
- [_defines.dm](./_defines.dm) contains constant definitions.
- [api.dm](./api.dm) contains the bulk of the API code.
- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
- [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`.

View File

@@ -79,6 +79,7 @@
#define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses"
#define DMAPI5_REVISION_INFORMATION_COMMIT_SHA "commitSha"
#define DMAPI5_REVISION_INFORMATION_TIMESTAMP "timestamp"
#define DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA "originCommitSha"
#define DMAPI5_CHAT_USER_ID "id"

View File

@@ -15,8 +15,12 @@
var/datum/tgs_revision_information/revision
var/list/chat_channels
var/initialized = FALSE
/datum/tgs_api/v5/ApiVersion()
return new /datum/tgs_version(TGS_DMAPI_VERSION)
return new /datum/tgs_version(
#include "interop_version.dm"
)
/datum/tgs_api/v5/OnWorldNew(minimum_required_security_level)
server_port = world.params[DMAPI5_PARAM_SERVER_PORT]
@@ -46,6 +50,7 @@
if(istype(revisionData))
revision = new
revision.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA]
revision.timestamp = revisionData[DMAPI5_REVISION_INFORMATION_TIMESTAMP]
revision.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA]
else
TGS_ERROR_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_REVISION] from runtime information!")
@@ -61,15 +66,18 @@
if(revInfo)
tm.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA]
tm.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA]
tm.timestamp = entry[DMAPI5_REVISION_INFORMATION_TIMESTAMP]
else
TGS_WARNING_LOG("Failed to decode [DMAPI5_TEST_MERGE_REVISION] from test merge #[tm.number]!")
tm.time_merged = text2num(entry[DMAPI5_TEST_MERGE_TIME_MERGED])
if(!tm.timestamp)
tm.timestamp = entry[DMAPI5_TEST_MERGE_TIME_MERGED]
tm.title = entry[DMAPI5_TEST_MERGE_TITLE_AT_MERGE]
tm.body = entry[DMAPI5_TEST_MERGE_BODY_AT_MERGE]
tm.url = entry[DMAPI5_TEST_MERGE_URL]
tm.author = entry[DMAPI5_TEST_MERGE_AUTHOR]
tm.pull_request_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION]
tm.head_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION]
tm.comment = entry[DMAPI5_TEST_MERGE_COMMENT]
test_merges += tm
@@ -79,6 +87,7 @@
chat_channels = list()
DecodeChannels(runtime_information)
initialized = TRUE
return TRUE
/datum/tgs_api/v5/proc/RequireInitialBridgeResponse()
@@ -88,13 +97,6 @@
/datum/tgs_api/v5/OnInitializationComplete()
Bridge(DMAPI5_BRIDGE_COMMAND_PRIME)
var/tgs4_secret_sleep_offline_sauce = 29051994
var/old_sleep_offline = world.sleep_offline
world.sleep_offline = tgs4_secret_sleep_offline_sauce
sleep(1)
if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it
world.sleep_offline = old_sleep_offline
/datum/tgs_api/v5/proc/TopicResponse(error_message = null)
var/list/response = list()
response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message
@@ -105,12 +107,16 @@
var/list/params = params2list(T)
var/json = params[DMAPI5_TOPIC_DATA]
if(!json)
return FALSE //continue world/Topic
return FALSE // continue to /world/Topic
var/list/topic_parameters = json_decode(json)
if(!topic_parameters)
return TopicResponse("Invalid topic parameters json!");
if(!initialized)
TGS_WARNING_LOG("Missed topic due to not being initialized: [T]")
return TRUE // too early to handle, but it's still our responsibility
var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER]
if(their_sCK != access_identifier)
return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!");
@@ -267,7 +273,7 @@
var/port = result[DMAPI5_BRIDGE_RESPONSE_NEW_PORT]
if(!isnum(port))
return //this is valid, server may just want use to reboot
return //this is valid, server may just want use to reboot
if(port == 0)
//to byond 0 means any port and "none" means close vOv
@@ -282,7 +288,7 @@
/datum/tgs_api/v5/TestMerges()
RequireInitialBridgeResponse()
return test_merges
return test_merges.Copy()
/datum/tgs_api/v5/EndProcess()
Bridge(DMAPI5_BRIDGE_COMMAND_KILL)
@@ -327,7 +333,7 @@
/datum/tgs_api/v5/ChatChannelInfo()
RequireInitialBridgeResponse()
return chat_channels
return chat_channels.Copy()
/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json)
var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS]

View File

@@ -0,0 +1 @@
"5.3.0"

View File

@@ -0,0 +1,99 @@
#undef DMAPI5_PARAM_SERVER_PORT
#undef DMAPI5_PARAM_ACCESS_IDENTIFIER
#undef DMAPI5_BRIDGE_DATA
#undef DMAPI5_TOPIC_DATA
#undef DMAPI5_BRIDGE_COMMAND_PORT_UPDATE
#undef DMAPI5_BRIDGE_COMMAND_STARTUP
#undef DMAPI5_BRIDGE_COMMAND_PRIME
#undef DMAPI5_BRIDGE_COMMAND_REBOOT
#undef DMAPI5_BRIDGE_COMMAND_KILL
#undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND
#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER
#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS
#undef DMAPI5_RESPONSE_ERROR_MESSAGE
#undef DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE
#undef DMAPI5_BRIDGE_PARAMETER_CURRENT_PORT
#undef DMAPI5_BRIDGE_PARAMETER_VERSION
#undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE
#undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL
#undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT
#undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION
#undef DMAPI5_CHAT_MESSAGE_TEXT
#undef DMAPI5_CHAT_MESSAGE_CHANNEL_IDS
#undef DMAPI5_RUNTIME_INFORMATION_ACCESS_IDENTIFIER
#undef DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION
#undef DMAPI5_RUNTIME_INFORMATION_SERVER_PORT
#undef DMAPI5_RUNTIME_INFORMATION_API_VALIDATE_ONLY
#undef DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME
#undef DMAPI5_RUNTIME_INFORMATION_REVISION
#undef DMAPI5_RUNTIME_INFORMATION_TEST_MERGES
#undef DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL
#undef DMAPI5_CHAT_UPDATE_CHANNELS
#undef DMAPI5_TEST_MERGE_TIME_MERGED
#undef DMAPI5_TEST_MERGE_REVISION
#undef DMAPI5_TEST_MERGE_TITLE_AT_MERGE
#undef DMAPI5_TEST_MERGE_BODY_AT_MERGE
#undef DMAPI5_TEST_MERGE_URL
#undef DMAPI5_TEST_MERGE_AUTHOR
#undef DMAPI5_TEST_MERGE_NUMBER
#undef DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION
#undef DMAPI5_TEST_MERGE_COMMENT
#undef DMAPI5_CHAT_COMMAND_NAME
#undef DMAPI5_CHAT_COMMAND_PARAMS
#undef DMAPI5_CHAT_COMMAND_USER
#undef DMAPI5_EVENT_NOTIFICATION_TYPE
#undef DMAPI5_EVENT_NOTIFICATION_PARAMETERS
#undef DMAPI5_TOPIC_COMMAND_CHAT_COMMAND
#undef DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION
#undef DMAPI5_TOPIC_COMMAND_CHANGE_PORT
#undef DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE
#undef DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED
#undef DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE
#undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE
#undef DMAPI5_TOPIC_COMMAND_HEARTBEAT
#undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH
#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE
#undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND
#undef DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION
#undef DMAPI5_TOPIC_PARAMETER_NEW_PORT
#undef DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE
#undef DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME
#undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE
#undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION
#undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE
#undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES
#undef DMAPI5_REVISION_INFORMATION_COMMIT_SHA
#undef DMAPI5_REVISION_INFORMATION_TIMESTAMP
#undef DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA
#undef DMAPI5_CHAT_USER_ID
#undef DMAPI5_CHAT_USER_FRIENDLY_NAME
#undef DMAPI5_CHAT_USER_MENTION
#undef DMAPI5_CHAT_USER_CHANNEL
#undef DMAPI5_CHAT_CHANNEL_ID
#undef DMAPI5_CHAT_CHANNEL_FRIENDLY_NAME
#undef DMAPI5_CHAT_CHANNEL_CONNECTION_NAME
#undef DMAPI5_CHAT_CHANNEL_IS_ADMIN_CHANNEL
#undef DMAPI5_CHAT_CHANNEL_IS_PRIVATE_CHANNEL
#undef DMAPI5_CHAT_CHANNEL_TAG
#undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME
#undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT
#undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY

View File

@@ -0,0 +1,15 @@
/*!
* Copyright (c) 2021 Arm A. Hammer
* SPDX-License-Identifier: MIT
*/
/**
* tgui state: never_state
*
* Always closes the UI, no matter what. See the ui_state in religious_tool.dm to see an example
*/
GLOBAL_DATUM_INIT(never_state, /datum/ui_state/never_state, new)
/datum/ui_state/never_state/can_use_topic(src_object, mob/user)
return UI_CLOSE

View File

@@ -96,6 +96,8 @@
window.send_message("ping")
var/flush_queue = window.send_asset(get_asset_datum(
/datum/asset/simple/namespaced/fontawesome))
flush_queue |= window.send_asset(get_asset_datum(
/datum/asset/simple/namespaced/tgfont))
for(var/datum/asset/asset in src_object.ui_assets(user))
flush_queue |= window.send_asset(asset)
if (flush_queue)

View File

@@ -1,4 +1,4 @@
/**
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/

View File

@@ -1,4 +1,4 @@
/**
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/

View File

@@ -1,4 +1,4 @@
/**
/*!
* Copyright (c) 2020 Aleksej Komarov
* SPDX-License-Identifier: MIT
*/
@@ -37,6 +37,9 @@
* Initializes tgui panel.
*/
/datum/tgui_panel/proc/initialize(force = FALSE)
set waitfor = FALSE
// Minimal sleep to defer initialization to after client constructor
sleep(1)
initialized_at = world.time
// Perform a clean initialization
window.initialize(inline_assets = list(
@@ -44,9 +47,11 @@
get_asset_datum(/datum/asset/simple/tgui_panel),
))
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/tgfont))
window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat))
// Other setup
request_telemetry()
addtimer(CALLBACK(src, .proc/on_initialize_timed_out), 2 SECONDS)
addtimer(CALLBACK(src, .proc/on_initialize_timed_out), 5 SECONDS)
/**
* private

View File

@@ -6,6 +6,7 @@ $include game_options.txt
#$include antag_rep.txt
$include resources.txt
# Cit-specific imports
$include logging.txt
$include entries/urls.txt
$include entries/fail2topic.txt
$include entries/lobby.txt

2
config/logging.txt Normal file
View File

@@ -0,0 +1,2 @@
# Forces tgui hrefs to be logged. Debug only.
# EMERGENCY_TGUI_LOGGING

0
dependencies.sh Normal file → Executable file
View File

View File

@@ -1,15 +1,43 @@
rules:
## Enforce a maximum cyclomatic complexity allowed in a program
complexity: [error, { max: 25 }]
# complexity: [warn, { max: 25 }]
## Enforce consistent brace style for blocks
brace-style: [error, stroustrup, { allowSingleLine: false }]
# brace-style: [warn, stroustrup, { allowSingleLine: false }]
## Enforce the consistent use of either backticks, double, or single quotes
quotes: [error, single, {
avoidEscape: true,
allowTemplateLiterals: true,
}]
react/jsx-closing-bracket-location: [error, {
selfClosing: after-props,
nonEmpty: after-props,
}]
react/display-name: error
# quotes: [warn, single, {
# avoidEscape: true,
# allowTemplateLiterals: true,
# }]
# react/jsx-closing-bracket-location: [warn, {
# selfClosing: after-props,
# nonEmpty: after-props,
# }]
# react/display-name: warn
## Radar
## ------------------------------------------------------
# radar/cognitive-complexity: warn
radar/max-switch-cases: warn
radar/no-all-duplicated-branches: warn
radar/no-collapsible-if: warn
radar/no-collection-size-mischeck: warn
radar/no-duplicate-string: warn
radar/no-duplicated-branches: warn
radar/no-element-overwrite: warn
radar/no-extra-arguments: warn
radar/no-identical-conditions: warn
radar/no-identical-expressions: warn
radar/no-identical-functions: warn
radar/no-inverted-boolean-check: warn
radar/no-one-iteration-loop: warn
radar/no-redundant-boolean: warn
radar/no-redundant-jump: warn
radar/no-same-line-conditional: warn
radar/no-small-switch: warn
radar/no-unused-collection: warn
radar/no-use-of-empty-return-value: warn
radar/no-useless-catch: warn
radar/prefer-immediate-return: warn
radar/prefer-object-literal: warn
radar/prefer-single-boolean-return: warn
radar/prefer-while: warn

View File

@@ -9,9 +9,8 @@ env:
es6: true
browser: true
node: true
globals:
Byond: readonly
plugins:
- radar
- react
settings:
react:
@@ -20,7 +19,6 @@ rules:
## Possible Errors
## ----------------------------------------
## Enforce “for” loop update clause moving the counter in the right
## direction.
# for-direction: error
@@ -509,7 +507,7 @@ rules:
## Require braces around arrow function bodies
# arrow-body-style: error
## Require parentheses around arrow function arguments
arrow-parens: [error, as-needed]
# arrow-parens: [error, as-needed]
## Enforce consistent spacing before and after the arrow in arrow functions
arrow-spacing: [error, { before: true, after: true }]
## Require super() calls in constructors

8
tgui/.gitignore vendored
View File

@@ -13,10 +13,10 @@ package-lock.json
!/.yarn/lock.yml
## Build artifacts
/public/.tmp/**/*
/public/**/*
!/public/*.html
# /public/.tmp/**/*
# /public/**/*
# !/public/*.html
## Previously ignored locations that are kept to avoid confusing git
## while transitioning to a new project structure.
/packages/tgui/public/**
# /packages/tgui/public/**

12
tgui/.prettierrc.yml Normal file
View File

@@ -0,0 +1,12 @@
arrowParens: always
bracketSpacing: true
endOfLine: lf
jsxBracketSameLine: true
jsxSingleQuote: false
printWidth: 80
proseWrap: preserve
quoteProps: preserve
semi: true
singleQuote: true
tabWidth: 2
trailingComma: es5

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20
tgui/.yarn/sdks/eslint/lib/api.js vendored Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/lib/api.js
require(absPnpApiPath).setup();
}
}
// Defer to the real eslint/lib/api.js your application uses
module.exports = absRequire(`eslint/lib/api.js`);

View File

@@ -1,6 +1,6 @@
{
"name": "eslint",
"version": "7.19.0-pnpify",
"version": "7.21.0-pnpify",
"main": "./lib/api.js",
"type": "commonjs"
}

20
tgui/.yarn/sdks/typescript/lib/tsc.js vendored Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsc.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsc.js your application uses
module.exports = absRequire(`typescript/lib/tsc.js`);

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
const moduleWrapper = tsserver => {
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || str.match(/\$\$virtual\//))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes is much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = pnpApi.resolveVirtual(str);
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) {
str = resolved;
}
}
str = str.replace(/\\/g, `/`)
str = str.replace(/^\/?/, `/`);
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
if (str.match(/\.zip\//)) {
str = `${isVSCode ? `^` : ``}zip:${str}`;
}
}
return str;
}
function fromEditorPath(str) {
return process.platform === `win32`
? str.replace(/^\^?zip:\//, ``)
: str.replace(/^\^?zip:/, ``);
}
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let isVSCode = false;
return Object.assign(Session.prototype, {
onMessage(/** @type {string} */ message) {
const parsedMessage = JSON.parse(message)
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
parsedMessage.arguments.hostInfo === `vscode`
) {
isVSCode = true;
}
return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
}));
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserver.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserver.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/typescript.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/typescript.js your application uses
module.exports = absRequire(`typescript/lib/typescript.js`);

View File

@@ -1,6 +1,6 @@
{
"name": "typescript",
"version": "4.1.5-pnpify",
"version": "4.2.3-pnpify",
"main": "./lib/typescript.js",
"type": "commonjs"
}

View File

@@ -1,21 +1,17 @@
enableScripts: false
logFilters:
## DISABLED_BUILD_SCRIPTS
- code: YN0004
level: discard
## INCOMPATIBLE_OS - fsevents junk
- code: YN0062
level: discard
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
preferAggregateCacheInfo: true
preferInteractive: true
yarnPath: .yarn/releases/yarn-2.4.0.cjs
yarnPath: .yarn/releases/yarn-2.4.1.cjs

View File

@@ -8,13 +8,13 @@ const createBabelConfig = options => {
const { mode, presets = [], plugins = [] } = options;
return {
presets: [
['@babel/preset-typescript', {
[require.resolve('@babel/preset-typescript'), {
allowDeclareFields: true,
}],
['@babel/preset-env', {
[require.resolve('@babel/preset-env'), {
modules: 'commonjs',
useBuiltIns: 'entry',
corejs: '3.8',
corejs: '3.10',
spec: false,
loose: true,
targets: [],
@@ -22,13 +22,13 @@ const createBabelConfig = options => {
...presets,
],
plugins: [
['@babel/plugin-proposal-class-properties', {
[require.resolve('@babel/plugin-proposal-class-properties'), {
loose: true,
}],
'@babel/plugin-transform-jscript',
'babel-plugin-inferno',
'babel-plugin-transform-remove-console',
'common/string.babel-plugin.cjs',
require.resolve('@babel/plugin-transform-jscript'),
require.resolve('babel-plugin-inferno'),
require.resolve('babel-plugin-transform-remove-console'),
require.resolve('common/string.babel-plugin.cjs'),
...plugins,
],
};

View File

@@ -67,7 +67,7 @@ task-lint() {
cd "${base_dir}"
yarn run tsc
echo "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs "${@}"
yarn run eslint packages --ext .js,.cjs,.ts,.tsx "${@}"
echo "tgui: eslint check passed"
}
@@ -89,8 +89,9 @@ task-clean() {
rm -rf .yarn/cache
rm -rf .yarn/unplugged
rm -rf .yarn/webpack
rm -rf .yarn/build-state.yml
rm -rf .yarn/install-state.gz
rm -f .yarn/build-state.yml
rm -f .yarn/install-state.gz
rm -f .yarn/install-target
rm -f .pnp.js
## NPM artifacts
rm -rf **/node_modules

View File

@@ -1,116 +0,0 @@
## Copyright (c) 2020 Aleksej Komarov
## SPDX-License-Identifier: MIT
## Initial set-up
## --------------------------------------------------------
## Normalize current directory
$basedir = Split-Path $MyInvocation.MyCommand.Path
$basedir = Resolve-Path "$($basedir)\.."
Set-Location $basedir
[Environment]::CurrentDirectory = $basedir
## Functions
## --------------------------------------------------------
function yarn {
node.exe ".yarn\releases\yarn-berry.js" @Args
}
function Remove-Quiet {
Remove-Item -ErrorAction SilentlyContinue @Args
}
function task-install {
yarn install
}
## Runs webpack
function task-webpack {
yarn run webpack-cli @Args
}
## Runs a development server
function task-dev-server {
yarn node "packages/tgui-dev-server/index.esm.js" @Args
}
## Run a linter through all packages
function task-eslint {
yarn run eslint packages @Args
Write-Output "tgui: eslint check passed"
}
## Mr. Proper
function task-clean {
## Build artifacts
Remove-Quiet -Recurse -Force "public\.tmp"
Remove-Quiet -Force "public\*.map"
Remove-Quiet -Force "public\*.hot-update.*"
## Yarn artifacts
Remove-Quiet -Recurse -Force ".yarn\cache"
Remove-Quiet -Recurse -Force ".yarn\unplugged"
Remove-Quiet -Recurse -Force ".yarn\build-state.yml"
Remove-Quiet -Recurse -Force ".yarn\install-state.gz"
Remove-Quiet -Force ".pnp.js"
## NPM artifacts
Get-ChildItem -Path "." -Include "node_modules" -Recurse -File:$false | Remove-Item -Recurse -Force
Remove-Quiet -Force "package-lock.json"
}
## Main
## --------------------------------------------------------
if ($Args[0] -eq "--clean") {
task-clean
exit 0
}
if ($Args[0] -eq "--dev") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-dev-server @Rest
exit 0
}
if ($Args[0] -eq "--lint") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint @Rest
exit 0
}
if ($Args[0] -eq "--lint-harder") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint -c ".eslintrc-harder.yml" @Rest
exit 0
}
if ($Args[0] -eq "--fix") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint --fix @Rest
exit 0
}
## Analyze the bundle
if ($Args[0] -eq "--analyze") {
task-install
task-webpack --mode=production --analyze
exit 0
}
## Make a production webpack build
if ($Args.Length -eq 0) {
task-install
task-eslint
task-webpack --mode=production
exit 0
}
## Run webpack with custom flags
task-install
task-webpack @Args

View File

@@ -54,7 +54,7 @@ function task-dev-server {
function task-lint {
yarn run tsc
Write-Output "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs @Args
yarn run eslint packages --ext ".js,.cjs,.ts,.tsx" @Args
Write-Output "tgui: eslint check passed"
}
@@ -72,8 +72,9 @@ function task-clean {
Remove-Quiet -Recurse -Force ".yarn\cache"
Remove-Quiet -Recurse -Force ".yarn\unplugged"
Remove-Quiet -Recurse -Force ".yarn\webpack"
Remove-Quiet -Recurse -Force ".yarn\build-state.yml"
Remove-Quiet -Recurse -Force ".yarn\install-state.gz"
Remove-Quiet -Force ".yarn\build-state.yml"
Remove-Quiet -Force ".yarn\install-state.gz"
Remove-Quiet -Force ".yarn\install-target"
Remove-Quiet -Force ".pnp.js"
## NPM artifacts
Get-ChildItem -Path "." -Include "node_modules" -Recurse -File:$false | Remove-Item -Recurse -Force

17
tgui/global.d.ts vendored
View File

@@ -5,6 +5,23 @@
*/
declare global {
// Webpack asset modules.
// Should match extensions used in webpack config.
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
type ByondType = {
/**
* True if javascript is running in BYOND.

View File

@@ -1,14 +1,14 @@
module.exports = {
roots: ['<rootDir>/packages'],
testMatch: [
'<rootDir>/packages/**/__tests__/*.{js,jsx,ts,tsx}',
'<rootDir>/packages/**/*.{spec,test}.{js,jsx,ts,tsx}',
'<rootDir>/packages/**/__tests__/*.{js,ts,tsx}',
'<rootDir>/packages/**/*.{spec,test}.{js,ts,tsx}',
],
testEnvironment: 'jsdom',
testRunner: require.resolve('jest-circus/runner'),
transform: {
'^.+\\.(js|jsx|ts|tsx|cjs|mjs)$': require.resolve('babel-jest'),
'^.+\\.(js|cjs|ts|tsx)$': require.resolve('babel-jest'),
},
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
moduleFileExtensions: ['js', 'cjs', 'ts', 'tsx', 'json'],
resetMocks: true,
};

View File

@@ -6,38 +6,39 @@
"packages/*"
],
"dependencies": {
"@babel/core": "^7.12.17",
"@babel/eslint-parser": "^7.12.17",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/core": "^7.13.15",
"@babel/eslint-parser": "^7.13.14",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-transform-jscript": "^7.12.13",
"@babel/preset-env": "^7.12.17",
"@babel/preset-typescript": "^7.12.17",
"@types/jest": "^26.0.20",
"@types/jsdom": "^16.2.6",
"@types/node": "^14.14.31",
"@typescript-eslint/parser": "^4.15.1",
"@babel/preset-env": "^7.13.15",
"@babel/preset-typescript": "^7.13.0",
"@types/jest": "^26.0.22",
"@types/jsdom": "^16.2.10",
"@types/node": "^14.14.41",
"@typescript-eslint/parser": "^4.22.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-inferno": "^6.1.1",
"babel-plugin-inferno": "^6.2.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"common": "workspace:*",
"css-loader": "^5.0.2",
"eslint": "^7.20.0",
"eslint-plugin-react": "^7.22.0",
"css-loader": "^5.2.2",
"eslint": "^7.24.0",
"eslint-plugin-radar": "^0.2.1",
"eslint-plugin-react": "^7.23.2",
"file-loader": "^6.2.0",
"inferno": "^7.4.8",
"jest": "^26.6.3",
"jest-circus": "^26.6.3",
"jsdom": "^16.4.0",
"mini-css-extract-plugin": "^1.3.8",
"jsdom": "^16.5.3",
"mini-css-extract-plugin": "^1.4.1",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"typescript": "^4.1.5",
"typescript": "^4.2.4",
"url-loader": "^4.1.1",
"webpack": "^5.23.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0"
"webpack": "^5.33.2",
"webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^4.6.0"
}
}

View File

@@ -0,0 +1,20 @@
import { range, zip } from "./collections";
// Type assertions, these will lint if the types are wrong.
const _zip1: [string, number] = zip(["a"], [1])[0];
describe("range", () => {
test("range(0, 5)", () => {
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
});
});
describe("zip", () => {
test("zip(['a', 'b', 'c'], [1, 2, 3, 4])", () => {
expect(zip(["a", "b", "c"], [1, 2, 3, 4])).toEqual([
["a", 1],
["b", 2],
["c", 3],
]);
});
});

View File

@@ -171,6 +171,15 @@ export const sortBy = (...iterateeFns) => array => {
return mappedArray;
};
export const sort = sortBy();
/**
* Returns a range of numbers from start to end, exclusively.
* For example, range(0, 5) will return [0, 1, 2, 3, 4].
*/
export const range = (start: number, end: number): number[] =>
new Array(end - start).fill(null).map((_, index) => index + start);
/**
* A fast implementation of reduce.
*/
@@ -203,14 +212,17 @@ export const reduce = (reducerFn, initialValue) => array => {
* is determined by the order they occur in the array. The iteratee is
* invoked with one argument: value.
*/
export const uniqBy = iterateeFn => array => {
/* eslint-disable indent */
export const uniqBy = <T extends unknown>(
iterateeFn?: (value: T) => unknown
) => (array: T[]) => {
const { length } = array;
const result = [];
const seen = iterateeFn ? [] : result;
let index = -1;
outer:
while (++index < length) {
let value = array[index];
let value: T | 0 = array[index];
const computed = iterateeFn ? iterateeFn(value) : value;
value = value !== 0 ? value : 0;
if (computed === computed) {
@@ -234,15 +246,20 @@ export const uniqBy = iterateeFn => array => {
}
return result;
};
/* eslint-enable indent */
export const uniq = uniqBy();
type Zip<T extends unknown[][]> = {
[I in keyof T]: T[I] extends (infer U)[] ? U : never;
}[];
/**
* Creates an array of grouped elements, the first of which contains
* the first elements of the given arrays, the second of which contains
* the second elements of the given arrays, and so on.
*
* @returns {any[]}
*/
export const zip = (...arrays) => {
export const zip = <T extends unknown[][]>(...arrays: T): Zip<T> => {
if (arrays.length === 0) {
return;
}

View File

@@ -45,7 +45,7 @@ export const round = (value, precision) => {
m = Math.pow(10, precision);
value *= m;
// sign of the number
sgn = (value > 0) | -(value < 0);
sgn = +(value > 0) | -(value < 0);
// isHalf = value % 1 === 0.5 * sgn;
isHalf = Math.abs(value % 1) >= 0.4999999999854481;
f = Math.floor(value);

View File

@@ -1,73 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
/**
* Helper for conditionally adding/removing classes in React
*
* @param {any[]} classNames
* @return {string}
*/
export const classes = classNames => {
let className = '';
for (let i = 0; i < classNames.length; i++) {
const part = classNames[i];
if (typeof part === 'string') {
className += part + ' ';
}
}
return className;
};
/**
* Normalizes children prop, so that it is always an array of VDom
* elements.
*/
export const normalizeChildren = children => {
if (Array.isArray(children)) {
return children.flat().filter(value => value);
}
if (typeof children === 'object') {
return [children];
}
return [];
};
/**
* Shallowly checks if two objects are different.
* Credit: https://github.com/developit/preact-compat
*/
export const shallowDiffers = (a, b) => {
let i;
for (i in a) {
if (!(i in b)) {
return true;
}
}
for (i in b) {
if (a[i] !== b[i]) {
return true;
}
}
return false;
};
/**
* Default inferno hooks for pure components.
*/
export const pureComponentHooks = {
onComponentShouldUpdate: (lastProps, nextProps) => {
return shallowDiffers(lastProps, nextProps);
},
};
/**
* A helper to determine whether the object is renderable by React.
*/
export const canRender = value => {
return value !== undefined
&& value !== null
&& typeof value !== 'boolean';
};

View File

@@ -11,7 +11,7 @@ export const IMPL_LOCAL_STORAGE = 1;
export const IMPL_INDEXED_DB = 2;
const INDEXED_DB_VERSION = 1;
const INDEXED_DB_NAME = 'tgui-citadel-main';
const INDEXED_DB_NAME = 'tgui-citadel-rp';
const INDEXED_DB_STORE_NAME = 'storage-v1';
const READ_ONLY = 'readonly';

1
tgui/packages/tgfont/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/dist

View File

@@ -0,0 +1,14 @@
/**
* @file
* @copyright 2021 AnturK https://github.com/AnturK
* @license MIT
*/
module.exports = {
name: 'tgfont',
inputDir: './icons',
outputDir: './dist',
fontTypes: ['woff2', 'eot'],
assetTypes: ['css'],
prefix: 'tg',
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 437.4 434.4" style="enable-background:new 0 0 437.4 434.4" xml:space="preserve"><style>.st0{fill:none;stroke:#000;stroke-width:.57;stroke-miterlimit:10}</style><g id="Layer_1"><path class="st0" d="m41.18 306.18 84.43 84.43-19.53 20.8c-11.26 11.99-26.1 17.98-40.93 17.98-14.84 0-29.68-5.99-40.93-17.98-22.52-23.97-22.52-63.2 0-87.18l16.96-18.05zm310.48-156.3L139.65 375.65l-84.43-84.43L269.8 62.7c22.51-23.98 59.34-23.98 81.86 0 22.51 23.97 22.51 63.2 0 87.18z"/><path d="M426.11 88.46c-24.57-26.71-49.15-53.42-73.72-80.13a93.834 93.834 0 0 1-30.93-4.39c-6.96 7.41-6.96 19.53 0 26.94l79.36 84.52c6.96 7.41 18.34 7.41 25.29 0 6.96-7.41 6.96-19.53 0-26.94zM248.07 35.83c13.66 16.01 4.9 48.19-19.57 71.87-24.47 23.68-33.21 29.27-46.88 13.25-13.66-16.01-2.33-26.15 22.14-49.83 24.47-23.67 30.65-51.3 44.31-35.29z" style="stroke-width:.5;fill:none;stroke:#000;stroke-miterlimit:10"/><path class="st0" d="m139.93 375.93-.28-.28m-13.72 15.28-.32-.32"/><path transform="rotate(-45.001 211.427 209.932)" style="stroke:#000;stroke-width:18;stroke-miterlimit:10" d="M191.43-6.09h40v432.04h-40z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 437.4 434.4" style="enable-background:new 0 0 437.4 434.4" xml:space="preserve"><style>.st0{fill:none;stroke:#000;stroke-width:.57;stroke-miterlimit:10}</style><g id="Layer_1"><path class="st0" d="m41.18 306.18 84.43 84.43-19.53 20.8c-11.26 11.99-26.1 17.98-40.93 17.98-14.84 0-29.68-5.99-40.93-17.98-22.52-23.97-22.52-63.2 0-87.18l16.96-18.05zm310.48-156.3L139.65 375.65l-84.43-84.43L269.8 62.7c22.51-23.98 59.34-23.98 81.86 0 22.51 23.97 22.51 63.2 0 87.18z"/><path d="M426.11 88.46c-24.57-26.71-49.15-53.42-73.72-80.13a93.834 93.834 0 0 1-30.93-4.39c-6.96 7.41-6.96 19.53 0 26.94l79.36 84.52c6.96 7.41 18.34 7.41 25.29 0 6.96-7.41 6.96-19.53 0-26.94zM248.07 35.83c13.66 16.01 4.9 48.19-19.57 71.87-24.47 23.68-33.21 29.27-46.88 13.25-13.66-16.01-2.33-26.15 22.14-49.83 24.47-23.67 30.65-51.3 44.31-35.29z" style="stroke-width:.5;fill:none;stroke:#000;stroke-miterlimit:10"/><path class="st0" d="m139.93 375.93-.28-.28m-13.72 15.28-.32-.32"/></g></svg>

After

Width:  |  Height:  |  Size: 1007 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="670" height="512"><path d="M655.714 228H484.286c-7.857 0-14.286 6.412-14.286 14.25v28.5c0 7.838 6.429 14.25 14.286 14.25h171.428c7.857 0 14.286-6.413 14.286-14.25v-28.5c0-7.838-6.429-14.25-14.286-14.25z" style="stroke-width:.89174"/><path d="M384 121.941V128H256V0h6.059a24 24 0 0 1 16.97 7.029l97.941 97.941a24.002 24.002 0 0 1 7.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160zm-135.46 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.54 328c4.686-4.686 11.799-4.201 16.485.485L160.54 368l103.52-103.52c4.686-4.686 12.284-4.686 16.971 0l39.514 39.515z"/></svg>

After

Width:  |  Height:  |  Size: 746 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="670" height="512"><path d="M655.714 228.429h-57.143v-57.143c0-7.857-6.428-14.286-14.285-14.286h-28.572c-7.857 0-14.285 6.429-14.285 14.286v57.143h-57.143c-7.857 0-14.286 6.428-14.286 14.285v28.572c0 7.857 6.429 14.285 14.286 14.285h57.143v57.143c0 7.857 6.428 14.286 14.285 14.286h28.572c7.857 0 14.285-6.429 14.285-14.286v-57.143h57.143c7.857 0 14.286-6.428 14.286-14.285v-28.572c0-7.857-6.429-14.285-14.286-14.285z" style="stroke-width:.892857"/><path d="M384 121.941V128H256V0h6.059a24 24 0 0 1 16.97 7.029l97.941 97.941a24.002 24.002 0 0 1 7.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160zm-135.46 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.54 328c4.686-4.686 11.799-4.201 16.485.485L160.54 368l103.52-103.52c4.686-4.686 12.284-4.686 16.971 0l39.514 39.515z"/></svg>

After

Width:  |  Height:  |  Size: 961 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 425 200" opacity=".33"><path d="M178.004.039H106.8a6.761 6.026 0 0 0-6.761 6.025v187.872a6.761 6.026 0 0 0 6.761 6.025h53.107a6.761 6.026 0 0 0 6.762-6.025V92.392l72.216 104.7a6.761 6.026 0 0 0 5.76 2.87H318.2a6.761 6.026 0 0 0 6.761-6.026V6.064A6.761 6.026 0 0 0 318.2.04h-54.717a6.761 6.026 0 0 0-6.76 6.025v102.62L183.763 2.909a6.761 6.026 0 0 0-5.76-2.87zM4.845 22.109A13.412 12.502 0 0 1 13.478.039h66.118A5.365 5 0 0 1 84.96 5.04v79.88zm415.31 155.782a13.412 12.502 0 0 1-8.633 22.07h-66.118a5.365 5 0 0 1-5.365-5.001v-79.88z"/></svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

After

Width:  |  Height:  |  Size: 770 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="542" height="512"><path d="M215.03 71.05 126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97z"/><path d="M527.714 228H356.286c-7.857 0-14.286 6.412-14.286 14.25v28.5c0 7.838 6.429 14.25 14.286 14.25h171.428c7.857 0 14.286-6.413 14.286-14.25v-28.5c0-7.838-6.429-14.25-14.286-14.25z" style="stroke-width:.89174"/></svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="542" height="512"><path d="M527.714 227.929h-57.143v-57.143c0-7.857-6.428-14.286-14.285-14.286h-28.572c-7.857 0-14.285 6.429-14.285 14.286v57.143h-57.143c-7.857 0-14.286 6.428-14.286 14.285v28.572c0 7.857 6.429 14.285 14.286 14.285h57.143v57.143c0 7.857 6.428 14.286 14.285 14.286h28.572c7.857 0 14.285-6.429 14.285-14.286v-57.143h57.143c7.857 0 14.286-6.428 14.286-14.285v-28.572c0-7.857-6.429-14.285-14.286-14.285z" style="stroke-width:.892857"/><path d="M215.03 71.05 126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 289.742" opacity=".33"><path d="M93.538 0c-18.113 0-34.22 3.112-48.324 9.334-13.965 6.222-24.612 15.072-31.94 26.547C6.084 47.22 2.972 60.631 2.972 76.116c0 10.647 2.725 20.465 8.175 29.453 5.616 8.987 14.039 17.352 25.27 25.094 11.23 7.606 26.507 15.419 45.83 23.438 19.984 8.296 34.849 15.555 44.593 21.776 9.744 6.223 16.761 12.859 21.055 19.91 4.295 7.052 6.442 15.764 6.442 26.134 0 16.178-5.202 28.483-15.606 36.917-10.24 8.435-25.022 12.653-44.345 12.653-14.039 0-25.516-1.66-34.434-4.978-8.918-3.457-16.186-8.711-21.8-15.763-5.616-7.052-10.076-16.661-13.379-28.829H0v56.827c33.857 7.328 63.749 10.994 89.678 10.994 16.02 0 30.72-1.383 44.098-4.148 13.542-2.904 25.104-7.467 34.683-13.69 9.744-6.359 17.34-14.519 22.79-24.474 5.45-10.093 8.175-22.4 8.175-36.917 0-12.997-3.302-24.335-9.908-34.014-6.44-9.818-15.525-18.527-27.251-26.132-11.561-7.604-27.911-15.831-49.051-24.68-17.506-7.19-30.72-13.69-39.638-19.497S54.969 93.756 49.479 87.316c-5.426-6.366-9.658-15.07-9.658-24.887 0-9.264 2.075-17.214 6.223-23.85C57.142 24.18 87.331 36.782 91.12 62.925c4.84 6.775 8.85 16.247 12.03 28.415h20.532v-56c-4.479-5.924-9.955-10.631-15.909-14.373 1.64.479 3.19 1.023 4.639 1.64 6.498 2.626 12.168 7.327 17.007 14.103 4.84 6.775 8.85 16.246 12.03 28.414 0 0 8.48-.129 8.49-.002.417 6.415-1.754 9.453-4.124 12.561-2.417 3.17-5.145 6.79-4.003 13.003 1.508 8.203 10.184 10.597 14.622 9.312-3.318-.5-5.318-1.75-5.318-1.75s1.876.999 5.65-1.36c-3.276.956-10.704-.797-11.8-6.763-.958-5.208.946-7.295 3.4-10.514 2.455-3.22 5.285-6.959 4.685-14.489l.003.002h8.927v-56c-15.072-3.871-27.653-6.36-37.747-7.465C114.279.552 104.046 0 93.537 0zm70.321 17.309.238 40.305c1.318 1.226 2.44 2.278 3.341 3.106 4.84 6.775 8.85 16.246 12.03 28.414H200v-56c-6.677-4.594-19.836-10.473-36.14-15.825zm-28.12 5.605 8.565 17.717c-11.97-6.467-13.847-9.717-8.565-17.717zm22.797 0c2.771 8 1.787 11.25-4.494 17.717l4.494-17.717zm15.222 24.009 8.565 17.716c-11.97-6.466-13.847-9.717-8.565-17.716zm22.797 0c2.771 8 1.787 11.25-4.494 17.716l4.494-17.716zM97.44 49.13l8.565 17.716c-11.97-6.467-13.847-9.717-8.565-17.716zm22.795 0c2.772 7.999 1.788 11.25-4.493 17.716l4.493-17.716z"/></svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,14 @@
/**
* @file
* @copyright 2021 AnturK https://github.com/AnturK
* @license MIT
*/
// Change working directory to project root
process.chdir(__dirname);
// Silently make a dist folder
try {
require('fs').mkdirSync('dist');
}
catch (err) {}

View File

@@ -0,0 +1,11 @@
{
"private": true,
"name": "tgfont",
"version": "1.0.0",
"dependencies": {
"fantasticon": "^1.1.3"
},
"scripts": {
"build": "node mkdist.cjs && fantasticon --config config.cjs"
}
}

View File

@@ -52,7 +52,7 @@ DreamSeeker.getInstancesByPids = async pids => {
}
if (pidsToResolve.length > 0) {
try {
const command = 'netstat -ano | findstr LISTENING';
const command = 'netstat -ano | findstr TCP | findstr 0.0.0.0:0';
const { stdout } = await promisify(exec)(command, {
// Max buffer of 1MB (default is 200KB)
maxBuffer: 1024 * 1024,

View File

@@ -9,6 +9,6 @@
"glob": "^7.1.6",
"source-map": "^0.7.3",
"stacktrace-parser": "^0.1.10",
"ws": "^7.4.3"
"ws": "^7.4.4"
}
}

View File

@@ -8,7 +8,6 @@ import { createLogger } from 'common/logging.js';
import fs from 'fs';
import os from 'os';
import { basename } from 'path';
import { promisify } from 'util';
import { resolveGlob, resolvePath } from './util.js';
import { regQuery } from './winreg.js';
import { DreamSeeker } from './dreamseeker.js';
@@ -68,7 +67,7 @@ export const findCacheRoot = async () => {
const onCacheRootFound = cacheRoot => {
logger.log(`found cache at '${cacheRoot}'`);
// Plant dummy
// Plant a dummy
fs.closeSync(fs.openSync(cacheRoot + '/dummy', 'w'));
};
@@ -93,15 +92,21 @@ export const reloadByondCache = async bundleDir => {
for (let cacheDir of cacheDirs) {
// Clear garbage
const garbage = await resolveGlob(cacheDir, './*.+(bundle|chunk|hot-update).*');
for (let file of garbage) {
await promisify(fs.unlink)(file);
try {
for (let file of garbage) {
fs.unlinkSync(file);
}
// Copy assets
for (let asset of assets) {
const destination = resolvePath(cacheDir, basename(asset));
fs.writeFileSync(destination, fs.readFileSync(asset));
}
logger.log(`copied ${assets.length} files to '${cacheDir}'`);
}
// Copy assets
for (let asset of assets) {
const destination = resolvePath(cacheDir, basename(asset));
await promisify(fs.copyFile)(asset, destination);
catch (err) {
logger.error(`failed copying to '${cacheDir}'`);
logger.error(err);
}
logger.log(`copied ${assets.length} files to '${cacheDir}'`);
}
// Notify dreamseeker
const dss = await dssPromise;

View File

@@ -4,7 +4,7 @@
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.2.6",
"dompurify": "^2.2.7",
"inferno": "^7.4.8",
"tgui": "workspace:*",
"tgui-dev-server": "workspace:*",

View File

@@ -73,19 +73,19 @@ export const setClientTheme = name => {
'output.text-color': COLOR_WHITE_TEXT,
'statwindow.background-color': COLOR_WHITE_DARKBG,
'statwindow.text-color': COLOR_WHITE_TEXT,
'stat.background-color': COLOR_WHITE_BG,
'stat.tab-background-color': COLOR_WHITE_DARKBG,
'stat.text-color': COLOR_WHITE_TEXT,
'stat.tab-text-color': COLOR_WHITE_TEXT,
'stat.prefix-color': COLOR_WHITE_TEXT,
'stat.suffix-color': COLOR_WHITE_TEXT,
'statbrowser.background-color': COLOR_WHITE_BG,
'statbrowser.tab-background-color': COLOR_WHITE_DARKBG,
'statbrowser.text-color': COLOR_WHITE_TEXT,
'statbrowser.tab-text-color': COLOR_WHITE_TEXT,
'statbrowser.prefix-color': COLOR_WHITE_TEXT,
'statbrowser.suffix-color': COLOR_WHITE_TEXT,
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_WHITE_DARKBG,
'saybutton.text-color': COLOR_WHITE_TEXT,
// 'oocbutton.background-color': COLOR_WHITE_DARKBG,
// 'oocbutton.text-color': COLOR_WHITE_TEXT,
// 'mebutton.background-color': COLOR_WHITE_DARKBG,
// 'mebutton.text-color': COLOR_WHITE_TEXT,
'oocbutton.background-color': COLOR_WHITE_DARKBG,
'oocbutton.text-color': COLOR_WHITE_TEXT,
'mebutton.background-color': COLOR_WHITE_DARKBG,
'mebutton.text-color': COLOR_WHITE_TEXT,
'asset_cache_browser.background-color': COLOR_WHITE_DARKBG,
'asset_cache_browser.text-color': COLOR_WHITE_TEXT,
'tooltip.background-color': COLOR_WHITE_BG,
@@ -123,19 +123,19 @@ export const setClientTheme = name => {
'output.text-color': COLOR_DARK_TEXT,
'statwindow.background-color': COLOR_DARK_DARKBG,
'statwindow.text-color': COLOR_DARK_TEXT,
'stat.background-color': COLOR_DARK_DARKBG,
'stat.tab-background-color': COLOR_DARK_BG,
'stat.text-color': COLOR_DARK_TEXT,
'stat.tab-text-color': COLOR_DARK_TEXT,
'stat.prefix-color': COLOR_DARK_TEXT,
'stat.suffix-color': COLOR_DARK_TEXT,
'statbrowser.background-color': COLOR_DARK_DARKBG,
'statbrowser.tab-background-color': COLOR_DARK_BG,
'statbrowser.text-color': COLOR_DARK_TEXT,
'statbrowser.tab-text-color': COLOR_DARK_TEXT,
'statbrowser.prefix-color': COLOR_DARK_TEXT,
'statbrowser.suffix-color': COLOR_DARK_TEXT,
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_DARK_BG,
'saybutton.text-color': COLOR_DARK_TEXT,
// 'oocbutton.background-color': COLOR_DARK_BG,
// 'oocbutton.text-color': COLOR_DARK_TEXT,
// 'mebutton.background-color': COLOR_DARK_BG,
// 'mebutton.text-color': COLOR_DARK_TEXT,
'oocbutton.background-color': COLOR_DARK_BG,
'oocbutton.text-color': COLOR_DARK_TEXT,
'mebutton.background-color': COLOR_DARK_BG,
'mebutton.text-color': COLOR_DARK_TEXT,
'asset_cache_browser.background-color': COLOR_DARK_BG,
'asset_cache_browser.text-color': COLOR_DARK_TEXT,
'tooltip.background-color': COLOR_DARK_BG,
@@ -173,19 +173,19 @@ export const setClientTheme = name => {
'output.text-color': COLOR_DARK_TEXT,
'statwindow.background-color': COLOR_DARK_DARKBG,
'statwindow.text-color': COLOR_DARK_TEXT,
'stat.background-color': COLOR_DARK_DARKBG,
'stat.tab-background-color': COLOR_DARK_BG,
'stat.text-color': COLOR_DARK_TEXT,
'stat.tab-text-color': COLOR_DARK_TEXT,
'stat.prefix-color': COLOR_DARK_TEXT,
'stat.suffix-color': COLOR_DARK_TEXT,
'statbrowser.background-color': COLOR_DARK_DARKBG,
'statbrowser.tab-background-color': COLOR_DARK_BG,
'statbrowser.text-color': COLOR_DARK_TEXT,
'statbrowser.tab-text-color': COLOR_DARK_TEXT,
'statbrowser.prefix-color': COLOR_DARK_TEXT,
'statbrowser.suffix-color': COLOR_DARK_TEXT,
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_DARK_BG,
'saybutton.text-color': COLOR_DARK_TEXT,
// 'oocbutton.background-color': COLOR_DARK_BG,
// 'oocbutton.text-color': COLOR_DARK_TEXT,
// 'mebutton.background-color': COLOR_DARK_BG,
// 'mebutton.text-color': COLOR_DARK_TEXT,
'oocbutton.background-color': COLOR_DARK_BG,
'oocbutton.text-color': COLOR_DARK_TEXT,
'mebutton.background-color': COLOR_DARK_BG,
'mebutton.text-color': COLOR_DARK_TEXT,
'asset_cache_browser.background-color': COLOR_DARK_BG,
'asset_cache_browser.text-color': COLOR_DARK_TEXT,
'tooltip.background-color': COLOR_DARK_BG,

View File

@@ -14,8 +14,4 @@ import './ie8';
import './dom4';
import './css-om';
import './inferno';
// Fetch is required for Webpack HMR
if (module.hot) {
require('whatwg-fetch');
}
import 'unfetch/polyfill';

View File

@@ -3,8 +3,8 @@
"name": "tgui-polyfill",
"version": "4.3.0",
"dependencies": {
"core-js": "^3.9.0",
"regenerator-runtime": "^0.13.7",
"whatwg-fetch": "^3.6.1"
"core-js": "^3.10.1",
"regenerator-runtime": "^0.13.8",
"unfetch": "^4.2.0"
}
}

View File

@@ -1,3 +1,5 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user-secret" class="svg-inline--fa fa-user-secret fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" opacity=".33">
<path fill="currentColor" d="M383.9 308.3l23.9-62.6c4-10.5-3.7-21.7-15-21.7h-58.5c11-18.9 17.8-40.6 17.8-64v-.3c39.2-7.8 64-19.1 64-31.7 0-13.3-27.3-25.1-70.1-33-9.2-32.8-27-65.8-40.6-82.8-9.5-11.9-25.9-15.6-39.5-8.8l-27.6 13.8c-9 4.5-19.6 4.5-28.6 0L182.1 3.4c-13.6-6.8-30-3.1-39.5 8.8-13.5 17-31.4 50-40.6 82.8-42.7 7.9-70 19.7-70 33 0 12.6 24.8 23.9 64 31.7v.3c0 23.4 6.8 45.1 17.8 64H56.3c-11.5 0-19.2 11.7-14.7 22.3l25.8 60.2C27.3 329.8 0 372.7 0 422.4v44.8C0 491.9 20.1 512 44.8 512h358.4c24.7 0 44.8-20.1 44.8-44.8v-44.8c0-48.4-25.8-90.4-64.1-114.1zM176 480l-41.6-192 49.6 32 24 40-32 120zm96 0l-32-120 24-40 49.6-32L272 480zm41.7-298.5c-3.9 11.9-7 24.6-16.5 33.4-10.1 9.3-48 22.4-64-25-2.8-8.4-15.4-8.4-18.3 0-17 50.2-56 32.4-64 25-9.5-8.8-12.7-21.5-16.5-33.4-.8-2.5-6.3-5.7-6.3-5.8v-10.8c28.3 3.6 61 5.8 96 5.8s67.7-2.1 96-5.8v10.8c-.1.1-5.6 3.2-6.4 5.8z"></path>
</svg>
</svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="meteor" class="svg-inline--fa fa-meteor fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity=".33"><path fill="currentColor" d="M511.328,20.8027c-11.60759,38.70264-34.30724,111.70173-61.30311,187.70077,6.99893,2.09372,13.4042,4,18.60653,5.59368a16.06158,16.06158,0,0,1,9.49854,22.906c-22.106,42.29635-82.69047,152.795-142.47819,214.40356-.99984,1.09373-1.99969,2.5-2.99954,3.49995A194.83046,194.83046,0,1,1,57.085,179.41009c.99985-1,2.40588-2,3.49947-3,61.59994-59.90549,171.97367-120.40473,214.37343-142.4982a16.058,16.058,0,0,1,22.90274,9.49988c1.59351,5.09368,3.49947,11.5936,5.5929,18.59351C379.34818,35.00565,452.43074,12.30281,491.12794.70921A16.18325,16.18325,0,0,1,511.328,20.8027ZM319.951,320.00207A127.98041,127.98041,0,1,0,191.97061,448.00046,127.97573,127.97573,0,0,0,319.951,320.00207Zm-127.98041-31.9996a31.9951,31.9951,0,1,1-31.9951-31.9996A31.959,31.959,0,0,1,191.97061,288.00247Zm31.9951,79.999a15.99755,15.99755,0,1,1-15.99755-15.9998A16.04975,16.04975,0,0,1,223.96571,368.00147Z"></path></svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,383 +0,0 @@
/**
* This file provides a clear separation layer between backend updates
* and what state our React app sees.
*
* Sometimes backend can response without a "data" field, but our final
* state will still contain previous "data" because we are merging
* the response with already existing state.
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { perf } from 'common/perf';
import { setupDrag } from './drag';
import { focusMap } from './focus';
import { createLogger } from './logging';
import { resumeRenderer, suspendRenderer } from './renderer';
const logger = createLogger('backend');
export const backendUpdate = state => ({
type: 'backend/update',
payload: state,
});
export const backendSetSharedState = (key, nextState) => ({
type: 'backend/setSharedState',
payload: { key, nextState },
});
export const backendSuspendStart = () => ({
type: 'backend/suspendStart',
});
export const backendSuspendSuccess = () => ({
type: 'backend/suspendSuccess',
payload: {
timestamp: Date.now(),
},
});
const initialState = {
config: {},
data: {},
shared: {},
// Start as suspended
suspended: Date.now(),
suspending: false,
};
export const backendReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === 'backend/update') {
// Merge config
const config = {
...state.config,
...payload.config,
};
// Merge data
const data = {
...state.data,
...payload.static_data,
...payload.data,
};
// Merge shared states
const shared = { ...state.shared };
if (payload.shared) {
for (let key of Object.keys(payload.shared)) {
const value = payload.shared[key];
if (value === '') {
shared[key] = undefined;
}
else {
shared[key] = JSON.parse(value);
}
}
}
// Return new state
return {
...state,
config,
data,
shared,
suspended: false,
};
}
if (type === 'backend/setSharedState') {
const { key, nextState } = payload;
return {
...state,
shared: {
...state.shared,
[key]: nextState,
},
};
}
if (type === 'backend/suspendStart') {
return {
...state,
suspending: true,
};
}
if (type === 'backend/suspendSuccess') {
const { timestamp } = payload;
return {
...state,
data: {},
shared: {},
config: {
...state.config,
title: '',
status: 1,
},
suspending: false,
suspended: timestamp,
};
}
return state;
};
export const backendMiddleware = store => {
let fancyState;
let suspendInterval;
return next => action => {
const { suspended } = selectBackend(store.getState());
const { type, payload } = action;
if (type === 'update') {
store.dispatch(backendUpdate(payload));
return;
}
if (type === 'suspend') {
store.dispatch(backendSuspendSuccess());
return;
}
if (type === 'ping') {
sendMessage({
type: 'pingReply',
});
return;
}
if (type === 'backend/suspendStart' && !suspendInterval) {
logger.log(`suspending (${window.__windowId__})`);
// Keep sending suspend messages until it succeeds.
// It may fail multiple times due to topic rate limiting.
const suspendFn = () => sendMessage({
type: 'suspend',
});
suspendFn();
suspendInterval = setInterval(suspendFn, 2000);
}
if (type === 'backend/suspendSuccess') {
suspendRenderer();
clearInterval(suspendInterval);
suspendInterval = undefined;
Byond.winset(window.__windowId__, {
'is-visible': false,
});
setImmediate(() => focusMap());
}
if (type === 'backend/update') {
const fancy = payload.config?.window?.fancy;
// Initialize fancy state
if (fancyState === undefined) {
fancyState = fancy;
}
// React to changes in fancy
else if (fancyState !== fancy) {
logger.log('changing fancy mode to', fancy);
fancyState = fancy;
Byond.winset(window.__windowId__, {
titlebar: !fancy,
'can-resize': !fancy,
});
}
}
// Resume on incoming update
if (type === 'backend/update' && suspended) {
// Show the payload
logger.log('backend/update', payload);
// Signal renderer that we have resumed
resumeRenderer();
// Setup drag
setupDrag();
// We schedule this for the next tick here because resizing and unhiding
// during the same tick will flash with a white background.
setImmediate(() => {
perf.mark('resume/start');
// Doublecheck if we are not re-suspended.
const { suspended } = selectBackend(store.getState());
if (suspended) {
return;
}
Byond.winset(window.__windowId__, {
'is-visible': true,
});
perf.mark('resume/finish');
if (process.env.NODE_ENV !== 'production') {
logger.log('visible in',
perf.measure('render/finish', 'resume/finish'));
}
});
}
return next(action);
};
};
/**
* Sends a message to /datum/tgui_window.
*/
export const sendMessage = (message = {}) => {
const { payload, ...rest } = message;
const data = {
// Message identifying header
tgui: 1,
window_id: window.__windowId__,
// Message body
...rest,
};
// JSON-encode the payload
if (payload !== null && payload !== undefined) {
data.payload = JSON.stringify(payload);
}
Byond.topic(data);
};
/**
* Sends an action to `ui_act` on `src_object` that this tgui window
* is associated with.
*/
export const sendAct = (action, payload = {}) => {
// Validate that payload is an object
const isObject = typeof payload === 'object'
&& payload !== null
&& !Array.isArray(payload);
if (!isObject) {
logger.error(`Payload for act() must be an object, got this:`, payload);
return;
}
sendMessage({
type: 'act/' + action,
payload,
});
};
/**
* @typedef BackendState
* @type {{
* config: {
* title: string,
* status: number,
* interface: string,
* window: {
* key: string,
* size: [number, number],
* fancy: boolean,
* locked: boolean,
* },
* client: {
* ckey: string,
* address: string,
* computer_id: string,
* },
* user: {
* name: string,
* observer: number,
* },
* },
* data: any,
* shared: any,
* suspending: boolean,
* suspended: boolean,
* }}
*/
/**
* Selects a backend-related slice of Redux state
*
* @return {BackendState}
*/
export const selectBackend = state => state.backend || {};
/**
* A React hook (sort of) for getting tgui state and related functions.
*
* This is supposed to be replaced with a real React Hook, which can only
* be used in functional components.
*
* @return {BackendState & {
* act: sendAct,
* }}
*/
export const useBackend = context => {
const { store } = context;
const state = selectBackend(store.getState());
return {
...state,
act: sendAct,
};
};
/**
* Allocates state on Redux store without sharing it with other clients.
*
* Use it when you want to have a stateful variable in your component
* that persists between renders, but will be forgotten after you close
* the UI.
*
* It is a lot more performant than `setSharedState`.
*
* @param {any} context React context.
* @param {string} key Key which uniquely identifies this state in Redux store.
* @param {any} initialState Initializes your global variable with this value.
*/
export const useLocalState = (context, key, initialState) => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
: initialState;
return [
sharedState,
nextState => {
store.dispatch(backendSetSharedState(key, (
typeof nextState === 'function'
? nextState(sharedState)
: nextState
)));
},
];
};
/**
* Allocates state on Redux store, and **shares** it with other clients
* in the game.
*
* Use it when you want to have a stateful variable in your component
* that persists not only between renders, but also gets pushed to other
* clients that observe this UI.
*
* This makes creation of observable s
*
* @param {any} context React context.
* @param {string} key Key which uniquely identifies this state in Redux store.
* @param {any} initialState Initializes your global variable with this value.
*/
export const useSharedState = (context, key, initialState) => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
: initialState;
return [
sharedState,
nextState => {
sendMessage({
type: 'setSharedState',
key,
value: JSON.stringify(
typeof nextState === 'function'
? nextState(sharedState)
: nextState
) || '',
});
},
];
};

View File

@@ -63,7 +63,7 @@ export class AnimatedNumber extends Component {
if (!isSafeNumber(targetValue)) {
return targetValue || null;
}
let formattedValue = currentValue;
let formattedValue;
// Use custom formatter
if (format) {
formattedValue = format(currentValue);

View File

@@ -1,232 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { createVNode } from 'inferno';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { CSS_COLORS } from '../constants';
/**
* Coverts our rem-like spacing unit into a CSS unit.
*/
export const unit = value => {
if (typeof value === 'string') {
// Transparently convert pixels into rem units
if (value.endsWith('px') && !Byond.IS_LTE_IE8) {
return parseFloat(value) / 12 + 'rem';
}
return value;
}
if (typeof value === 'number') {
if (Byond.IS_LTE_IE8) {
return value * 12 + 'px';
}
return value + 'rem';
}
};
/**
* Same as `unit`, but half the size for integers numbers.
*/
export const halfUnit = value => {
if (typeof value === 'string') {
return unit(value);
}
if (typeof value === 'number') {
return unit(value * 0.5);
}
};
const isColorCode = str => !isColorClass(str);
const isColorClass = str => typeof str === 'string'
&& CSS_COLORS.includes(str);
const mapRawPropTo = attrName => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
style[attrName] = value;
}
};
const mapUnitPropTo = (attrName, unit) => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
style[attrName] = unit(value);
}
};
const mapBooleanPropTo = (attrName, attrValue) => (style, value) => {
if (value) {
style[attrName] = attrValue;
}
};
const mapDirectionalUnitPropTo = (attrName, unit, dirs) => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
for (let i = 0; i < dirs.length; i++) {
style[attrName + '-' + dirs[i]] = unit(value);
}
}
};
const mapColorPropTo = attrName => (style, value) => {
if (isColorCode(value)) {
style[attrName] = value;
}
};
const styleMapperByPropName = {
// Direct mapping
position: mapRawPropTo('position'),
overflow: mapRawPropTo('overflow'),
overflowX: mapRawPropTo('overflow-x'),
overflowY: mapRawPropTo('overflow-y'),
top: mapUnitPropTo('top', unit),
bottom: mapUnitPropTo('bottom', unit),
left: mapUnitPropTo('left', unit),
right: mapUnitPropTo('right', unit),
width: mapUnitPropTo('width', unit),
minWidth: mapUnitPropTo('min-width', unit),
maxWidth: mapUnitPropTo('max-width', unit),
height: mapUnitPropTo('height', unit),
minHeight: mapUnitPropTo('min-height', unit),
maxHeight: mapUnitPropTo('max-height', unit),
fontSize: mapUnitPropTo('font-size', unit),
fontFamily: mapRawPropTo('font-family'),
lineHeight: (style, value) => {
if (typeof value === 'number') {
style['line-height'] = value;
}
else if (typeof value === 'string') {
style['line-height'] = unit(value);
}
},
opacity: mapRawPropTo('opacity'),
textAlign: mapRawPropTo('text-align'),
verticalAlign: mapRawPropTo('vertical-align'),
// Boolean props
inline: mapBooleanPropTo('display', 'inline-block'),
bold: mapBooleanPropTo('font-weight', 'bold'),
italic: mapBooleanPropTo('font-style', 'italic'),
nowrap: mapBooleanPropTo('white-space', 'nowrap'),
// Margins
m: mapDirectionalUnitPropTo('margin', halfUnit, [
'top', 'bottom', 'left', 'right',
]),
mx: mapDirectionalUnitPropTo('margin', halfUnit, [
'left', 'right',
]),
my: mapDirectionalUnitPropTo('margin', halfUnit, [
'top', 'bottom',
]),
mt: mapUnitPropTo('margin-top', halfUnit),
mb: mapUnitPropTo('margin-bottom', halfUnit),
ml: mapUnitPropTo('margin-left', halfUnit),
mr: mapUnitPropTo('margin-right', halfUnit),
// Margins
p: mapDirectionalUnitPropTo('padding', halfUnit, [
'top', 'bottom', 'left', 'right',
]),
px: mapDirectionalUnitPropTo('padding', halfUnit, [
'left', 'right',
]),
py: mapDirectionalUnitPropTo('padding', halfUnit, [
'top', 'bottom',
]),
pt: mapUnitPropTo('padding-top', halfUnit),
pb: mapUnitPropTo('padding-bottom', halfUnit),
pl: mapUnitPropTo('padding-left', halfUnit),
pr: mapUnitPropTo('padding-right', halfUnit),
// Color props
color: mapColorPropTo('color'),
textColor: mapColorPropTo('color'),
backgroundColor: mapColorPropTo('background-color'),
// Utility props
fillPositionedParent: (style, value) => {
if (value) {
style['position'] = 'absolute';
style['top'] = 0;
style['bottom'] = 0;
style['left'] = 0;
style['right'] = 0;
}
},
};
export const computeBoxProps = props => {
const computedProps = {};
const computedStyles = {};
// Compute props
for (let propName of Object.keys(props)) {
if (propName === 'style') {
continue;
}
// IE8: onclick workaround
if (Byond.IS_LTE_IE8 && propName === 'onClick') {
computedProps.onclick = props[propName];
continue;
}
const propValue = props[propName];
const mapPropToStyle = styleMapperByPropName[propName];
if (mapPropToStyle) {
mapPropToStyle(computedStyles, propValue);
}
else {
computedProps[propName] = propValue;
}
}
// Concatenate styles
let style = '';
for (let attrName of Object.keys(computedStyles)) {
const attrValue = computedStyles[attrName];
style += attrName + ':' + attrValue + ';';
}
if (props.style) {
for (let attrName of Object.keys(props.style)) {
const attrValue = props.style[attrName];
style += attrName + ':' + attrValue + ';';
}
}
if (style.length > 0) {
computedProps.style = style;
}
return computedProps;
};
export const computeBoxClassName = props => {
const color = props.textColor || props.color;
const backgroundColor = props.backgroundColor;
return classes([
isColorClass(color) && 'color-' + color,
isColorClass(backgroundColor) && 'color-bg-' + backgroundColor,
]);
};
export const Box = props => {
const {
as = 'div',
className,
children,
...rest
} = props;
// Render props
if (typeof children === 'function') {
return children(computeBoxProps(props));
}
const computedClassName = typeof className === 'string'
? className + ' ' + computeBoxClassName(rest)
: computeBoxClassName(rest);
const computedProps = computeBoxProps(rest);
// Render a wrapper element
return createVNode(
VNodeFlags.HtmlElement,
as,
computedClassName,
children,
ChildFlags.UnknownChildren,
computedProps);
};
Box.defaultHooks = pureComponentHooks;

View File

@@ -1,88 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { Box, unit } from './Box';
export const computeFlexProps = props => {
const {
className,
direction,
wrap,
align,
justify,
inline,
spacing = 0,
...rest
} = props;
return {
className: classes([
'Flex',
Byond.IS_LTE_IE10 && (
direction === 'column'
? 'Flex--iefix--column'
: 'Flex--iefix'
),
inline && 'Flex--inline',
spacing > 0 && 'Flex--spacing--' + spacing,
className,
]),
style: {
...rest.style,
'flex-direction': direction,
'flex-wrap': wrap,
'align-items': align,
'justify-content': justify,
},
...rest,
};
};
export const Flex = props => (
<Box {...computeFlexProps(props)} />
);
Flex.defaultHooks = pureComponentHooks;
export const computeFlexItemProps = props => {
const {
className,
style,
grow,
order,
shrink,
// IE11: Always set basis to specified width, which fixes certain
// bugs when rendering tables inside the flex.
basis = props.width,
align,
...rest
} = props;
return {
className: classes([
'Flex__item',
Byond.IS_LTE_IE10 && 'Flex__item--iefix',
Byond.IS_LTE_IE10 && grow > 0 && 'Flex__item--iefix--grow',
className,
]),
style: {
...style,
'flex-grow': grow,
'flex-shrink': shrink,
'flex-basis': unit(basis),
'order': order,
'align-self': align,
},
...rest,
};
};
export const FlexItem = props => (
<Box {...computeFlexItemProps(props)} />
);
FlexItem.defaultHooks = pureComponentHooks;
Flex.Item = FlexItem;

View File

@@ -8,11 +8,11 @@ import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { Box, BoxProps, unit } from './Box';
export interface FlexProps extends BoxProps {
direction: string | BooleanLike;
wrap: string | BooleanLike;
align: string | BooleanLike;
justify: string | BooleanLike;
inline: BooleanLike;
direction?: string | BooleanLike;
wrap?: string | BooleanLike;
align?: string | BooleanLike;
justify?: string | BooleanLike;
inline?: BooleanLike;
}
export const computeFlexProps = (props: FlexProps) => {

View File

@@ -28,17 +28,23 @@ export const Icon = props => {
if (typeof rotation === 'number') {
style['transform'] = `rotate(${rotation}deg)`;
}
const faRegular = FA_OUTLINE_REGEX.test(name);
const faName = name.replace(FA_OUTLINE_REGEX, '');
let iconClass = "";
if (name.startsWith("tg-")) {
// tgfont icon
iconClass = name;
} else {
// font awesome icon
const faRegular = FA_OUTLINE_REGEX.test(name);
const faName = name.replace(FA_OUTLINE_REGEX, '');
iconClass = (faRegular ? 'far ' : 'fas ') + 'fa-'+ faName + (spin ? " fa-spin" : "");
}
return (
<Box
as="i"
className={classes([
'Icon',
className,
faRegular ? 'far' : 'fas',
'fa-' + faName,
spin && 'fa-spin',
iconClass,
])}
style={style}
{...rest} />

View File

@@ -1,92 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { Box, unit } from './Box';
import { Divider } from './Divider';
export const LabeledList = props => {
const { children } = props;
return (
<table className="LabeledList">
{children}
</table>
);
};
LabeledList.defaultHooks = pureComponentHooks;
export const LabeledListItem = props => {
const {
className,
label,
labelColor = 'label',
color,
textAlign,
buttons,
content,
children,
} = props;
return (
<tr
className={classes([
'LabeledList__row',
className,
])}>
<Box
as="td"
color={labelColor}
className={classes([
'LabeledList__cell',
'LabeledList__label',
])}>
{label ? label + ':' : null}
</Box>
<Box
as="td"
color={color}
textAlign={textAlign}
className={classes([
'LabeledList__cell',
'LabeledList__content',
])}
colSpan={buttons ? undefined : 2}>
{content}
{children}
</Box>
{buttons && (
<td className="LabeledList__cell LabeledList__buttons">
{buttons}
</td>
)}
</tr>
);
};
LabeledListItem.defaultHooks = pureComponentHooks;
export const LabeledListDivider = props => {
const padding = props.size
? unit(Math.max(0, props.size - 1))
: 0;
return (
<tr className="LabeledList__row">
<td
colSpan={3}
style={{
'padding-top': padding,
'padding-bottom': padding,
}}>
<Divider />
</td>
</tr>
);
};
LabeledListDivider.defaultHooks = pureComponentHooks;
LabeledList.Item = LabeledListItem;
LabeledList.Divider = LabeledListDivider;

View File

@@ -10,7 +10,7 @@ import { Box, unit } from './Box';
import { Divider } from './Divider';
type LabeledListProps = {
children: InfernoNode;
children?: any;
};
export const LabeledList = (props: LabeledListProps) => {

View File

@@ -1,78 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { canRender, classes } from 'common/react';
import { Component, createRef } from 'inferno';
import { addScrollableNode, removeScrollableNode } from '../events';
import { computeBoxClassName, computeBoxProps } from './Box';
export class Section extends Component {
constructor(props) {
super(props);
this.ref = createRef();
this.scrollable = props.scrollable;
}
componentDidMount() {
if (this.scrollable) {
addScrollableNode(this.ref.current);
}
}
componentWillUnmount() {
if (this.scrollable) {
removeScrollableNode(this.ref.current);
}
}
render() {
const {
className,
title,
level = 1,
buttons,
fill,
fitted,
scrollable,
children,
...rest
} = this.props;
const hasTitle = canRender(title) || canRender(buttons);
const hasContent = canRender(children);
return (
<div
ref={this.ref}
className={classes([
'Section',
'Section--level--' + level,
Byond.IS_LTE_IE8 && 'Section--iefix',
fill && 'Section--fill',
fitted && 'Section--fitted',
scrollable && 'Section--scrollable',
className,
...computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
{hasTitle && (
<div className="Section__title">
<span className="Section__titleText">
{title}
</span>
<div className="Section__buttons">
{buttons}
</div>
</div>
)}
{fitted && children
|| hasContent && (
<div className="Section__content">
{children}
</div>
)}
</div>
);
}
}

View File

@@ -1,4 +1,4 @@
import { toFixed } from 'common/math';
import { formatTime } from '../format';
import { Component } from 'inferno';
// AnimatedNumber Copypaste
@@ -57,13 +57,7 @@ export class TimeDisplay extends Component {
if (!isSafeNumber(val)) {
return this.state.value || null;
}
// THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER
// HH:MM:SS
// 00:02:13
const seconds = toFixed(Math.floor((val/10) % 60)).padStart(2, "0");
const minutes = toFixed(Math.floor((val/(10*60)) % 60)).padStart(2, "0");
const hours = toFixed(Math.floor((val/(10*60*60)) % 24)).padStart(2, "0");
const formattedValue = `${hours}:${minutes}:${seconds}`;
return formattedValue;
return formatTime(val);
}
}

View File

@@ -148,7 +148,7 @@ window.addEventListener('beforeunload', e => {
const keyHeldByCode = {};
class KeyEvent {
export class KeyEvent {
constructor(e, type, repeat) {
this.event = e;
this.type = type;

View File

@@ -105,7 +105,7 @@ export const formatMoney = (value, precision = 0) => {
*/
export const formatDb = value => {
const db = 20 * Math.log(value) / Math.log(10);
const sign = db >= 0 ? '+' : db < 0 ? '' : '';
const sign = db >= 0 ? '+' : '';
let formatted = Math.abs(db);
if (formatted === Infinity) {
formatted = 'Inf';
@@ -169,3 +169,30 @@ export const formatSiBaseTenUnit = (
);
return finalString.trim();
};
/**
* Formats decisecond count into HH::MM::SS display by default
* "short" format does not pad and adds hms suffixes
*/
export const formatTime = (val, formatType) => {
// THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER
// HH:MM:SS
// 00:02:13
const seconds = toFixed(Math.floor((val/10) % 60));
const minutes = toFixed(Math.floor((val/(10*60)) % 60));
const hours = toFixed(Math.floor((val/(10*60*60)) % 24));
switch (formatType) {
case "short": {
const hours_truncated = hours > 0 ? `${hours}h` : "";
const minutes_truncated = minutes > 0 ? `${minutes}m` : "";
const seconds_truncated = seconds > 0 ? `${seconds}s` : "";
return `${hours_truncated}${minutes_truncated}${seconds_truncated}`;
}
default: {
const seconds_padded = seconds.padStart(2, "0");
const minutes_padded = minutes.padStart(2, "0");
const hours_padded = hours.padStart(2, "0");
return `${hours_padded}:${minutes_padded}:${seconds_padded}`;
}
}
};

View File

@@ -4,34 +4,37 @@
* @license MIT
*/
import { KEY_CTRL, KEY_ENTER, KEY_ESCAPE, KEY_F, KEY_F5, KEY_R, KEY_SHIFT, KEY_SPACE, KEY_TAB } from 'common/keycodes';
import { globalEvents } from './events';
import * as keycodes from 'common/keycodes';
import { globalEvents, KeyEvent } from './events';
import { createLogger } from './logging';
const logger = createLogger('hotkeys');
// BYOND macros, in `key: command` format.
const byondMacros = {};
const byondMacros: Record<string, string> = {};
// Array of acquired keys, which will not be sent to BYOND.
// Default set of acquired keys, which will not be sent to BYOND.
const hotKeysAcquired = [
// Default set of acquired keys
KEY_ESCAPE,
KEY_ENTER,
KEY_SPACE,
KEY_TAB,
KEY_CTRL,
KEY_SHIFT,
KEY_F5,
keycodes.KEY_ESCAPE,
keycodes.KEY_ENTER,
keycodes.KEY_SPACE,
keycodes.KEY_TAB,
keycodes.KEY_CTRL,
keycodes.KEY_SHIFT,
keycodes.KEY_UP,
keycodes.KEY_DOWN,
keycodes.KEY_LEFT,
keycodes.KEY_RIGHT,
keycodes.KEY_F5,
];
// State of passed-through keys.
const keyState = {};
const keyState: Record<string, boolean> = {};
/**
* Converts a browser keycode to BYOND keycode.
*/
const keyCodeToByond = keyCode => {
const keyCodeToByond = (keyCode: number) => {
if (keyCode === 16) return 'Shift';
if (keyCode === 17) return 'Ctrl';
if (keyCode === 18) return 'Alt';
@@ -63,14 +66,15 @@ const keyCodeToByond = keyCode => {
* Keyboard passthrough logic. This allows you to keep doing things
* in game while the browser window is focused.
*/
const handlePassthrough = key => {
const handlePassthrough = (key: KeyEvent) => {
const keyString = String(key);
// In addition to F5, support reloading with Ctrl+R and Ctrl+F5
if (key.ctrl && (key.code === KEY_F5 || key.code === KEY_R)) {
if (keyString === 'Ctrl+F5' || keyString === 'Ctrl+R') {
location.reload();
return;
}
// Prevent passthrough on Ctrl+F
if (key.ctrl && key.code === KEY_F) {
if (keyString === 'Ctrl+F') {
return;
}
// NOTE: Alt modifier is pretty bad and sticky in IE11.
@@ -109,14 +113,14 @@ const handlePassthrough = key => {
* Acquires a lock on the hotkey, which prevents it from being
* passed through to BYOND.
*/
export const acquireHotKey = keyCode => {
export const acquireHotKey = (keyCode: number) => {
hotKeysAcquired.push(keyCode);
};
/**
* Makes the hotkey available to BYOND again.
*/
export const releaseHotKey = keyCode => {
export const releaseHotKey = (keyCode: number) => {
const index = hotKeysAcquired.indexOf(keyCode);
if (index >= 0) {
hotKeysAcquired.splice(index, 1);
@@ -133,25 +137,33 @@ export const releaseHeldKeys = () => {
}
};
type ByondSkinMacro = {
command: string;
name: string;
};
export const setupHotKeys = () => {
// Read macros
Byond.winget('default.*').then(data => {
Byond.winget('default.*').then((data: Record<string, string>) => {
// Group each macro by ref
const groupedByRef = {};
const groupedByRef: Record<string, ByondSkinMacro> = {};
for (let key of Object.keys(data)) {
const keyPath = key.split('.');
const ref = keyPath[1];
const prop = keyPath[2];
if (ref && prop) {
// This piece of code imperatively adds each property to a
// ByondSkinMacro object in the order we meet it, which is hard
// to express safely in typescript.
if (!groupedByRef[ref]) {
groupedByRef[ref] = {};
groupedByRef[ref] = {} as any;
}
groupedByRef[ref][prop] = data[key];
}
}
// Insert macros
const escapedQuotRegex = /\\"/g;
const unescape = str => str
const unescape = (str: string) => str
.substring(1, str.length - 1)
.replace(escapedQuotRegex, '"');
for (let ref of Object.keys(groupedByRef)) {
@@ -165,7 +177,7 @@ export const setupHotKeys = () => {
globalEvents.on('window-blur', () => {
releaseHeldKeys();
});
globalEvents.on('key', key => {
globalEvents.on('key', (key: KeyEvent) => {
handlePassthrough(key);
});
};

View File

@@ -16,6 +16,7 @@ import './styles/themes/ntos.scss';
import './styles/themes/paper.scss';
import './styles/themes/retro.scss';
import './styles/themes/syndicate.scss';
import './styles/themes/wizard.scss';
import { perf } from 'common/perf';
import { setupHotReloading } from 'tgui-dev-server/link/client';

View File

@@ -13,17 +13,17 @@ export const captureExternalLinks = () => {
/** @type {HTMLElement} */
let target = e.target;
// Recurse down the tree to find a valid link
while (target && target !== document.body) {
while (true) {
// Reached the end, bail.
if (!target || target === document.body) {
return;
}
const tagName = String(target.tagName).toLowerCase();
if (tagName === 'a') {
break;
}
target = target.parentElement;
}
// Not a link, do nothing.
if (!target) {
return;
}
const hrefAttr = target.getAttribute('href') || '';
// Leave BYOND links alone
const isByondLink = hrefAttr.charAt(0) === '?'

View File

@@ -4,10 +4,12 @@
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.2.6",
"dateformat": "^4.5.1",
"dompurify": "^2.2.7",
"inferno": "^7.4.8",
"inferno-vnode-flags": "^7.4.8",
"marked": "^2.0.0",
"js-yaml": "^4.1.0",
"marked": "^2.0.3",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
}

View File

@@ -8,7 +8,7 @@ import { selectBackend } from './backend';
import { selectDebug } from './debug/selectors';
import { Window } from './layouts';
const requireInterface = require.context('./interfaces', false, /\.js$/);
const requireInterface = require.context('./interfaces');
const routingError = (type, name) => () => {
return (
@@ -47,15 +47,27 @@ export const getRoutedComponent = store => {
}
}
const name = config?.interface;
const interfacePathBuilders = [
name => `./${name}.tsx`,
name => `./${name}.js`,
name => `./${name}/index.tsx`,
name => `./${name}/index.js`,
];
let esModule;
try {
esModule = requireInterface(`./${name}.js`);
}
catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
return routingError('notFound', name);
while (!esModule && interfacePathBuilders.length > 0) {
const interfacePathBuilder = interfacePathBuilders.shift();
const interfacePath = interfacePathBuilder(name);
try {
esModule = requireInterface(interfacePath);
}
throw err;
catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err;
}
}
}
if (!esModule) {
return routingError('notFound', name);
}
const Component = esModule[name];
if (!Component) {

View File

@@ -0,0 +1,10 @@
@use '../colors.scss';
a {
&:link, &:visited {
color: colors.$blue;
}
&:hover, &:active {
color: colors.$primary;
}
}

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