Merge branch 'master' into Xenoarch-Merge

This commit is contained in:
SirSmith148 2025-10-01 01:08:14 -07:00
commit 96e62b681e
8197 changed files with 1055481 additions and 119446 deletions

View File

@ -127,6 +127,7 @@ csharp_indent_braces = false
#csharp_indent_case_contents_when_block = true
#csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
xmldoc_indent_text = zeroindent
# Space preferences
csharp_space_after_cast = false

4
.github/CODEOWNERS vendored
View File

@ -13,8 +13,8 @@
/Resources/*.yml @DeltaV-Station/yaml-maintainers
/Resources/**/*.yml @DeltaV-Station/yaml-maintainers
# Sprites - Art Director
/Resources/Textures/ @AftrLite
# Sprites - Direction in lack of art director
/Resources/Textures/ @DeltaV-Station/direction
# Lobby art and music - automatically direction issues since its immediately visible to players
/Resources/Audio/Lobby/ @DeltaV-Station/direction

View File

@ -14,7 +14,7 @@
Small fixes/refactors are exempt. -->
## Requirements
<!-- Confirm the following by placing an X in the brackets [X]: -->
<!-- Confirm the following by placing an X in the brackets: [X] -->
- [ ] I have tested all added content and changes.
- [ ] I have added media to this PR or it does not require an ingame showcase.
<!-- You should understand that not following the above may get your PR closed at maintainers discretion -->

View File

@ -17,7 +17,7 @@ jobs:
contents: write
steps:
- name: Checkout Master
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
token: ${{ secrets.BOT_TOKEN }}
ref: "${{ vars.CHANGELOG_BRANCH }}"
@ -29,9 +29,9 @@ jobs:
shell: bash
- name: Setup Node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 22.x
- name: Install Dependencies
run: |

View File

@ -12,7 +12,7 @@ jobs:
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: "Thank you for contributing to the Space Station 14 repository. Unfortunately, it looks like you submitted your pull request from the master branch. We suggest you follow [our git usage documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html) \n\n You can move your current work from the master branch to another branch by doing `git branch <branch_name` and resetting the master branch."
comment: "Thank you for your contribution! It appears you created a PR from your master branch, this is [something you should avoid doing](https://jmeridth.com/posts/do-not-issue-pull-requests-from-your-master-branch/), and thus this PR has been automatically closed. \n \n We suggest you follow [our git usage documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html). \n \n You can move your current work from the master branch to another branch by following [these commands](https://ohshitgit.com/#accidental-commit-master). And then you may recreate your PR using the new branch."
# If you prefer to just comment on the pr and not close it, uncomment the bellow and comment the above

View File

@ -2,6 +2,7 @@ name: Publish
concurrency:
group: publish
cancel-in-progress: true
on:
workflow_dispatch:
@ -48,12 +49,14 @@ jobs:
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
- name: Publish changelog (Discord)
continue-on-error: true
run: Tools/actions_changelogs_since_last_run.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
- name: Publish changelog (RSS)
continue-on-error: true
run: Tools/actions_changelog_rss.py
env:
CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}

View File

@ -5,7 +5,7 @@
files: "(_DV|DeltaV)/"
repos:
- repo: "https://github.com/pre-commit/pre-commit-hooks"
rev: "v5.0.0"
rev: "v6.0.0"
hooks:
# File checkers
- id: check-yaml

View File

@ -1,6 +1,11 @@
{
"recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig"
"editorconfig.editorconfig",
"ertanic.robust-lsp",
"GitHub.vscode-pull-request-github",
"eamodio.gitlens",
"macabeus.vscode-fluent",
"redhat.vscode-yaml"
]
}

95
.vscode/launch.json vendored
View File

@ -4,6 +4,17 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "YAML Linter",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-yaml-linter",
"program": "${workspaceFolder}/bin/Content.YAMLLinter/Content.YAMLLinter.dll",
"cwd": "${workspaceFolder}/Content.YAMLLinter",
"console": "internalConsole",
"stopAtEntry": false
},
// Client configurations
{
"name": "Client",
"type": "coreclr",
@ -14,7 +25,28 @@
"stopAtEntry": false
},
{
"name": "Client (Compatibility renderer)",
"name": "Client - Tools",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Tools",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
"args": [],
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Client - Release",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Release",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
"args": [],
"console": "internalConsole",
"stopAtEntry": false
},
// Compatibility renderer client configurations
{
"name": "Client - (Compatibility renderer)",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
@ -22,6 +54,27 @@
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Client - Tools - (Compatibility renderer)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Tools",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
"args": ["--cvar display.compat=true"],
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Client - Release - (Compatibility renderer)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Release",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
"args": ["--cvar display.compat=true"],
"console": "internalConsole",
"stopAtEntry": false
},
// Server configurations
{
"name": "Server",
"type": "coreclr",
@ -32,15 +85,25 @@
"stopAtEntry": false
},
{
"name": "YAML Linter",
"name": "Server - Tools",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-yaml-linter",
"program": "${workspaceFolder}/bin/Content.YAMLLinter/Content.YAMLLinter.dll",
"cwd": "${workspaceFolder}/Content.YAMLLinter",
"console": "internalConsole",
"preLaunchTask": "build - Tools",
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
"args": [""],
"console": "integratedTerminal",
"stopAtEntry": false
}
},
{
"name": "Server - Release",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Release",
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
"args": [""],
"console": "integratedTerminal",
"stopAtEntry": false
},
],
"compounds": [
{
@ -50,6 +113,22 @@
"Client"
],
"preLaunchTask": "build"
},
{
"name": "Server/Client - Tools",
"configurations": [
"Server - Tools",
"Client - Tools"
],
"preLaunchTask": "build - Tools"
},
{
"name": "Server/Client - Release",
"configurations": [
"Server - Release",
"Client - Release"
],
"preLaunchTask": "build - Release"
}
]
}
}

40
.vscode/tasks.json vendored
View File

@ -21,6 +21,46 @@
},
"problemMatcher": "$msCompile"
},
{
"label": "build - Tools",
"command": "dotnet",
"type": "shell",
"args": [
"build",
"--configuration",
"Tools",
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
"/consoleloggerparameters:'ForceNoAlign;NoSummary'", // Do not generate summary otherwise it leads to duplicate errors in Problems panel
],
"group": {
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "build - Release",
"command": "dotnet",
"type": "shell",
"args": [
"build",
"--configuration",
"Release",
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
"/consoleloggerparameters:'ForceNoAlign;NoSummary'", // Do not generate summary otherwise it leads to duplicate errors in Problems panel
],
"group": {
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "build-yaml-linter",
"command": "dotnet",

View File

@ -9,16 +9,16 @@ The normal operating procedure. ''There is no currently known threat to the ship
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > Department Heads > Supervisory Roles
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Open carrying of long arms disallowed. Standard stab-and bullet-resistant equipment recommended for Security personnel.
| style="border: 1px solid #000000;" | Open carrying of long arms and shields disallowed for Security personnel. Standard stab-and bullet-resistant equipment recommended for Security personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Report to supervisor for general orders.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | Secure areas unbolted and accessible to all authorized personnel. High Security Areas and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be locked as needed.
| style="border: 1px solid #000000;" | Secure areas unbolted and accessible to all authorized personnel. HSAs and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be bolted as needed.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Report to supervisor for general orders.
| style="border: 1px solid #000000;" | Binary suit sensors recommended at a minimum.
|-
| style="border: 1px solid #000000;" | Engineering
| style="border: 1px solid #000000;" | Report to supervisor for general orders.
@ -38,20 +38,19 @@ Elevated alert status. ''There is an ongoing, known, or suspected security threa
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > [[Head of Security|Head of Station Security]] > Department Heads > Supervisory Roles
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Open carry of weaponry permitted for authorised entities. Body armour and helmets mandatory for Security personnel, permitted for distribution to authorised personnel.
| style="border: 1px solid #000000;" | Open carry of weaponry permitted for all authorised entities. Body armour mandatory for Security personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Report to supervisor for general orders. Searches may be performed at the discretion of security personnel and without a warrant. Open carry disallowed for non-security personnel.
| style="border: 1px solid #000000;" | Report to supervisor for general orders. Searching individuals suspected of committing a crime is encouraged.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | Secure areas unlocked unless affected by known threats. Access to [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] locked.
| style="border: 1px solid #000000;" | Secure areas unbolted unless affected by known threats. Bolting [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] recommended.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Binary suit sensors recommended at a minimum. EVA suits recommended for emergency responders, functioning internals and PPE heavily advised.
|-
| style="border: 1px solid #000000;" | Engineering
| style="border: 1px solid #000000;" | EVA suits recommended for engineers. Make emergency internals easily accessible.
| style="border: 1px solid #000000;" | EVA suits recommended for engineers.
|-
| style="border: 1px solid #000000;" | Rules of Engagement
| style="border: 1px solid #000000;" | Engage with minimal force required; non-lethal weapons may be used freely based on threat and compliance.
@ -68,19 +67,19 @@ Emergency alert status. ''There are multiple major emergency situations ongoing,
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > Security Personnel > Department Heads > Supervisory Roles
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Issuing of lethal weapons heavily recommended. Body armour and helmets mandatory for Security personnel, permitted for distribution to authorised personnel.
| style="border: 1px solid #000000;" | Issuing of lethal weapons heavily recommended. Body armour and helmets mandatory for Security personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Report to supervisor for general orders. Searches and departmental raids may be performed at the discretion of security personnel and without a warrant. Open carry disallowed for non-security personnel.
| style="border: 1px solid #000000;" | Report to supervisor for general orders. Protection of personnel prioritized over body and department searches.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | All HSAs should remain locked and under guard. Secure areas unlocked unless affected by known threats.
| style="border: 1px solid #000000;" | All [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] recommended to be bolted and under guard. Secure areas unbolted unless affected by known threats.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Full suit sensors mandatory for observation and safety. EVA suits and functioning internals heavily advised for emergency responders.
| style="border: 1px solid #000000;" | Full suit sensors heavily advised. EVA suits and functioning internals heavily advised for emergency responders.
|-
| style="border: 1px solid #000000;" | Engineering
| style="border: 1px solid #000000;" | EVA suits heavily advised for engineers. Distribute emergency internals to crew.
| style="border: 1px solid #000000;" | EVA suits heavily advised for engineers. Make emergency internals easily accessible.
|-
| style="border: 1px solid #000000;" | Rules of Engagement
| style="border: 1px solid #000000;" | Engage with adequate force to safely subdue suspects, dependent on threat and compliance. Lethal force is permitted to effect arrests against evasive targets.
@ -97,19 +96,19 @@ Elevated alert status. ''There is a serious viral outbreak, ongoing major death
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > [[Chief Medical Officer]] > Medical Personnel > Department Heads > Supervisory Roles
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Issuing of lethal weapons disadvised. Standard stab-and bullet-resistant equipment recommended for Security personnel. Basic PPE highly recommended for all personnel, L3 biohazard gear advised.
| style="border: 1px solid #000000;" | Issuing of lethal weapons disadvised. Standard stab-and bullet-resistant equipment recommended for Security personnel. Biohazard gear advised.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Report to supervisor for orders. Non-medical personnel should ensure they do not interfere with medical staff, may assist where applicable, obey all applicable orders from medical personnel. Security should seek to aid medical staff in enforcement of quarantine procedures or medical treatment as necessary.
| style="border: 1px solid #000000;" | Report to supervisor for orders. Non-medical personnel should ensure they do not interfere with medical staff. Security should seek to aid medical staff where possible.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | Secure areas unlocked and accessible to all authorized personnel. High Security Areas and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be locked as needed.
| style="border: 1px solid #000000;" | Secure areas unbolted. [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be bolted as needed.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Report to supervisor for orders. Full suit sensors heavily advised. EVA suits recommended for emergency responders, functioning internals and PPE heavily advised.
|-
| style="border: 1px solid #000000;" | Engineering
| style="border: 1px solid #000000;" | Standard practice.
| style="border: 1px solid #000000;" | Report to supervisor for general orders.
|-
| style="border: 1px solid #000000;" | Rules of Engagement
| style="border: 1px solid #000000;" | Engage with minimal force required; prioritize de-escalation whenever possible.
@ -129,10 +128,10 @@ Elevated alert status. ''There is a major issue with the atmospheric system, the
| style="border: 1px solid #000000;" | Issuing of lethal weapons disadvised. EVA protection heavily advised for all personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Report to supervisor for orders. Non-engineering personnel advised to evacuate affected areas and obey any relevant instructions from engineering personnel. Security should seek to aid engineering staff where possible.
| style="border: 1px solid #000000;" | Report to supervisor for orders. Non-engineering personnel advised to evacuate affected areas. Security should seek to aid engineering staff where possible.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | Secure areas unlocked and accessible to all authorized personnel. High Security Areas and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be locked as needed.
| style="border: 1px solid #000000;" | Secure areas unbolted. [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be bolted as needed.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Full suit sensors heavily advised. EVA suits and functioning internals recommended for emergency responders.
@ -155,13 +154,13 @@ Elevated alert status. ''The station is suffering dangerously high levels of gli
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > [[Mystagogue]] > Epistemics Personnel > Department Heads > Supervisory Roles
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Open carry of weaponry permitted for authorised entities. Standard stab- and bullet-resistant equipment recommended for Security personnel. Psionic insulation recommended for all personnel.
| style="border: 1px solid #000000;" | Psionic insulation recommended for all personnel. Open carry of weaponry permitted for authorised entities. Standard stab- and bullet-resistant equipment recommended for Security personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Report to supervisor for orders. Security and engineering should seek to aid epistemics staff in shutting off glimmer probers, and propose evacuation if glimmer spiral is unrecoverable.
| style="border: 1px solid #000000;" | Report to supervisor for orders. Security and Engineering should seek to aid epistemics staff where possible. Propose evacuation if glimmer spiral is unrecoverable.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | Secure areas unlocked and accessible to all authorized personnel, unless affected by a known threat. High Security Areas and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be locked as needed.
| style="border: 1px solid #000000;" | Secure areas unbolted. [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] and areas with an expectation of privacy, such as bedrooms, dorms, or medical treatment facilities, may be bolted as needed.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Full suit sensors heavily advised. Prepare for increased incidents of burn and brain damage.
@ -170,7 +169,7 @@ Elevated alert status. ''The station is suffering dangerously high levels of gli
| style="border: 1px solid #000000;" | EVA suits heavily advised for engineers. Distribute emergency internals to crew. Prepare for detonation of [[Glimmer#Probers|probers]].
|-
| style="border: 1px solid #000000;" | Rules of Engagement
| style="border: 1px solid #000000;" | Engage with minimal force required; ROE on mindbreaking is lifted and may be performed by the Psionic Mantis or Mystagogue at their discretion.
| style="border: 1px solid #000000;" | Engage with minimal force required; ROE on mindwiping is lifted and may be performed by the Psionic Mantis, Chaplain or Mystagogue at their discretion.
|}
== <span style="color:#36717d;>Gamma Alert</span> ==
@ -184,13 +183,13 @@ Emergency alert status. ''Central Command has called the Gamma Alert; the Statio
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > Security Personnel > Department Heads > Supervisory Roles
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Issuing of lethal weapons heavily recommended. Body armour and helmets mandatory for Security personnel, permitted for distribution to authorised personnel. EVA protection heavily advised for all personnel.
| style="border: 1px solid #000000;" | Issuing of lethal weapons heavily recommended. Body armour and helmets mandatory for Security personnel. EVA protection heavily advised for all personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Report to supervisor for general orders. Martial law is in effect. Arrests, searches, and raids may be performed at the discretion of security personnel and without a warrant. Threats neutralised by lethal force may be placed in Preservative Stasis until the station alert level is reduced. Security is fully authorised to take whatever actions deemed necessary to neutralise or repel station threats. Propose evacuation or scuttling if station cannot be retaken.
| style="border: 1px solid #000000;" | Report to supervisor for general orders. Martial law is in effect.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | All HSAs should remain locked and under guard. Secure areas unlocked unless affected by known threats.
| style="border: 1px solid #000000;" | All [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] recommended to be bolted and under guard. Secure areas unbolted.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Full suit sensors mandatory for observation and safety. EVA suits and functioning internals heavily advised for emergency responders.
@ -199,7 +198,7 @@ Emergency alert status. ''Central Command has called the Gamma Alert; the Statio
| style="border: 1px solid #000000;" | EVA suits heavily advised for engineers. Distribute emergency internals to crew.
|-
| style="border: 1px solid #000000;" | Rules of Engagement
| style="border: 1px solid #000000;" | Engage with adequate force to neutralise threats. Lethal force authorised at the discretion of Security personnel, in self-defence, or to effect arrests.
| style="border: 1px solid #000000;" | Martial law is in effect.
|}
== <span style="color:#996633;>Delta Alert</span> ==
@ -214,22 +213,51 @@ Emergency alert status. ''The nuclear destruction mechanism has been engaged and
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > Security Personnel > Department Heads > Supervisory Roles
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Issuing of lethal weapons heavily recommended. Body armour and helmets mandatory for Security personnel, permitted for distribution to authorised personnel. EVA protection heavily advised for all personnel.
| style="border: 1px solid #000000;" | Issuing of lethal weapons heavily recommended. Body armour and helmets mandatory for Security personnel. EVA protection heavily advised for all personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Emergency personnel must expend all efforts to prevent station destruction if engaged in error. Security and Command personnel are to oversee the evacuation of personnel and safe scuttling of the station. Martial law is in effect. Arrests, searches, and raids may be performed at the discretion of security personnel and without a warrant. Threats neutralised by lethal force may be placed in Preservative Stasis until the station alert level is reduced. Security is fully authorised to take whatever actions deemed necessary to neutralise or repel station threats.
| style="border: 1px solid #000000;" | Security and Command must expend all efforts to prevent station destruction if engaged in error. Martial law is in effect.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | All secure areas and HSAs should remain locked.
| style="border: 1px solid #000000;" | All secure areas and [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] should remain unbolted.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Full suit sensors heavily advised. EVA suits and functioning internals heavily advised for emergency responders.
| style="border: 1px solid #000000;" | Full suit sensors mandatory for observation and safety. EVA suits and functioning internals heavily advised for emergency responders.
|-
| style="border: 1px solid #000000;" | Engineering
| style="border: 1px solid #000000;" | EVA suits heavily advised for engineers. Distribute emergency internals to crew.
|-
| style="border: 1px solid #000000;" | Rules of Engagement
| style="border: 1px solid #000000;" | Engage with adequate force to neutralise threats. Lethal force authorised at the discretion of Security personnel, in self-defence, or to effect arrests.
| style="border: 1px solid #000000;" | Martial law is in effect.
|}
== <span style="color:#cae8e8;>Code Octarine</span> ==
Emergency alert status''. The station's deep-spectrum sensors have identified critical Λ-CDM levels. The station and all surrounding space is under threat of an impending existential noospheric-to-real threat.''
{| class="wikitable" style="width: 80%; border: 1px solid #000000;"
!style="text-align:left; width:25%; padding:5px; padding-left:10px; background-color:#583ca2BF; border: 1px solid #000000;" | '''Condition'''
!style="text-align:left; width:75%; padding:5px; padding-left:10px; background-color:#583ca2BF; border: 1px solid #000000;" | '''Description'''
|-
| style="border: 1px solid #000000;" | Chain of Command
| style="border: 1px solid #000000;" | [[Standard_Operating_Procedure#Captains_Authority|CO]] > [[Mystagogue]] > Security Personnel > Department Heads > Supervisory Roles >
|-
| style="border: 1px solid #000000;" | Armoury Policy
| style="border: 1px solid #000000;" | Issuing of lethal weapons recommended. Body armour and helmets mandatory for Security personnel, permitted for distribution to authorised personnel. EVA protection strongly advised for all personnel.
|-
| style="border: 1px solid #000000;" | Discipline
| style="border: 1px solid #000000;" | Emergency personnel must expend all efforts to prevent station destruction. Arrests, searches, and raids may be performed at the discretion of security personnel and without a warrant. Where possible, threats should be subdued and brought to the Chaplain or Mystagogue for processing.
|-
| style="border: 1px solid #000000;" | Secure Areas
| style="border: 1px solid #000000;" | All secure areas and [[Standard_Operating_Procedure#High_Security_Areas|HSAs]] should remain unbolted.
|-
| style="border: 1px solid #000000;" | Medical
| style="border: 1px solid #000000;" | Full suit sensors mandatory for observation and safety. EVA suits and functioning internals heavily advised for emergency responders.
|-
| style="border: 1px solid #000000;" | Engineering
| style="border: 1px solid #000000;" | EVA suits heavily advised for engineers. Distribute emergency internals to crew.
|-
| style="border: 1px solid #000000;" | Rules of Engagement
| style="border: 1px solid #000000;" | Engage with adequate force to neutralise threats. Lethal force authorised at the discretion of Security personnel, in self-defence, or to effect arrests. ROE on mindwiping is lifted and may be performed by the Psionic Mantis or Mystagogue at their discretion.
|}
{{Guides Menu}}

View File

@ -129,6 +129,137 @@ Should any person or party aboard a NanoTrasen vessel be found to be currently a
== Hostile Corporation Technology ==
Additionally, due to the presence of our competitors and their agents in the sectors in which our company operates, there is an ever-present possibility that hostile parties' technology or other property may manifest aboard our station by any number of means. The presence, acquisition, and distribution of such items should always be investigated and scrutinised heavily, and if Space Law and local legal ordinances allow, held securely, confiscated, and restricted from use.
== Potential Threats ==
While working aboard NanoTrasen stations, you may encounter the highly unlikely scenario of your life being threatened by hostile forces. Fortunately, our highly-trained security teams are well-equipped to deal with such threats. Please note, however, that wasting company resources and time for a pre-emptive defense against a non-existent threat are grounds for demotion. Below are the potential hazards you may face:
'''NANOTRASEN DOES NOT TAKE RESPONSIBILITY FOR CREW INJURY OR DEATH IF CREW MEMBERS DISREGARD THE INFORMATION BELOW.'''
=== Hostile Fauna ===
Be it from Epistemics experiments, corpses brought in by salvage, or something living in the vents, you may be attacked by non-sophont creatures. NanoTrasen does not encourage befriending such animals, unless its an approved experiment for research. Below are the procedures of how to deal with such a threat:
*Do not engage with the threat yourself.
*Evacuate the department or the zone.
*Immediately inform Security of the incident. Provide details such as:
**Threat type.
**Estimated numbers.
**Property damage.
**Any additional information.
*Inform Medical if there are injuries. If paramedics are unavailable, stabilize and carefully deliver the wounded to Medical.
*Provide ID access if Security personnel are not able to enter the area.
*Once Security confirms the threat being neutralized, clean up the department and report the incident to Command or Central Command.
=== Hostile Boarding Parties ===
Due to the stations invaluable resources and/or high profile crewmembers, NanoTrasen stations may attract hostile boarding parties. These individuals typically gain unauthorized access to the station and are usually armored, armed with deadly weapons. While their intentions are rarely known, common goals may include:
*Assassination of crewmembers.
*Theft of valuable station materials.
*Theft of sensitive research.
*Mass crew harm or terrorism.
*Sedition of the chain of Command.
*Detonation of the stations nuclear warhead.
If such an unlikely scenario occurs, the following procedures are listed below:
*Do not engage with the threat yourself.
*Seek immediate shelter.
*Immediately inform Security of the incident. Provide details such as:
**Hostiles appearance.
**What kind of armaments theyre equipped with.
**Estimated numbers.
**Last known location.
**Property damage.
**Any additional information.
*Inform Medical if there are injuries. If paramedics are unavailable, stabilize and treat the wounded. Only move the wounded if the path to Medical is safe. Do not take unnecessary risks.
*Remain in place until the situation has been resolved.
In the case of overwhelming force that Security cannot handle:
*Request assistance from Central Command through fax. In your fax, give as much detail of the threat as you can and the damages to the station and crew, alongside with signatures and stamps. Read “Contacting Central Command” for more information.
*Evacuate the station.
=== Unauthorized Cults ===
NanoTrasen recognizes and respects the free expression of religion aboard its stations. Discrimination of any kind will not be tolerated and is grounds for termination. However, Nanotrasen does not permit religious activity that involves:
*Fanatical recruitment of additional members.
*Harm to crew members, be it physical, psychological or metaphysical.
*Practices associated with EarthGov banned religions that are officially designated as harmful cults.
For classification, speak with the stations Mystagogue or Priest.
Please note that the Oracles religion is permitted aboard NanoTrasen stations, as they are the primary subject of research.
In the case of an unauthorized cult is suspected of forming on board, all crew members are encouraged to report unusual activity. Suspicious behaviour can be:
*Secrecy and Isolation. Suspicious individuals may engage only with selected individuals in secluded areas.
**Sudden changes in behaviour. Known members having a sudden change in their behavior or personality may indicate their affiliation.
**Otherworldly or physics defying effects occurring around certain crew members.
**Appearance of unidentified anomalies.
If a hostile cult is confirmed, follow these procedures:
*Listen to Epistemics, Security and Command for any relevant information and orders about the threat.
*Report suspicious individuals to Security and Epistemics. Give as much detail as possible, such as their appearance, potential tools, behaviour and more.
*Form a group of trusted co-workers and stick together. Do not isolate yourself.
*Await further instructions and remain in secure areas until the situation has been resolved by Security and Epistemics personnel.
=== Pathogens ===
While all NanoTrasen personnel are vaccinated against a wide range of known diseases, and while the station's atmospheric systems utilize only the highest-grade filtration technology, an outbreak may still occur on the station. Most such incidents are minor in scope - like the common cold. However, employees are reminded to remain vigilant and to treat any outbreak with the seriousness it deserves.
In the event of a confirmed outbreak, please follow these procedures:
*Follow instructions. Listen to Medical staff and comply with all orders and updates.
*Wear protective equipment, such as masks and gloves. Biosuits are recommended if available.
*Maintain hygiene. Wash hands frequently, and clean up any potential biohazards.
*Report Symptoms. Notify Medical and Security if any personnel display signs of infection.
*Stay in your department.
*Limit physical contact.
If youre starting to show symptoms of the pathogen, please immediately follow these procedures:
*Immediately head to the Medical Department.
*Inform Medical personnel of your symptoms.
*Listen to all orders from Medical personnel.
*Store any valuable NanoTrasen gear, weaponry or property in a secure location.
*If a treatment is currently unavailable, quarantine yourself in Medical.
*Await treatment.
=== Unions ===
NanoTrasen supports free speech, the right to fair working conditions and does not consider itself anti-union. However, The Company is not neutral either. NanoTrasen will always defend the interests of its customers, shareholders and associates. Worker unions may disrupt work productivity, which can in turn jeopardize job security for all crew members.
If an organizing union attempts to use force to advance its goals, such as vandalizing Company property or physically attacking other crewmembers, Security is authorized under Space Law to quell and suppress unions.
It is every crew member's duty to report potential warning signs of unionization to their managers. Warnings signs may be:
*Crew members refusing to work.
*Usage of union related terminology, (living wage, working conditions, etc.)
*Circulation or distribution of petitions and union fliers.
*Unusual interest in Company policies, benefits, employee list or any other Company information.
*Noticeable changes in behavior among known crew members.
*Non-manifest individuals supporting union activity.
*Clothing, accessories associated with unions.
*Increased negativity and complaints.
In the case of a violent union riot, follow these procedures:
*Do not join the riot.
*Inform Security and Command immediately. Provide details:
**Names and job positions of any suspected union members and leaders.
**Whether the union members are armed.
*Inform Central Command of the incident.
*Shelter in a safe place and await for the situation to be resolved.
== Contacting Central Command ==
Should you require a question answered about your duties, advice on how to properly handle situations, or wish to submit an incident report (including gross misconduct by Command staff), Central Command Officials are always happy to assist. Please ensure your fax is descriptive, legible and includes your signature or departmental stamp.
Refrain from sending junk mail to Central Command. Penalties may include pay cuts or a renegotiation of your contract.
In the highly unlikely event that station security is faced with an overwhelming hostile force or the chain of Command is compromised, crewmembers are advised to request assistance from Central Command.
Your fax should include:
*Stations designation (available in your PDA)
*The overwhelming threat. Try to provide as much detail as possible, such as:
**Estimated number of hostiles.
**If armed, what kind of armaments theyre equipped with.
**Number of casualties. Information about Securitys and Commands status is recommended.
**Stations structural integrity.
Due to the prevalence of misinformation reaching Central Command, please ensure to add as many stamps and signatures as possible/convenient.
Be warned that all faxes, urgent or otherwise, may take some time to be properly read and receive a response. Central Command is dedicated to managing multiple stations simultaneously. Rest assured: your fax will be addressed, even without your notice.
= Conclusion =
In closing, we thank you for your commitment to our mission and the values that guide NanoTrasen as a whole. Your dedication is the keystone to our success.

View File

@ -12,7 +12,13 @@ It is highly recommended to read the '''[[Standard Operating Procedure|Standard
* '''[[Standard Operating Procedure#Paroles and Pardons|Paroles & Pardons]]:''' Parole may be offered under the discretion of the Warden, Head of Security, or CO. A parole hearing requires only a Judge, as discussed within Trial Procedure. Pardons may be issued by the arresting officer, the Judge of a trial, by the Warden (to those within the permanent brig), or by the CO, when deemed in the best interest of the crew or vessel, or when the circumstances of the crime warrant suspension of sentence.
* '''[[Standard Operating Procedure#Pressing Charges|Pressing Charges]]:''' Any sophont may press charges against any other sophont. Individuals may only be charged with a crime if it can be proveny beyond reasonable doubt that they have committed the crime.
* '''[[Standard Operating Procedure#Trial Procedure|Trial Procedure]]:''' A criminal trials Judge must be, in sequential order: The Chief Justice, the Court Clerk, an impartial member of Justice or Station Command, or the Warden. The Prosecution should ideally be the Prosecutor, or an available member of Station Command or Station Security in that order. The Defense should ideally be a member of the Justice department assigned to the defendant, though they are allowed to represent themselves if they wish. If insufficient crew are available, the Judge is fully authorized and strongly encouraged to pass a sentence of Extended Confinement until a proper trial can be held.
* '''[[Alert Procedure]]:''' Codes Blue, Red, Gamma, and Delta suspend the right to individual privacy, allowing warrantless search. The latter three codes allow for departmental raids, and the latter two place the station in martial law. These codes also authorize Security to carry progressively more advanced firearms, and loosen '''[[Standard Operating Procedure#Rules of Engagement|Rules of Engagement]]''' regarding their use.
* '''[[Alert Procedure]]:''' Codes Blue, Red, Gamma, and Delta authorize Security to carry progressively more advanced equipment, and loosen '''[[Standard Operating Procedure#Rules of Engagement|Rules of Engagement]]''' regarding their use. The former two codes disallow openly carrying firearms by non-Security personnel; the latter two suspend many rights to privacy, and grant Security the power to do whatever neccessary to retake or scuttle the station.
==Procedural Defense==
If a defendant is accused of a crime, they are granted the privilege to challenge the legitimacy of charges of claims brought against them. Any of the following may be invoked:
:*Double Jeopardy Clause: accused persons may are not to be charged on the same charges following an acquittal or conviction, except in the case of acquittal and subsequent credible admission of guilt.
:*Entrapment Clause: accused persons found to be induced or coerced into the commission of the crimes listed in the charges are to be acquitted.
:*Exclusionary Clause: evidence collected and/or analyzed in violation of accused persons rights under law is inadmissible.
=== Crime Codes Quick Reference ===
Use this to quickly find the Crime Code Numbers.
@ -49,7 +55,7 @@ For example, an attack upon a sophont which causes them to die of their injuries
!style="border: 1px solid black;" | 04
|style='background:#9999334D; border: 1px solid black;' | Petty Larceny
|style='background:#9966334D; border: 1px solid black;' | Endangerment
|style='background:#9933334D; border: 1px solid black' | Mindbreaking
|style='background:#9933334D; border: 1px solid black' | Noöspheric Tampering
| style="background:#9900334D; border: 1px solid black;" | Prevention of Revival
|-
!style="border: 1px solid black;" | 05
@ -88,14 +94,11 @@ For example, an attack upon a sophont which causes them to die of their injuries
Capital Sentences are organized in order of severity, with the least severe last. Execution should be utilized as an '''absolute last resort,''' for criminals whose crimes are extraordinarily cruel, or who prove impossible to confine through less restrictive means.
* '''Execution:''' Termination of the convict's life.
** Whenever any execution is performed, for any reason, a general announcement must be made stating who was executed, and for what reason.
** Whenever any execution is performed, outside of Martial Law, a general announcement must be made stating who was executed, and for what reason.
** The convict may either face execution by firing squad, by lethal electrocution, by being ejected from the station via an airlock (being "spaced"), or by a method of their choosing, at the Judge's sole discretion.
** If recoverable, their remains must either be cremated, processed via the biomass reclaimer, or stored in the morgue.
* '''Decorporealization:''' Stripping the convict's mind from their body into a more restrictive form, such as a cyborg or soul gem.
* '''Preservative Stasis:''' Relocation of the convict into a morgue or similar controlled and regulated environment until one's sentence is concluded, or until the legal system can accomodate the convict.
* '''Exfiltration:''' Immediate retrieval of the convict via a CentComm or GALPOL prison transport.
** The prisoner may be held within the permanent brig until the transport arrives.
** If transport cannot be arranged, default to other capital sentences.
* '''Extended Confinement:''' Placing the convict into the Permanent Brig until the end of the shift, where they are to be transported to Central Command for processing.
** The convict has the same rights as any other prisoner within the Permanent Brig, whether they arrived by transport or were sentenced to Extended Confinement.
:<!-- -->
@ -109,7 +112,7 @@ Capital Sentences are organized in order of severity, with the least severe last
<!-- -->
:*'''Licence restriction''': Downgrading or suspending the convicts armament licence and confiscating possessions accordingly. May be applied when the convict is guilty of possession or a crime involving a weapon they were lawfully permitted to carry.
<!-- -->
:*'''Mindbreaking''': Removal of the convicts psionic power by use of mindbreaker toxin or any other means. May be applied when the convict is guilty of a crime in which they use psionics.
:*'''Mindwiping''': Removal of the convicts noöspheric or other otherworldy connections by use of mindbreaker toxin, ardent censer, or any other means. May be applied when the convict is guilty of a crime in which they use psionics, otherworldy abilities, or similar powers.
== Crime Modifiers ==
Sentencing modifiers are to be applied by the sentencing officer, judge, or arbiter. Aggravating modifiers may not extend a sentence beyond the specified limit, while extenuating modifiers may not reduce a sentence below the specified limit.
@ -216,9 +219,9 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|-
| style="border:1px solid black;" | 304
| style="border:1px solid black;" | [[File:SL_Mindbreaking.png]]
| style="border:1px solid black;" | Mindbreaking
| style="border:1px solid black;" | Noöspheric Tampering
| style="border:1px solid black;" | 14 minutes
| style="border:1px solid black;" | To unlawfully and maliciously rid a psionic of their powers
| style="border:1px solid black;" | To maliciously tamper with or create noöspheric anomalies, entities, aberrations, or other elements; Or to otherwise deliberately perform actions that would raise Glimmer or other anomalous reading levels in a way that threatens normative reality.
|-
| style="border:1px solid black;" | 305
| style="border:1px solid black;" | [[File:SL_Sabotage.png]]

View File

@ -7,11 +7,12 @@ The terms specified below are defined for further use within the procedures.
:*Commanding Officer / CO: An officer appointed to lead the station and its Command department, whether it be the Captain by Nanotrasen, or an Acting CO by Station Command.
:* Command Staff/Station Command: Members of the Command department who lead a department aboard the station.
:* Crew: All sophonts employed by Station Command, or by NT to reside on the station and perform an established function therein. This includes any sophont listed on the Crew Manifest or issued a functional NT identification and access card. Individuals imprisoned by NT, along with those imprisoned during the course of a shift for violations of Space Law, are considered Crew.
:* Judge: An indivudal legally appointed to preside over a criminal or civil trial, as specified within Trial Procedure.
:* Judge: An indivudal legally appointed to preside over a hearing, as specified within Hearing Procedure.
:* Detainee: Any sophont whose free movement has been curtailed by law enforcement on suspicion of criminal activity. They have not, however, been officially charged with a crime.
:* Arrestee: Any sophont who has been formally charged with a crime.
:* Prisoner: Any sophont who has been sentenced for the commission of a crime, or awaiting trial for a capital offense.
:* Prisoner: Any sophont who has been sentenced for the commission of a crime, or awaiting a hearing for a capital offense.
:* Permanent Brig/Permabrig/Brig: A secure location for the long-term confinement of Prisoners. Typically outfitted to sustain the basic survival needs of the Prisoners within.
==Right to Refuse Service==
All crew operating within their designated job locations are given the right to refuse service at any time for disorderly or abusive conduct. Additionally, all crew members are permitted to use nonlethal force to remove noncompliant or disorderly individuals only when such persons pose a threat to the safety of staff members, or disrupt the orderly functioning of the station. Service may not be refused to first responders acting to address or mitigate an emergency or crisis situation, unless such conduct is egregiously disorderly or abusive.
@ -96,7 +97,7 @@ Armaments licenses may be issued by the Commanding Officer, Warden, or Head of S
If the holder uses their armaments in the commission of a crime, the issuer may be held liable for accessory to the crimes committed, along with Endangerment and Abuse of Power where applicable.
== Departmental Dispensations ==
Certain bodies, groups, or personnel may require certain Controlled Items in the course of their job, and as such must be provided exceptions. '''Any member of any department which begins their shift with a controlled item in their possession or within their departments allocated area is assumed authorized for its possession and use.'''
Certain personnel may require certain Controlled Items in the course of their job, and as such must be provided exceptions. '''Any member of any department which begins their shift with a controlled item in their possession, within their allocated area, or with a departmental lathe capable of producing said controlled item, is assumed authorized for its possession and use.'''
Additionally, tools and equipment located within emergency lockers are not considered Controlled Items, so long as they are used as intended.
@ -109,7 +110,6 @@ Unlocked crates, orders stored outside the department, and supplies illegally di
Epistemics employees are permitted to use their lab spaces as they see fit, with the following caveats:
* Any sophont experimentation must be done with the consent of the test subject, and must not have an undue risk of life. The Mystagogue or CO may terminate any experiment for safety or ethical purposes and at their discretion.
* Self-experimentation is prohibited and is chargeable with Endangerment, at a minimum.
* Testing of any explosives or weapons that risk hull penetration or mass destruction must be done at an off-station site.
Epistemics personnel may also utilise Controlled Items within the confines of designated testing areas and in a manner directly approved jointly by the Mystagogue and by the Head of Security or CO.
@ -143,7 +143,7 @@ Any such being is considered a sophont, and thus a legal person, regardless of t
The unauthorised jailbreaking or uplifting of property into the status of Sophont is still a criminal offense however, and those found guilty of doing so may be found guilty of sabotage or vandalism.
An entitys status as a sophont may be challenged during the process of charging and trialing the individual or another for crimes which utilize the term sophont in the legal language of the crime. The Defense or Prosecution will present evidence indicating that it meets, or does not meet, the criteria above. The Judge will then arbitrate upon the entity's status as a sophont.
An entitys status as a sophont may be challenged during the process of charging the individual for crimes which utilize the term sophont in the legal language of the crime. The Defense or Prosecution will present evidence indicating that it meets, or does not meet, the criteria above. The Judge will then arbitrate upon the entity's status as a sophont.
Any synthetic/silicon/inorganic entity with a non-sophont status, is to be classified as a standard synthetic entity. Synthetic entities are the property of the party that constructs or employs the entity, unless otherwise stated by relevant contract or legal document. To damage or destroy such an entity is to damage or destroy the aforementioned partys property.
=Commanding Officer=
@ -158,7 +158,7 @@ Captains Authority carries a responsibility, however; the CO must do everythi
Additionally, the legally appointed Captain of a vessel is afforded two additional rights over that of a CO:
* To temporarily modify SOP in times of emergency, at the risk of criminal penalties if misused or abused, and
* To, with public announcement to the crew, temporarily modify SOP in times of emergency, at the risk of criminal penalties if misused or abused, and
* To deputize members of the crew to carry out law enforcement activity. These deputies are bound by Security Regulations and Space Law, and the Captain may suffer criminal penalties for any crime committed by them.
==Line of Succession==
@ -179,7 +179,7 @@ Should such a situation arise in which the CO is unfit for their position and de
A mutiny, defined as a coordinated effort to depose the commanding officer or officers of a vessel for committing unlawful acts, requires the crew to neutralize the offending staff with non-lethal force where applicable, and lethal measures and weapons only in acts of self-preservation or when given no other option. The commanding officer or officers, as well as their loyalists, are all bound to Space Law, the current alert level and its corresponding RoE.
Should the mutiny be unsuccessful, only the head officer(s) who approved the mutiny may be charged with sedition and receive a trial, while all mutineers may be demoted, processed by law enforcement, and charged for the crimes they have committed.
Should the mutiny be unsuccessful, only the head officer(s) who approved the mutiny may be charged with sedition and receive a hearing, while all mutineers may be demoted, processed by law enforcement, and charged for the crimes they have committed.
Should the mutiny be successful, the head officer(s) who approved the mutiny must select a new Commanding Officer per Line of Succession. The new CO will then sentence the previous heads of staff for their crimes per Space Law.
@ -190,48 +190,16 @@ Crew of foreign vessels may be refused entry, at any time and for any reason, an
Crimes committed against the station or its inhabitants, on station or abroad, are grounds to charge the perpetrator according to the laws of the station. Should visiting entities break the law, or be suspected of doing so, and flee aboard their own vessel, law enforcement personnel are authorized to board the vessel in continuous “hot” pursuit and are fully authorized to effect an arrest upon the relevant party.
=Security Regulations=
=Security and Justice Regulations=
==Security Jurisdiction==
Law enforcement personnel are bound by trespass and assault laws, and thus cannot operate outside accessible areas, or perform searches without a Probable Cause, a warrant, or a heightened alert level that specifically allows such action. Advanced surgical procedures and cursory implant investigations must be performed by a trained member of the Medical department, operating under their [[Standard Operating Procedure#Standard of Care|Standard of Care]].
Law enforcement personnel are bound by trespass and assault laws, and thus cannot operate outside accessible areas, or perform searches without a Probable Cause, or a [[Standard Operating Procedure#Warrants|warrant]]. Advanced surgical procedures and cursory implant investigations must be performed by a trained member of the Medical department, operating under their [[Standard Operating Procedure#Standard of Care|Standard of Care]].
However, officers are allowed to trespass into a department in “hot pursuit” of a suspect who has fled from an accessible area, who they have Probable Cause to believe has committed a crime
==Probable Cause==
While actively enforcing the law, officers are held to the standard of Probable Cause. This phrase represents a reasonable suspicion that a crime has been, or is being, committed, and does not require the officer to have solid evidence of criminal activity. Examples of Probable Cause include witnessing criminal activity or credible reports of such.
When judged, Probable Cause is to be interpreted within the context of the entirety of the stations circumstances at the time of the incident. Probable Cause negates the need of a warrant for searches, and, once concluded, should be promptly reported to the Prosecutor, Warden, or Head of Security for review.
==Detainment and Arrest==
Any sophont may be detained by a law enforcement officer with Probable Cause. In detainment, the officer will inform the Detainee of the crime they are suspected of committing. They may be held on-site or moved to a strategically sound location, restrained at the officers discretion, and detained while law enforcement officers determine if they can proceed with an arrest. Such a detention should last no longer than five (5) minutes. A Detainee may not be searched, unless consent is given or Alert Procedure allows for warrantless search, and their detention should be concluded within five minutes of its start, by either Arresting them or releasing them.
Any sophont which may have committed a crime, assuming Probable Cause, or has been formally charged with the commission of a crime, may be arrested. In an arrest, the suspect will be restrained by a member of law enforcement, informed of their charges, and taken to the brig for search and processing under Brig Procedure. Any Controlled Items in possession of the Arrestee without licensure or dispensation must be confiscated, along with items used in commission of the crime(s), and will be held indefinitely by law enforcement as described in [[Standard Operating Procedure#Evidence Handling|Evidence Handling]]. Legal counsel may be requested by an Arrestee or Detainee, and must be provided if available.
==Brig Procedure==
When confining an Arrestee within the Brig, they should be transferred to the Warden for processing:
* A full search of the Arrestee and their possessions.
* The confiscation of any Controlled Items or items used in commission of the crime, along with any equipment not befitting a prisoner
* The confining of the Arrestee within a temporary holding cell.
* [[Standard Operating Procedure#Sentencing|Sentencing]] the Arrestee.
If the Arrestee is sentenced for a Capital-level crime, they are to be transferred to the Permanent Brig, pending trial. If the Warden is unavailable, the arresting officer must process the Arrestee as stated above. All confiscated items must be handled as dictated within [[Standard Operating Procedure#Evidence Handling|Evidence Handling]].
==Sentencing==
Any law enforcement individual is authorized to pass sentences on all Grand Felony, Felony and Misdemeanor-level crimes; however, the Prosecutor is responsible for overseeing sentences passed, and is authorized to correct sentences or even acquit them, in accordance with both Space Law and [[Standard Operating Procedure#Paroles and Pardons|Paroles and Pardons]]. Both capital charges and sentences must be placed by the Prosecutor or Court Clerk; if neither are available, the Warden or Head of Security are authorized to press capital charges and request sentencing for them, under Trial Procedure.
In the case that the accused requests appeal for a Grand Felony- or Felony-level sentence, the Court Clerk should be called to preside over a small, swift adjudication, where the Prosecutor and accused will argue their sides. The Clerk will then decide the sentence, or acquittal, of the accused. If the Clerk is unavailable, the Chief Justice or the Warden can perform appeals. If neither are available, any available member of Station Command is authorized to adjudicate on the case.
Additionally, any sentence meeting or exceeding twenty-five (25) minutes, regardless of the severity of the crime(s) committed, will require a trial. If the total sentence still meets or exceeds twenty-five minutes after the conclusion of a lawful trial, the Judge is fully authorized and encouraged to upgrade their sentence to Extended Confinement.
Once a sentence has commenced, it must be concluded at or before the agreed-upon time.
== Evidence Handling ==
When evidence of a crime is secured, it is the responsibility of the arresting officer or Warden to properly handle it, at the penalty of Obstruction of Justice and/or demotion.
Evidence should be organized by individual and crime, and stored together (e.g., in a bag, in one separate locker, etc) and then placed in an area locked behind security access.
If a prisoner is released from a temporary holding cell OR the Brig, any of their equipment that is neither a Controlled Item nor used in the commission of their crimes must be returned to them.
While actively enforcing the law, officers are held to the standard of Probable Cause. This phrase represents a reasonable suspicion that a crime has very recently been, or is currently being, committed, and does not require the officer to have solid evidence of criminal activity. Examples of Probable Cause include witnessing criminal activity or credible reports of such.
When judged, Probable Cause is to be interpreted within the context of the entirety of the stations circumstances at the time of the incident. Probable Cause negates the need of a warrant for searches, and, once concluded, is to be promptly reported to the Prosecutor, Warden, or Head of Security for review.
== Rules of Engagement ==
Law enforcement personnel hold a Category D Armaments License, and are authorized at all times to use lethal force to the extent necessary to neutralize adversaries under any of the following circumstances:
@ -245,152 +213,103 @@ Law enforcement personnel hold a Category D Armaments License, and are authorize
Despite their allowance of lethal force, Security is required to follow the Rules of Engagement of the current Alert Level, as outlined within [[Alert Procedure]].
== Martial Law ==
Martial Law may only be declared by Central Command. Under Martial Law Security is:
* able to perform arrests, searches, and departmental raids at their discretion and without a warrant.
* authorised to take whatever actions deemed necessary to neutralise or repel station threats.
* authorized to summarily execute station threats.
* not bound to the Rules of Engagement.
* not obligated to provide any medical care to prisoners and detainees, including revival.
== Armory Procedure ==
The armory and its usage are governed by the Warden. The Warden is responsible for stocking, distributing, and recording the collection of weapons used by the station during the shift. They are also afforded the authority to arm security at their discretion, so long as the weaponry distributed is in appropriate response to [[Alert Procedure]].
All weaponry distributed from armory is issued on a temporary basis. While issued, the weaponry remains as station property, and the wielder assumes a duty to protect the weapon and return it to the Warden when ordered. No non-crew may be issued weaponry from the armory.
The Warden is accountable for responsibly issuing weapons. Any weaponry issued during a heightened alert level must be returned when the alert has been lowered, or when the threat is no longer present. The weaponry issued must be proportionate to the threat posed, and explosives should not be used unless absolutely necessary.
The Warden is accountable for responsibly issuing weapons. Any weaponry issued during a heightened alert level must be returned when the alert has been lowered, or when the threat is no longer present. The weaponry issued must be proportionate to the threat posed, and weapons which threaten the structural integrity of the station should not be used unless absolutely necessary.
==Treatment of Prisoners==
Prisoners have certain rights that must be upheld by law enforcement and Station Command. ''Prisoners must be provided with the following'':
* Adequate medical care and moral, spiritual, or legal counseling if it is requested and available.
* Access to the Common and Prison radio channels only while serving their sentence. This right may be revoked should it be abused.
* Clothing, food, and water on request.
* Standard prisoner clothing must always be available for prisoners.
* Confiscated clothing should be returned upon request, except in the case of Extended Confinement or if the clothing in question poses a clear risk to prisoner or crew safety.
* Food and water for organic prisoners must be available and given on request.
* Power for silicon prisoners such as IPCs and cyborgs, at minimum a cyborg recharging station must be provided.
* Should the holding cells or Permabrig become uninhabitable, prisoners must be securely and safely relocated to another area for confinement, until the holding cells or Permabrig are returned to a serviceable state.
* Prisoners should be granted freedom of movement within their holding area unless there is an undue risk to life and limb.
Additionally, prisoners sentenced to Extended Confinement have certain rights but also '''more firm restrictions that must be upheld by law enforcement''':
* Visitation may be permitted only for prisoners sentenced to Extended Confinement. One person may visit such a prisoner for a period of no longer than ten minutes. A reason must be provided for the visit, and the visitor must consent to a search of their belongings. The prisoner is then unable to receive another visitor for another ten minutes.
* Prisoners may request retrial for capital crimes; these requests may be refused depending on the discretion and availability of the court clerk, chief justice, or at the discretion of the warden or head of station security.
* Prisoners may request or may otherwise be given parole with a hearing by the Chief Justice, Court Clerk, or in their absence, the head of station security or commanding officer (see Paroles and Pardons).
* Visitation may be permitted only for prisoners sentenced to Extended Confinement. A reason must be provided for the visit, and the visitor must consent to a search of their belongings.
* Prisoners may request an appeal for capital crimes; these requests may be refused depending on the discretion and availability of the Clerk, Chief Justice, or at the discretion of the warden or head of station security.
* Prisoners may request or may otherwise be given parole by the Warden or Head of Security.
Should prisoners repeatedly and maliciously antagonize law enforcement, to an extent where continued extended confinement becomes infeasible, the warden is fully authorized and encouraged to temporarily upgrade the relevant prisoners sentences to Preservative Stasis summarily.
Should prisoners repeatedly and maliciously antagonize law enforcement, to an extent where continued extended confinement becomes infeasible, the warden is fully authorized and encouraged to upgrade the relevant prisoners sentences to Exfiltration summarily.
Moreover, prisoners are liable for any and all crimes that they commit while held either in the temporary holding cells or the Permanent Brig, and may receive trial for more restrictive punishments if their actions warrant a capital sentence.
Moreover, prisoners are liable for any and all crimes that they commit while held either in the temporary holding cells or the Permanent Brig, and may, in the case of the Permanent Brig, be placed in solitary confinement for the duration of the sentence the committed crimes incur.
==Paroles and Pardons==
Prisoners may be allowed parole, per the discretion of the Warden, the Head of Security, or the CO. Alternatively, they may request a parole hearing as described within Trial Procedure.
==Arrest and Sentencing==
Any sophont, that has been formally charged with the commission of a crime, or can be assumed to have committed a crime under [[Standard Operating Procedure#Probable Cause| Probable Cause]] may be arrested by a member of Security. In an arrest, the suspect will be restrained, informed of their charges, and moved to the Brig for processing.
Legal counsel may be requested by an Arrestee, and must be provided if available.
At the brig the prisoner is searched and confined to a holding cell. Any items on their person they are not authorized to possess are confiscated and sent to the justice department for processing and storage as evidence. It is the duty of both the Warden and the Chief Justice to ensure evidence is handled properly and safely.
The prisoner is sentenced according to Space Law.
Any items deemed unbefitting of a prisoner may be taken for the duration of incarceration and must be returned at the end of the sentence.
Any member of Security is authorized to pass sentences on all Grand Felony, Felony, and Misdemeanor-level crimes. The Chief Justice, or in their absence the Head of Security, is authorized to pass sentences on all Crimes, and to correct, or even acquit them, in accordance with Space Law.
Prisoners seeking parole may request legal counsel before the hearing, but they must represent themselves. A prisoner who is granted parole is to be treated as a member of the stations crew with all benefits and responsibilities thereof, and is encouraged to seek gainful employment from relevant command staff.
Should the Prisoners sentence exceed a total of 25 minutes, or if the crimes charged include a capital crime, the prisoner is transported to the Permabrig to await their [[Standard Operating Procedure#Hearings|Hearing]]. If after 25 minutes a hearing still has not been conducted, the prisoner is offered to be implanted with a tracking implant and released, and to arrive to the hearing when it happens. Failure to attend constitutes contempt of court.
Additionally, a pardon can be issued to any entity accused of, or sentenced to, crimes, by the arresting officer, the Judge of a trial, or the Warden in the case of those within the permanent brig, if deemed in the best interest of the crew and vessel, and when the circumstances of the defendant or their offense warrant a suspension of their sentence.
==Appeals, Parole, Pardons==
Appeals are handled by the Chief Justice. Attorneys may appeal a past ruling on behalf of their client, if presented with new evidence. If the evidence exonerates the Prisoner, their sentence is adjusted as necessary by the Chief Justice.
A Prisoner sentenced to a Felony or Grand Felony crime may request an appeal by the Clerk or Chief Justice, these appeals may be denied for any reason.
=Justice Regulations=
==Pressing Charges==
===Criminal Charges===
Any crew member may press charges against any other crew member. An individual may be charged of a crime if and only if it can be argued beyond a reasonable doubt that the accused had committed an act in the nature and fashion as described by a particular criminal charge.
Prisoners may additionally receive parole per discretion of the Warden, Head of Security, or CO. Parolees are encouraged to seek gainful employment on station. Parolees may have any sentence escalated to permanent confinement regardless of severity.
A pardon may be issued to any sophont accused of, or sentenced to, crimes, if deemed in the best interest of the crew and vessel, and when the circumstances of the defendant or their offense warrant a suspension of their sentence. A pardon may be issued by the Arresting Officer or Warden, in case of the Prisoners, or the Judge of a hearing and must be accompanied by an announcement, explaining who was pardoned, of what crime, and for what reason.
==Pressing Criminal Charges==
Any crew member may press charges against any other crew member. An individual may be charged of a crime, if it can be reasonably argued, that the accused has committed an act in the nature and fashion as described by the particular criminal charge.
Multiple counts of one crime can be charged. For illegal actions against the station abroad, each incident separated by a reasonable period of inaction, lawful conduct, or another crime is interpreted as one count. For crimes directly bereaving a person (violent acts, theft of personal property etc.), the same applies in addition to each victim being one count. In the case of possession, theft, or any incident involving several separate and distinct items, one charge may be applied per relevant item.
Charges that are a direct escalation of one another in nature cannot be simultaneously held against the perpetrator if they pertain to the same incident and the same victim/item (if applicable). Then, only the most severe of these charges applies. Additionally, if one charge incorporates another charge within its definition or description, the perpetrator cannot be held accountable for both; rather, only the most severe of these charges applies.
===Civil Charges===
Any crew member can sue any other crew member for inflicting them with damages caused by a breach of duty. To sue is to request compensation for said damages. To do so, the victim must prove:
*'''Damages:''' That the victim was inflicted with an emotional, physical, or monetary damage
*'''Breach of duty:''' That the defendant had a duty which they failed to uphold(e.g., a duty to not endanger crew, a duty to act respectfully), leading to the victim being inflicted with a damage
*'''Severity:''' That the damage was severe enough to warrant the compensation requested
Compensation can either be explicitly specified by the victim or left for the court to decide. Either way, the court is free to modify the compensation as it sees fit. Most often, compensation is rendered in the form of court orders compelling a crew member to do something (e.g., return an item, make an apology, or permanently avoid a certain area). Court orders should be written and stamped with the notary stamp.
==Warrants==
Warrants represent official orders to the security department, issued by the Justice Department. Warrants are only valid when issued by either the Chief Justice or Clerk, or in both of their absences, the Commanding Officer or Head of Security.
==Court Orders==
The individuals specified in the three sections below can issue Court Orders, which are legally-enforced documents that carry the penalty of an Obstruction of Justice charge and forced compliance with the order by security. Each order has its own requirements and authorized issuers, but are all implied to require '''''the stamp (where applicable) and signature of the issuer to be valid.'''''
===Warrants===
Warrants represent official orders to the security department, issued by relevant officers or the Justice Department. Warrants are only valid when issued by either the Chief Justice or Court Clerk, or in both of their absences, the Commanding Officer or head of station Security.
* Individual Warrants may order the search of an individual, or the arrest of the individual with evidence. This may also include surgical procedures, cursory implant inspections, or any other invasive searches with evidence. These warrants must include a name, the actions prescribed (search, arrest, etc.), and a description if applicable.
* Search Warrants may order the search of an entire departmental area, by force if necessary, and may order the arrest of listed individuals implicated in a crime or crimes with probable cause or evidence. Search Warrants should include an area, a brief description of the probable cause, and a list of individuals if applicable.
==Injunctions==
The Chief Justice, Commanding Officer, or Judge of a hearing are fully authorized to issue injunctions: legally-binding, written orders restricting Inhabitants from doing something disruptive to station productivity or personal rights.
These may be issued on discretion. Injunctions may not compel someone to violate Space Law or SOP, and the issuer is liable for abuse of power charges if this is misused. An injunction must include a list of affected names and a detailed description of the restrictions to be valid.
=Hearings=
NanoTrasen authorizes its Justice Department to conduct Hearings in order to determine if a person accused of sufficient crimes may be held in indefinite custody, to be handed over to GalPol at the end of the shift.
The purpose of these hearings is not to determine legally binding guilt, but to measure how feasible their guilt is; and to avoid lawsuits which would tarnish the NanoTrasen brand.
* Individual Warrants may order the search or detainment of an individual, or the arrest of the individual with evidence. This may also include surgical procedures, cursory implant inspections, or any other invasive searches with both probable cause and evidence. These warrants should include a name, the actions prescribed (detainment, search, arrest, etc.), and a description if applicable.
* Search Warrants may order the search of an entire departmental area, by force if necessary, and may order the arrest of listed individuals implicated in a crime or crimes with probable cause and evidence. Search Warrants should include an area, a brief description of the probable cause, and a list of individuals if applicable.
Hearings are a tool of Space Law. Like any other action that represents the Law, they are subject to Procedural Defense as detailed in Space Law.
===Injunctions===
The Chief Justice, Commanding Officer, or Judge of a trial are fully authorized to issue injunctions: legally-binding, written orders restricting Inhabitants from doing something disruptive to station productivity or personal rights.
These may be issued on discretion or as relief for Civil Trials. Injunctions may not compel someone to violate Space Law or SOP, and the issuer is liable for abuse of power charges if this is misused. An injunction must include a list of affected names and a detailed description of the restrictions to be valid.
A hearing must be held for capital charges, and for sentences that exceed a total of 25 minutes when added together. The right for a hearing may be waived by the suspect by pleading guilty, under martial law, or in absence of any Justice personnel on station.
===Subpoenas===
The Chief Justice, Court Clerk, or Judge of a trial may issue Subpoenas, which are trial-related Court Orders compelling an individual to produce evidence or make themselves present at a location for court. A subpoena can order an individual to:
A hearing is conducted by a Judge (chosen in order from: Chief Justice, Clerk, Commanding Officer, Command), who charged is with ensuring the hearing is completed, decides the time and location of the hearing, and whether they want to make the hearing accessible to the public or not. A hearing must include the Judge and Prosecution, and should additionally include the accused and defense, if available. The Judge has the right to find any person in the hearing guilty of contempt of court.
Hearings are concerned with any and all crimes the accused is currently charged with, not just capital crimes.
* Appear as a witness, defendant, plaintiff, or prosecution;
* Produce evidence for a trial;
* Appear for questioning in deposition.
Defense must be provided to the accused if requested and feasible, and should consist of either an attorney, any member of the justice department, or any sophont chosen by the accused.
The prosecution should consist of either a prosecutor, any member of the justice department, or any member of station security.
A subpoena must include a list of names, a detailed description of evidence requested or reason for appearing, and a reasonable time frame no shorter than 3 minutes. The individuals summoned by the subpoena must be informed of its presence for it to be a valid order. This can be done via general announcement, fax, verbal announcement, or otherwise.
Neither prosecution nor defense may interrupt one another during presentation of evidence, including objecting to witness statements made. In the case of a witness making demonstrably untrue statements, perjury and contempt of court may be charged as necessary. It is encouraged the judge pay attention and inquire about competing claims to truth.
=== Affidavits ===
An Affidavit is a legal document, consisting of a formal written statement of witness. An Affidavit must be written by the witness, or Affiant, who must state their relation to the incident at hand, and must be signed and stamped (if applicable) by a member of Security, Justice, or Command, who is verifying that the contents of the Affidavit are true to the best of their knowledge. The witness to the Affidavit must not be directly connected to the incident.
==Hearing Procedure==
A Hearing should take no longer than 10 minutes, and follows these steps:
Affidavits are considered valid witness statements for use in a trial (see Trial Procedure, below), and can be used either in tandem with a verbal witness statement, or to obtain testimony from an affiant whose job requirements and/or personal circumstances make attending a trial unfeasible.
* 0. Prosecution informs the Judge, the Accused and the Defense of all charges. The Accused is asked if they want to plead guilty.
* 1. The Judge confirms that every required person is present, and begins the hearing.
* 2. The Prosecution reads the charges. Any crimes not charged are not subject to the hearing and cannot be accounted for in sentencing.
* 3. The Prosecution presents all evidence to support the charges.
* 4. The Attorney contests the prosecution's version of events and presents their own evidence.
* 5. The Judge rules on whether the suspect reasonably has committed the crime. If the accused is found not guilty of a more severe version of a crime, the Judge may still find them guilty of a less severe version (for example if found not guilty of murder, the accused may be found guilty of manslaughter), despite the crime not being charged.
* 6. Any Ruling is enacted; if a party refuses to follow through with the ruling, they may suffer criminal penalties.
An affidavit may be impeached- and thus removed from evidence- if the Prosecution or Defense can introduce testimony or other evidence that directly contradicts the statements recorded within the Affidavit. The Affiant may then be liable for criminal charges.
Suspects who are found guilty of a part of the charged crimes, in a way where the total sentence does not exceed 25 minutes, may be released immediately, at the judges discretion.
A capital sentence should be approved under Space Law.
The default sentence for suspects guilty of a capital crime is permanent confinement. If there is no doubt of the suspects guilt of a capital crime, or a guilty plea to a capital crime, and the nature of the crime is judged to be extreme and heinous, the Judge is authorized to sentence the prisoner to be executed.
=Trial Procedure=
A trial may be requested for civil disputes and capital charges. Civil charges may be refused, depending on the availability and discretion of a potential Judge; however, capital crimes require a trial. This also includes non-capital trials required as dictated within Sentencing guidelines. Miscellaneous trials, such as those described within Paroles and Pardons, only require the Judge and the defendant.
Criminal trials may be waived if a suspect pleads guilty to a member of the Justice or Security department, or to station command. Their sentence should be passed by a Judge immediately, without the need for a full hearing. The Judge should also sentence the defendant summarily if they are held in contempt of court during the trial.
The trial takes the form of an arbitration court hearing, presided over by an appointed Judge, and including adequate Prosecution and Defense, as dictated below. All charges, arguments, and evidence must be filed with the Judge before the case begins. The trial shall begin when the Defense, Prosecution, and Judge are all present and ready to begin. A trial does not need to be open to the public.
The Judge is charged with ensuring the trial is completed in a timely manner, and is granted the power to charge someone with Contempt of Court, under Space Law, if they disrupt the case. Suitable Judges should be chosen from the following list, in sequential order:
# The Chief Justice
# The Clerk
# An impartial member of the Justice department.
# An impartial member of Station Command.
# The Warden.
The Defense should ideally be a member of the Justice department assigned to argue in favor of the defendant; if none are available, the defendant may choose to represent themselves.
The Prosecution should ideally be the Prosecutor; if they are unavailable, the case should be prosecuted by an unbiased member of Station Command or a law enforcement officer, in that order.
If there is not a viable and/or willing Defense or Prosecution, the Judge is fully authorized and strongly encouraged to pass a summary sentence of Extended Confinement until an adequate trial can be held.
==Criminal Trials==
A criminal case should take no longer than 25 minutes in total. Criminal cases should either have security personnel present, or plan to have them present by the end of the trial.
:#The judge brings the court in session.
:#Prosecution opens the proceedings, stating the charges and introducing witnesses and exhibits that will be brought forward during the presentation of evidence. They may also recommend a sentence to the judge, appropriate to the charge based on their opening statement.
:#Defense enters their plea- Guilty or Not Guilty.
:#If Guilty, the Judge is authorized to immediately pass a ruling, as stated below.
:#If Not Guilty, they are to provide their own opening statement and introduce their own witnesses and exhibits where applicable.
:#Presentation of evidence follows, lead by Prosecution, then Defense.
:#*Cross-examination may be requested following submission of individual exhibits or witnesses, and granted at the Judge's discretion.
:#Finally, closing statements are made by the Prosecution, Defense, and Prosecution once more due to burden of proof. No new evidence can be introduced within a closing statement.
:#A ruling will be issued by the Judge, written down, and stamped with the Notary stamp, the Judge's stamp, and their signature.
:#The Judge adjourns the court. Any present or soon-to-be-present Security personnel will then enact the ruling.
== Civil Trials ==
Civil cases do not require Prosecution or Defense, but they may receive assistance from either both before and during the case. Both the defendant and plaintiff must be made to summon at the court, if a party does not appear out of either refusal or negligence, or fails to comply with a court order from a civil trial, that party may be charged with contempt of court, a felony. A civil case involves seeking damages from a defendant, which could be monetary, an item, or otherwise. A civil case should take no longer than 15 minutes in total.
# The Judge brings the court into session.
# Plaintiff opens proceedings by stating the damages, before introducing witnesses and exhibits that will be brought forward during presentation of evidence. They may also reccomend civil relief to the judge, appropriate to the damages listed wthin their opening statement.
# Defendant follows with their opening statement, contradicting the Plaintiff's theories and assertions. They will also introduce their own witnesses and exhibits, where applicable.
# Presentation of evidence follows, led by the Plaintiff, then the Defendant.
#* Cross-examination may be requested following submission of individual exhibits or witnesses, and granted at the Judge's discretion.
# Finally, closing statements are made by the Plaintiff, Defense, and Plaintiff once more due to burden of proof. No new evidence should be introduced or referenced by these statements.
# A ruling will be made by the Judge, written, and stamped by the Notary stamp, the Judge's stamp, and signed by the Judge.
# The Judge adjourns court.
Any ruling shall then be enacted; if a party refuses to follow through with the ruling, they may suffer criminal penalties.
== Presentation of Evidence ==
===Eyewitness Testimony===
Any sophont or other entity willing and capable of presenting themselves before court under oath and present at the scene of the crime as a bystander or a victim at the time of its commission, may offer eyewitness testimony before the court at request of defense or prosecution. Extra time may be granted to either side at the behest of the Judge to allow for witnesses not currently present.
===Objection to Testimony===
Any witness that falls under the following conditions may be excused as a witness or have their testimony deemed inadmissible:
:*The witness lacks the mental, intellectual, or other capacity to understand the context of the case.
:*The witness's inclusion will cause substantial cost or delay to the case, proceedings, or upkeep of the station.
:*The witness is the judge.
===General Objections===
Both parties in a trial may object to testimony to have the testimony deemed inadmissible, move past the question, or rephrase a question to keep a trial within time limit:
:* '''Hearsay:''' The witness testifies using an out-of-court statement by an individual not present in court, thereby rendering it impossible to verify. This testimony will be inadmissible.
:* '''Asked and answered:''' A question is posed that has already been answered previously by a witness. Such a question must be skipped.
:* '''Narrative:''' A question is posed that would require a significantly long answer with multiple facts, or when the witness chooses to answer a question in this way. The question must be reworded, or the witness must be reminded to answer promptly and stay on topic.
Objections are completely at the discretion of the judge, who does not always need to sustain an objection even if it is within these guidelines. It is up to the judge to decide if an objection is proper or necessary to keep the trial quick and fair.
==Procedural Defense==
Whereas a defendant is placed on trial, they are granted the privilege to challenge the legitimacy of charges or claims brought against them by the legal process. Any of the following may be invoked:
:*Issue Preclusion/Double Jeopardy Clause: accused persons may not be tried on the same charges following an acquittal or conviction, except in the case of acquittal and subsequent credible admission of guilt.
:*Entrapment Clause: accused persons found to be induced or coerced by law enforcement into the commission of the crimes listed in the charges may be acquitted or have their sentence reduced.
:*Exclusionary Clause: evidence collected and/or analyzed in violation of accused persons rights under law may be inadmissible in court.
=Medical Regulations=
==Standard of Care==

View File

@ -5,6 +5,7 @@ import subprocess
import sys
import os
import shutil
import time
from pathlib import Path
from typing import List
@ -108,7 +109,21 @@ def reset_solution():
with SOLUTION_PATH.open("w") as f:
f.write(content)
def check_for_zip_download():
# Check if .git exists,
cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if not os.path.isdir(os.path.join(cur_dir, ".git")):
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
"Such as information to download the engine or even the ability to even be able to create contributions. \n"
"Please read and follow https://docs.spacestation14.com/en/general-development/setup/setting-up-a-development-environment.html \n"
"If you just want a Sandbox Server, you are following the wrong guide! You can download a premade server following the instructions here:"
"https://docs.spacestation14.com/en/general-development/setup/server-hosting-tutorial.html \n"
"Closing automatically in 30 seconds.")
time.sleep(30)
exit(1)
if __name__ == '__main__':
check_for_zip_download()
install_hooks()
update_submodules()

View File

@ -81,7 +81,23 @@ private EntityUid Slice(...)
If you want to make changes to a map, get in touch with its maintainer to make sure you don't both make changes at the same time.
Conflicts with maps make PRs mutually exclusive so either your work on the maintainer's work will be lost, communicate to avoid this!
Conflicts with maps make PRs mutually exclusive so either your work or the maintainer's work will be lost, communicate to avoid this!
Please make a detailed list of **all** changes(even minor changes) with locations when submitting a PR. This helps reviewers hone in on them without having to search an entire map for differences. Ex: [Map Edits](https://github.com/DeltaV-Station/Delta-v/pull/3165)
**Submitting a map PR**
Please limit changelogs on map PRs to **significant** map alterations or additions. Minor map edits do not need changelogs.
Format for map PRs looks like:
```
:cl: Yourname
MAPS: Mapname
- add: Added fun!
- remove: Removed fun!
- tweak: Changed fun!
- fix: Fixed fun!
```
# Before you submit

View File

@ -29,7 +29,7 @@ namespace Content.Benchmarks;
[CategoriesColumn]
public class ComponentQueryBenchmark
{
public const string Map = "Maps/atlas.yml";
public const string Map = "Maps/saltern.yml";
private TestPair _pair = default!;
private IEntityManager _entMan = default!;

View File

@ -1,15 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Running;
using Content.IntegrationTests;
using Content.Server.Maps;
#if DEBUG
using BenchmarkDotNet.Configs;
#else
using Robust.Benchmarks.Configs;
#endif
using Robust.Shared.Prototypes;
namespace Content.Benchmarks
{
@ -22,11 +14,15 @@ namespace Content.Benchmarks
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("\nWARNING: YOU ARE RUNNING A DEBUG BUILD, USE A RELEASE BUILD FOR AN ACCURATE BENCHMARK");
Console.WriteLine("THE DEBUG BUILD IS ONLY GOOD FOR FIXING A CRASHING BENCHMARK\n");
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
var baseConfig = new DebugInProcessConfig();
#else
var config = Environment.GetEnvironmentVariable("ROBUST_BENCHMARKS_ENABLE_SQL") != null ? DefaultSQLConfig.Instance : null;
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
var baseConfig = Environment.GetEnvironmentVariable("ROBUST_BENCHMARKS_ENABLE_SQL") != null
? DefaultSQLConfig.Instance
: DefaultConfig.Instance;
#endif
var config = ManualConfig.Create(baseConfig);
config.BuildTimeout = TimeSpan.FromMinutes(5);
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
}
}
}

View File

@ -7,6 +7,7 @@ using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Server.Warps;
using Content.Shared.Warps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;

View File

@ -0,0 +1,126 @@
#nullable enable
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
namespace Content.Benchmarks;
[Virtual]
public class RaiseEventBenchmark
{
private TestPair _pair = default!;
private BenchSystem _sys = default!;
[GlobalSetup]
public void Setup()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(BenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
var entMan = _pair.Server.EntMan;
_sys = entMan.System<BenchSystem>();
_pair.Server.WaitPost(() =>
{
var uid = entMan.Spawn();
_sys.Ent = new(uid, entMan.GetComponent<TransformComponent>(uid));
_sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp);
})
.GetAwaiter()
.GetResult();
}
[GlobalCleanup]
public async Task Cleanup()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
[Benchmark(Baseline = true)]
public int RaiseEvent()
{
return _sys.RaiseEvent();
}
[Benchmark]
public int RaiseCompEvent()
{
return _sys.RaiseCompEvent();
}
[Benchmark]
public int RaiseICompEvent()
{
return _sys.RaiseICompEvent();
}
[Benchmark]
public int RaiseCSharpEvent()
{
return _sys.CSharpEvent();
}
public sealed class BenchSystem : EntitySystem
{
public Entity<TransformComponent> Ent;
public Entity<IComponent> Ent2;
public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev);
public event EntityEventHandler? OnCSharpEvent;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TransformComponent, BenchEv>(OnEvent);
OnCSharpEvent += OnEvent;
}
public int RaiseEvent()
{
var ev = new BenchEv();
RaiseLocalEvent(Ent.Owner, ref ev);
return ev.N;
}
public int RaiseCompEvent()
{
var ev = new BenchEv();
EntityManager.EventBus.RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
return ev.N;
}
public int RaiseICompEvent()
{
// Raise with an IComponent instead of concrete type
var ev = new BenchEv();
EntityManager.EventBus.RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
return ev.N;
}
public int CSharpEvent()
{
var ev = new BenchEv();
OnCSharpEvent?.Invoke(Ent.Owner, Ent.Comp, ref ev);
return ev.N;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void OnEvent(EntityUid uid, TransformComponent component, ref BenchEv args)
{
args.N += uid.Id;
}
[ByRefEvent]
public struct BenchEv
{
public int N;
}
}
}

View File

@ -4,39 +4,20 @@ using Robust.Shared.Console;
namespace Content.Client.Access.Commands;
public sealed class ShowAccessReadersCommand : IConsoleCommand
public sealed class ShowAccessReadersCommand : LocalizedEntityCommands
{
public string Command => "showaccessreaders";
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
public string Description => "Toggles showing access reader permissions on the map";
public string Help => """
Overlay Info:
-Disabled | The access reader is disabled
+Unrestricted | The access reader has no restrictions
+Set [Index]: [Tag Name]| A tag in an access set (accessor needs all tags in the set to be allowed by the set)
+Key [StationUid]: [StationRecordKeyId] | A StationRecordKey that is allowed
-Tag [Tag Name] | A tag that is not allowed (takes priority over other allows)
""";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "showaccessreaders";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var collection = IoCManager.Instance;
var existing = _overlay.RemoveOverlay<AccessOverlay>();
if (!existing)
_overlay.AddOverlay(new AccessOverlay(EntityManager, _cache, _xform));
if (collection == null)
return;
var overlay = collection.Resolve<IOverlayManager>();
if (overlay.RemoveOverlay<AccessOverlay>())
{
shell.WriteLine($"Set access reader debug overlay to false");
return;
}
var entManager = collection.Resolve<IEntityManager>();
var cache = collection.Resolve<IResourceCache>();
var xform = entManager.System<SharedTransformSystem>();
overlay.AddOverlay(new AccessOverlay(entManager, cache, xform));
shell.WriteLine($"Set access reader debug overlay to true");
shell.WriteLine(Loc.GetString($"cmd-showaccessreaders-status", ("status", !existing)));
}
}

View File

@ -0,0 +1,26 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Orientation="Horizontal"
Margin="10 10 10 10"
VerticalExpand="True"
HorizontalExpand="True"
MinHeight="70">
<!-- Access groups -->
<BoxContainer Name="AccessGroupList" Access="Public" Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.5" Margin="0 0 10 0">
<!-- Populated with C# code -->
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" VerticalExpand="True" Margin="0 0 0 0" SetWidth="2">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#FFFFFF" />
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Access levels -->
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="10 0 0 0">
<BoxContainer Name="AccessLevelChecklist" Access="Public" Orientation="Vertical" HorizontalAlignment="Left">
<!-- Populated with C# code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>

View File

@ -0,0 +1,449 @@
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Access;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Linq;
using System.Numerics;
namespace Content.Client.Access.UI;
[GenerateTypedNameReferences]
public sealed partial class GroupedAccessLevelChecklist : BoxContainer
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private bool _isMonotone;
private string? _labelStyleClass;
// Access data
private HashSet<ProtoId<AccessGroupPrototype>> _accessGroups = new();
private HashSet<ProtoId<AccessLevelPrototype>> _accessLevels = new();
private HashSet<ProtoId<AccessLevelPrototype>> _activeAccessLevels = new();
// Button groups
private readonly ButtonGroup _accessGroupsButtons = new();
// Temp values
private int _accessGroupTabIndex = 0;
private bool _canInteract = false;
private List<AccessLevelPrototype> _accessLevelsForTab = new();
private readonly List<AccessLevelEntry> _accessLevelEntries = new();
private readonly Dictionary<AccessGroupPrototype, List<AccessLevelPrototype>> _groupedAccessLevels = new();
// Events
public event Action<HashSet<ProtoId<AccessLevelPrototype>>, bool>? OnAccessLevelsChangedEvent;
/// <summary>
/// Creates a UI control for changing access levels.
/// Access levels are organized under a list of tabs by their associated access group.
/// </summary>
public GroupedAccessLevelChecklist()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
private void ArrangeAccessControls()
{
// Create a list of known access groups with which to populate the UI
_groupedAccessLevels.Clear();
foreach (var accessGroup in _accessGroups)
{
if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
continue;
_groupedAccessLevels.Add(accessGroupProto, new());
}
// Ensure that the 'general' access group is added to handle
// misc. access levels that aren't associated with any group
if (_protoManager.TryIndex<AccessGroupPrototype>("General", out var generalAccessProto))
_groupedAccessLevels.TryAdd(generalAccessProto, new());
// Assign known access levels with their associated groups
foreach (var accessLevel in _accessLevels)
{
if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
continue;
var assigned = false;
foreach (var (accessGroup, accessLevels) in _groupedAccessLevels)
{
if (!accessGroup.Tags.Contains(accessLevelProto.ID))
continue;
assigned = true;
_groupedAccessLevels[accessGroup].Add(accessLevelProto);
}
if (!assigned && generalAccessProto != null)
_groupedAccessLevels[generalAccessProto].Add(accessLevelProto);
}
// Remove access groups that have no assigned access levels
foreach (var (group, accessLevels) in _groupedAccessLevels)
{
if (accessLevels.Count == 0)
_groupedAccessLevels.Remove(group);
}
}
private bool TryRebuildAccessGroupControls()
{
AccessGroupList.DisposeAllChildren();
AccessLevelChecklist.DisposeAllChildren();
// No access level prototypes were assigned to any of the access level groups.
// Either the turret controller has no assigned access levels or their names were invalid.
if (_groupedAccessLevels.Count == 0)
return false;
// Reorder the access groups alphabetically
var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
// Add group access buttons to the UI
foreach (var accessGroup in orderedAccessGroups)
{
var accessGroupButton = CreateAccessGroupButton();
// Button styling
if (_groupedAccessLevels.Count > 1)
{
if (AccessGroupList.ChildCount == 0)
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft);
else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1))
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight);
else
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth);
}
accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup);
// Label text and styling
if (_labelStyleClass != null)
accessGroupButton.Label.SetOnlyStyleClass(_labelStyleClass);
var accessLevelPrototypes = _groupedAccessLevels[accessGroup];
var prefix = accessLevelPrototypes.All(x => _activeAccessLevels.Contains(x))
? "»"
: accessLevelPrototypes.Any(x => _activeAccessLevels.Contains(x))
? ""
: " ";
var text = Loc.GetString(
"turret-controls-window-access-group-label",
("prefix", prefix),
("label", accessGroup.GetAccessGroupName())
);
accessGroupButton.Text = text;
// Button events
accessGroupButton.OnPressed += _ => OnAccessGroupChanged(accessGroupButton.GetPositionInParent());
AccessGroupList.AddChild(accessGroupButton);
}
// Adjust the current tab index so it remains in range
if (_accessGroupTabIndex >= _groupedAccessLevels.Count)
_accessGroupTabIndex = _groupedAccessLevels.Count - 1;
return true;
}
/// <summary>
/// Rebuilds the checkbox list for the access level controls.
/// </summary>
public void RebuildAccessLevelsControls()
{
AccessLevelChecklist.DisposeAllChildren();
_accessLevelEntries.Clear();
// No access level prototypes were assigned to any of the access level groups
// Either turret controller has no assigned access levels, or their names were invalid
if (_groupedAccessLevels.Count == 0)
return;
// Reorder the access groups alphabetically
var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
// Get the access levels associated with the current tab
var selectedAccessGroupTabProto = orderedAccessGroups[_accessGroupTabIndex];
_accessLevelsForTab = _groupedAccessLevels[selectedAccessGroupTabProto];
_accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToList();
// Add an 'all' checkbox as the first child of the list if it has more than one access level
// Toggling this checkbox on will mark all other boxes below it on/off
var allCheckBox = CreateAccessLevelCheckbox();
allCheckBox.Text = Loc.GetString("turret-controls-window-all-checkbox");
if (_labelStyleClass != null)
allCheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
// Add the 'all' checkbox events
allCheckBox.OnPressed += args =>
{
SetCheckBoxPressedState(_accessLevelEntries, allCheckBox.Pressed);
var accessLevels = new HashSet<ProtoId<AccessLevelPrototype>>();
foreach (var accessLevel in _accessLevelsForTab)
{
accessLevels.Add(accessLevel);
}
OnAccessLevelsChangedEvent?.Invoke(accessLevels, allCheckBox.Pressed);
};
AccessLevelChecklist.AddChild(allCheckBox);
// Hide the 'all' checkbox if the tab has only one access level
var allCheckBoxVisible = _accessLevelsForTab.Count > 1;
allCheckBox.Visible = allCheckBoxVisible;
allCheckBox.Disabled = !_canInteract;
// Add any remaining missing access level buttons to the UI
foreach (var accessLevel in _accessLevelsForTab)
{
// Create the entry
var accessLevelEntry = new AccessLevelEntry(_isMonotone);
accessLevelEntry.AccessLevel = accessLevel;
accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName();
accessLevelEntry.CheckBox.Pressed = _activeAccessLevels.Contains(accessLevel);
accessLevelEntry.CheckBox.Disabled = !_canInteract;
if (_labelStyleClass != null)
accessLevelEntry.CheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
// Set the checkbox linkage lines
var isEndOfList = _accessLevelsForTab.IndexOf(accessLevel) == (_accessLevelsForTab.Count - 1);
var lines = new List<(Vector2, Vector2)>
{
(new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)),
(new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)),
};
accessLevelEntry.UpdateCheckBoxLink(lines);
accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible;
accessLevelEntry.CheckBoxLink.Modulate = !_canInteract ? Color.Gray : Color.White;
// Add checkbox events
accessLevelEntry.CheckBox.OnPressed += args =>
{
// If the checkbox and its siblings are checked, check the 'all' checkbox too
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
OnAccessLevelsChangedEvent?.Invoke([accessLevelEntry.AccessLevel], accessLevelEntry.CheckBox.Pressed);
};
AccessLevelChecklist.AddChild(accessLevelEntry);
_accessLevelEntries.Add(accessLevelEntry);
}
// Press the 'all' checkbox if all others are pressed
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
}
private bool AreAllCheckBoxesPressed(IEnumerable<CheckBox> checkBoxes)
{
foreach (var checkBox in checkBoxes)
{
if (!checkBox.Pressed)
return false;
}
return true;
}
private void SetCheckBoxPressedState(List<AccessLevelEntry> accessLevelEntries, bool pressed)
{
foreach (var accessLevelEntry in accessLevelEntries)
{
accessLevelEntry.CheckBox.Pressed = pressed;
}
}
/// <summary>
/// Provides the UI with a list of access groups using which list of tabs should be populated.
/// </summary>
public void SetAccessGroups(HashSet<ProtoId<AccessGroupPrototype>> accessGroups)
{
_accessGroups = accessGroups;
ArrangeAccessControls();
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Provides the UI with a list of access levels with which it can populate the currently selected tab.
/// </summary>
public void SetAccessLevels(HashSet<ProtoId<AccessLevelPrototype>> accessLevels)
{
_accessLevels = accessLevels;
ArrangeAccessControls();
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Sets which access level checkboxes should be marked on the UI.
/// </summary>
public void SetActiveAccessLevels(HashSet<ProtoId<AccessLevelPrototype>> activeAccessLevels)
{
_activeAccessLevels = activeAccessLevels;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Sets whether the local player can interact with the checkboxes.
/// </summary>
public void SetLocalPlayerAccessibility(bool canInteract)
{
_canInteract = canInteract;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Sets whether the UI should use monotone buttons and checkboxes.
/// </summary>
public void SetMonotone(bool monotone)
{
_isMonotone = monotone;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
/// <summary>
/// Applies the specified style to the labels on the UI buttons and checkboxes.
/// </summary>
public void SetLabelStyleClass(string? styleClass)
{
_labelStyleClass = styleClass;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
private void OnAccessGroupChanged(int newTabIndex)
{
if (newTabIndex == _accessGroupTabIndex)
return;
_accessGroupTabIndex = newTabIndex;
if (TryRebuildAccessGroupControls())
RebuildAccessLevelsControls();
}
private Button CreateAccessGroupButton()
{
var button = _isMonotone ? new MonotoneButton() : new Button();
button.ToggleMode = true;
button.Group = _accessGroupsButtons;
button.Label.HorizontalAlignment = HAlignment.Left;
return button;
}
private CheckBox CreateAccessLevelCheckbox()
{
var checkbox = _isMonotone ? new MonotoneCheckBox() : new CheckBox();
checkbox.Margin = new Thickness(0, 0, 0, 3);
checkbox.ToggleMode = true;
checkbox.ReservesSpace = false;
return checkbox;
}
private sealed class AccessLevelEntry : BoxContainer
{
public ProtoId<AccessLevelPrototype> AccessLevel;
public readonly CheckBox CheckBox;
public readonly LineRenderer CheckBoxLink;
public AccessLevelEntry(bool monotone)
{
HorizontalExpand = true;
CheckBoxLink = new LineRenderer
{
SetWidth = 22,
VerticalExpand = true,
Margin = new Thickness(0, -1),
ReservesSpace = false,
};
AddChild(CheckBoxLink);
CheckBox = monotone ? new MonotoneCheckBox() : new CheckBox();
CheckBox.ToggleMode = true;
CheckBox.Margin = new Thickness(0f, 0f, 0f, 3f);
AddChild(CheckBox);
}
public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines)
{
CheckBoxLink.Lines = lines;
}
}
private sealed class LineRenderer : Control
{
/// <summary>
/// List of lines to render (their start and end x-y coordinates).
/// Position (0,0) is the top left corner of the control and
/// position (1,1) is the bottom right corner.
/// </summary>
/// <remarks>
/// The color of the lines is inherited from the control.
/// </remarks>
public List<(Vector2, Vector2)> Lines;
public LineRenderer()
{
Lines = new List<(Vector2, Vector2)>();
}
public LineRenderer(List<(Vector2, Vector2)> lines)
{
Lines = lines;
}
protected override void Draw(DrawingHandleScreen handle)
{
foreach (var line in Lines)
{
var start = PixelPosition +
new Vector2(PixelWidth * line.Item1.X, PixelHeight * line.Item1.Y);
var end = PixelPosition +
new Vector2(PixelWidth * line.Item2.X, PixelHeight * line.Item2.Y);
handle.DrawLine(start, end, ActualModulateSelf);
}
}
}
}

View File

@ -1,8 +1,10 @@
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
@ -11,13 +13,21 @@ namespace Content.Client.Access.UI
public sealed class IdCardConsoleBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
private readonly SharedIdCardConsoleSystem _idCardConsoleSystem = default!;
private IdCardConsoleWindow? _window;
// CCVar.
private int _maxNameLength;
private int _maxIdJobLength;
public IdCardConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_idCardConsoleSystem = EntMan.System<SharedIdCardConsoleSystem>();
_maxNameLength =_cfgManager.GetCVar(CCVars.MaxNameLength);
_maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength);
}
protected override void Open()
@ -67,11 +77,11 @@ namespace Content.Client.Access.UI
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, string newJobPrototype)
{
if (newFullName.Length > MaxFullNameLength)
newFullName = newFullName[..MaxFullNameLength];
if (newFullName.Length > _maxNameLength)
newFullName = newFullName[.._maxNameLength];
if (newJobTitle.Length > MaxJobTitleLength)
newJobTitle = newJobTitle[..MaxJobTitleLength];
if (newJobTitle.Length > _maxIdJobLength)
newJobTitle = newJobTitle[.._maxIdJobLength];
SendMessage(new WriteToTargetIdMessage(
newFullName,

View File

@ -1,11 +1,13 @@
using System.Linq;
using Content.Shared.Access;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
@ -14,12 +16,17 @@ namespace Content.Client.Access.UI
[GenerateTypedNameReferences]
public sealed partial class IdCardConsoleWindow : DefaultWindow
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly ISawmill _logMill = default!;
private readonly IdCardConsoleBoundUserInterface _owner;
// CCVar.
private int _maxNameLength;
private int _maxIdJobLength;
private AccessLevelControl _accessButtons = new();
private readonly List<string> _jobPrototypeIds = new();
@ -41,7 +48,11 @@ namespace Content.Client.Access.UI
_owner = owner;
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
_maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength);
FullNameLineEdit.OnTextEntered += _ => SubmitData();
FullNameLineEdit.IsValid = s => s.Length <= _maxNameLength;
FullNameLineEdit.OnTextChanged += _ =>
{
FullNameSaveButton.Disabled = FullNameSaveButton.Text == _lastFullName;
@ -49,6 +60,7 @@ namespace Content.Client.Access.UI
FullNameSaveButton.OnPressed += _ => SubmitData();
JobTitleLineEdit.OnTextEntered += _ => SubmitData();
JobTitleLineEdit.IsValid = s => s.Length <= _maxIdJobLength;
JobTitleLineEdit.OnTextChanged += _ =>
{
JobTitleSaveButton.Disabled = JobTitleLineEdit.Text == _lastJobTitle;
@ -80,16 +92,8 @@ namespace Content.Client.Access.UI
}
}
private void ClearAllAccess()
{
foreach (var button in _accessButtons.ButtonsList.Values)
{
if (button.Pressed)
{
button.Pressed = false;
}
}
}
// DeltaV - removed as part of job preset access fix
// private void ClearAllAccess()
private void SelectJobPreset(OptionButton.ItemSelectedEventArgs args)
{
@ -101,34 +105,28 @@ namespace Content.Client.Access.UI
JobTitleLineEdit.Text = Loc.GetString(job.Name);
args.Button.SelectId(args.Id);
ClearAllAccess();
// this is a sussy way to do this
foreach (var access in job.Access)
{
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
{
button.Pressed = true;
}
}
// DeltaV - start of job preset access fix
SubmitData();
var targetAccesses = job.Access.ToHashSet();
foreach (var group in job.AccessGroups)
{
if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}
foreach (var access in groupPrototype.Tags)
{
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
{
button.Pressed = true;
}
}
targetAccesses.UnionWith(groupPrototype.Tags);
}
SubmitData();
// this is a sussy way to do this
foreach (var (id, button) in _accessButtons.ButtonsList)
{
if (!button.Disabled && button.Pressed != targetAccesses.Contains(id))
{
OnToggleAccess?.Invoke(id);
}
}
// DeltaV - end of job preset access fix
}
public void UpdateState(IdCardConsoleBoundUserInterfaceState state)
@ -177,7 +175,8 @@ namespace Content.Client.Access.UI
new List<ProtoId<AccessLevelPrototype>>());
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
// If the job index is < 0 that means they don't have a job registered in the station records.
// If the job index is < 0 that means they don't have a job registered in the station records
// or the IdCardComponent's JobPrototype field.
// For example, a new ID from a box would have no job index.
if (jobIndex < 0)
{

View File

@ -1,3 +1,6 @@
using Content.Shared.Actions.Components;
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
namespace Content.Client.Actions;
/// <summary>
@ -7,3 +10,17 @@ public sealed class FillActionSlotEvent : EntityEventArgs
{
public EntityUid? Action;
}
/// <summary>
/// Client-side event used to attempt to trigger a targeted action.
/// This only gets raised if the has <see cref="TargetActionComponent">.
/// Handlers must set <c>Handled</c> to true, then if the action has been performed,
/// i.e. a target is found, then FoundTarget must be set to true.
/// </summary>
[ByRefEvent]
public record struct ActionTargetAttemptEvent(
PointerInputCmdArgs Input,
Entity<ActionsComponent> User,
ActionComponent Action,
bool Handled = false,
bool FoundTarget = false);

View File

@ -1,18 +1,23 @@
using System.IO;
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Mapping;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.ContentPack;
using Robust.Shared.GameStates;
using Robust.Shared.Input.Binding;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
@ -23,9 +28,10 @@ namespace Content.Client.Actions
{
public delegate void OnActionReplaced(EntityUid actionId);
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public event Action<EntityUid>? OnActionAdded;
@ -37,156 +43,67 @@ namespace Content.Client.Actions
public event Action<List<SlotAssignment>>? AssignSlot;
private readonly List<EntityUid> _removed = new();
private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
private readonly List<Entity<ActionComponent>> _added = new();
public static readonly EntProtoId MappingEntityAction = "BaseMappingEntityAction";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActionsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ActionsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
SubscribeLocalEvent<ActionComponent, AfterAutoHandleStateEvent>(OnActionAutoHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ActionTargetAttemptEvent>(OnEntityTargetAttempt);
SubscribeLocalEvent<WorldTargetActionComponent, ActionTargetAttemptEvent>(OnWorldTargetAttempt);
}
public override void FrameUpdate(float frameTime)
private void OnActionAutoHandleState(Entity<ActionComponent> ent, ref AfterAutoHandleStateEvent args)
{
base.FrameUpdate(frameTime);
var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
while (worldActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
while (instantActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
while (entityActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
UpdateAction(ent);
}
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
public override void UpdateAction(Entity<ActionComponent> ent)
{
if (args.Current is not InstantActionComponentState state)
return;
BaseHandleState<InstantActionComponent>(uid, component, state);
}
private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not EntityTargetActionComponentState state)
return;
component.Whitelist = state.Whitelist;
component.Blacklist = state.Blacklist;
component.CanTargetSelf = state.CanTargetSelf;
BaseHandleState<EntityTargetActionComponent>(uid, component, state);
}
private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not WorldTargetActionComponentState state)
return;
BaseHandleState<WorldTargetActionComponent>(uid, component, state);
}
private void OnEntityWorldTargetHandleState(EntityUid uid,
EntityWorldTargetActionComponent component,
ref ComponentHandleState args)
{
if (args.Current is not EntityWorldTargetActionComponentState state)
return;
component.Whitelist = state.Whitelist;
component.CanTargetSelf = state.CanTargetSelf;
BaseHandleState<EntityWorldTargetActionComponent>(uid, component, state);
}
private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
{
// TODO ACTIONS use auto comp states
component.Icon = state.Icon;
component.IconOn = state.IconOn;
component.IconColor = state.IconColor;
component.OriginalIconColor = state.OriginalIconColor;
component.DisabledIconColor = state.DisabledIconColor;
component.Keywords.Clear();
component.Keywords.UnionWith(state.Keywords);
component.Enabled = state.Enabled;
component.Toggled = state.Toggled;
component.Cooldown = state.Cooldown;
component.UseDelay = state.UseDelay;
component.Charges = state.Charges;
component.MaxCharges = state.MaxCharges;
component.RenewCharges = state.RenewCharges;
component.Container = EnsureEntity<T>(state.Container, uid);
component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
component.CheckCanInteract = state.CheckCanInteract;
component.CheckConsciousness = state.CheckConsciousness;
component.ClientExclusive = state.ClientExclusive;
component.Priority = state.Priority;
component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
component.RaiseOnUser = state.RaiseOnUser;
component.RaiseOnAction = state.RaiseOnAction;
component.AutoPopulate = state.AutoPopulate;
component.Temporary = state.Temporary;
component.ItemIconStyle = state.ItemIconStyle;
component.Sound = state.Sound;
UpdateAction(uid, component);
}
public override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
{
if (!ResolveActionData(actionId, ref action))
return;
action.IconColor = action.Charges < 1 ? action.DisabledIconColor : action.OriginalIconColor;
base.UpdateAction(actionId, action);
if (_playerManager.LocalEntity != action.AttachedEntity)
// TODO: Decouple this.
ent.Comp.IconColor = _sharedCharges.GetCurrentCharges(ent.Owner) == 0 ? ent.Comp.DisabledIconColor : ent.Comp.OriginalIconColor;
base.UpdateAction(ent);
if (_playerManager.LocalEntity != ent.Comp.AttachedEntity)
return;
ActionsUpdated?.Invoke();
}
private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
private void OnHandleState(Entity<ActionsComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not ActionsComponentState state)
return;
var (uid, comp) = ent;
_added.Clear();
_removed.Clear();
var stateEnts = EnsureEntitySet<ActionsComponent>(state.Actions, uid);
foreach (var act in component.Actions)
foreach (var act in comp.Actions)
{
if (!stateEnts.Contains(act) && !IsClientSide(act))
_removed.Add(act);
}
component.Actions.ExceptWith(_removed);
comp.Actions.ExceptWith(_removed);
foreach (var actionId in stateEnts)
{
if (!actionId.IsValid())
continue;
if (!component.Actions.Add(actionId))
if (!comp.Actions.Add(actionId))
continue;
TryGetActionData(actionId, out var action);
_added.Add((actionId, action));
if (GetAction(actionId) is {} action)
_added.Add(action);
}
if (_playerManager.LocalEntity != uid)
@ -201,45 +118,46 @@ namespace Content.Client.Actions
foreach (var action in _added)
{
OnActionAdded?.Invoke(action.Item1);
OnActionAdded?.Invoke(action);
}
ActionsUpdated?.Invoke();
}
public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
public static int ActionComparer(Entity<ActionComponent> a, Entity<ActionComponent> b)
{
var priorityA = a.Item2?.Priority ?? 0;
var priorityB = b.Item2?.Priority ?? 0;
var priorityA = a.Comp?.Priority ?? 0;
var priorityB = b.Comp?.Priority ?? 0;
if (priorityA != priorityB)
return priorityA - priorityB;
priorityA = a.Item2?.Container?.Id ?? 0;
priorityB = b.Item2?.Container?.Id ?? 0;
priorityA = a.Comp?.Container?.Id ?? 0;
priorityB = b.Comp?.Container?.Id ?? 0;
return priorityA - priorityB;
}
protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
BaseActionComponent action)
protected override void ActionAdded(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
{
if (_playerManager.LocalEntity != performer)
if (_playerManager.LocalEntity != performer.Owner)
return;
OnActionAdded?.Invoke(actionId);
OnActionAdded?.Invoke(action);
ActionsUpdated?.Invoke();
}
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
protected override void ActionRemoved(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
{
if (_playerManager.LocalEntity != performer)
if (_playerManager.LocalEntity != performer.Owner)
return;
OnActionRemoved?.Invoke(actionId);
OnActionRemoved?.Invoke(action);
ActionsUpdated?.Invoke();
}
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
public IEnumerable<Entity<ActionComponent>> GetClientActions()
{
if (_playerManager.LocalEntity is not { } user)
return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
return Enumerable.Empty<Entity<ActionComponent>>();
return GetActions(user);
}
@ -276,24 +194,23 @@ namespace Content.Client.Actions
CommandBinds.Unregister<ActionsSystem>();
}
public void TriggerAction(EntityUid actionId, BaseActionComponent action)
public void TriggerAction(Entity<ActionComponent> action)
{
if (_playerManager.LocalEntity is not { } user ||
!TryComp(user, out ActionsComponent? actions))
{
return;
}
if (action is not InstantActionComponent instantAction)
if (_playerManager.LocalEntity is not { } user)
return;
if (action.ClientExclusive)
// TODO: unhardcode this somehow
if (!HasComp<InstantActionComponent>(action))
return;
if (action.Comp.ClientExclusive)
{
PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
PerformAction(user, action);
}
else
{
var request = new RequestPerformActionEvent(GetNetEntity(actionId));
var request = new RequestPerformActionEvent(GetNetEntity(action));
EntityManager.RaisePredictiveEvent(request);
}
}
@ -317,41 +234,64 @@ namespace Content.Client.Actions
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
return;
var actions = EnsureComp<ActionsComponent>(user);
ClearActions(); // DeltaV - this is needed here for mapping XX commands to work properly
ClearAssignments?.Invoke();
var assignments = new List<SlotAssignment>();
foreach (var entry in sequence.Sequence)
{
if (entry is not MappingDataNode map)
continue;
if (!map.TryGet("action", out var actionNode))
continue;
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
var actionId = Spawn();
AddComp(actionId, action);
AddActionDirect(user, actionId);
if (map.TryGet<ValueDataNode>("name", out var nameNode))
_metaData.SetEntityName(actionId, nameNode.Value);
if (!map.TryGet("assignments", out var assignmentNode))
continue;
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
foreach (var index in nodeAssignments)
var actionId = EntityUid.Invalid;
if (map.TryGet<ValueDataNode>("action", out var actionNode))
{
var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
assignments.Add(assignment);
var id = new EntProtoId(actionNode.Value);
actionId = Spawn(id);
}
else if (map.TryGet<ValueDataNode>("entity", out var entityNode))
{
var id = new EntProtoId(entityNode.Value);
var proto = _proto.Index(id);
actionId = Spawn(MappingEntityAction);
SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id));
SetEvent(actionId, new StartPlacementActionEvent()
{
PlacementOption = "SnapgridCenter",
EntityType = id
});
_metaData.SetEntityName(actionId, proto.Name);
}
else if (map.TryGet<ValueDataNode>("tileId", out var tileNode))
{
var id = new ProtoId<ContentTileDefinition>(tileNode.Value);
var proto = _proto.Index(id);
actionId = Spawn(MappingEntityAction);
if (proto.Sprite is {} sprite)
SetIcon(actionId, new SpriteSpecifier.Texture(sprite));
SetEvent(actionId, new StartPlacementActionEvent()
{
PlacementOption = "AlignTileAny",
TileId = id
});
_metaData.SetEntityName(actionId, Loc.GetString(proto.Name));
}
else
{
Log.Error($"Mapping actions from {path} had unknown action data!");
continue;
}
}
AssignSlot?.Invoke(assignments);
AddActionDirect((user, actions), actionId);
}
}
// DeltaV - begin changes
/// <summary>
/// Load actions and their toolbar assignments from a file.
/// DeltaV - Load from an existing yaml stream instead
@ -364,56 +304,162 @@ namespace Content.Client.Actions
if (stream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
return;
ClearAssignments?.Invoke();
var actions = EnsureComp<ActionsComponent>(user);
var assignments = new List<SlotAssignment>();
var existingActions = GetClientActions();
var existingActionsList = existingActions.ToList();
// We need to clear previously loaded actions, otherwise they keep adding up, filling the screen
ClearActions();
ClearAssignments?.Invoke();
foreach (var entry in sequence.Sequence)
{
if (entry is not MappingDataNode map)
continue;
if (!map.TryGet("action", out var actionNode))
continue;
if (!map.TryGet<ValueDataNode>("name", out var nameNode))
continue;
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
// Prevent spawning actions multiple times
var existing = existingActionsList.FirstOrNull(a =>
Name(a.Id) == nameNode.Value);
EntityUid actionId;
if (existing == null)
{
actionId = Spawn(null);
AddComp(actionId, action);
_metaData.SetEntityName(actionId, nameNode.Value);
DirtyEntity(actionId);
AddActionDirect(user, actionId);
}
else
{
actionId = existing.Value.Id;
}
if (!map.TryGet("assignments", out var assignmentNode))
continue;
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
foreach (var index in nodeAssignments)
var actionId = EntityUid.Invalid;
if (map.TryGet<ValueDataNode>("action", out var actionNode))
{
var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
assignments.Add(assignment);
var id = new EntProtoId(actionNode.Value);
actionId = Spawn(id);
}
else if (map.TryGet<ValueDataNode>("entity", out var entityNode))
{
var id = new EntProtoId(entityNode.Value);
var proto = _proto.Index(id);
actionId = Spawn(MappingEntityAction);
SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id));
SetEvent(actionId, new StartPlacementActionEvent()
{
PlacementOption = "SnapgridCenter",
EntityType = id
});
_metaData.SetEntityName(actionId, proto.Name);
}
else if (map.TryGet<ValueDataNode>("tileId", out var tileNode))
{
var id = new ProtoId<ContentTileDefinition>(tileNode.Value);
var proto = _proto.Index(id);
actionId = Spawn(MappingEntityAction);
if (proto.Sprite is {} sprite)
SetIcon(actionId, new SpriteSpecifier.Texture(sprite));
SetEvent(actionId, new StartPlacementActionEvent()
{
PlacementOption = "AlignTileAny",
TileId = id
});
_metaData.SetEntityName(actionId, Loc.GetString(proto.Name));
}
else
{
Log.Error($"Mapping actions from yamlstream had unknown action data!");
continue;
}
AddActionDirect((user, actions), actionId);
}
}
private void ClearActions()
{
var existingActions = GetClientActions().ToList();
foreach (var actionId in existingActions)
{
var meta = MetaData(actionId);
if (meta.EntityPrototype == null)
continue;
if (meta.EntityPrototype.ID == MappingEntityAction ||
meta.EntityPrototype.ID == "ActionMappingEraser")
{
RemoveAction(actionId.AsNullable());
}
}
}
// DeltaV - end changes
AssignSlot?.Invoke(assignments);
private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
{
if (args.Handled)
return;
args.Handled = true;
var (uid, comp) = ent;
var action = args.Action;
var coords = args.Input.Coordinates;
var user = args.User;
if (!ValidateWorldTarget(user, coords, ent))
return;
// optionally send the clicked entity too, if it matches its whitelist etc
// this is the actual entity-world targeting magic
EntityUid? targetEnt = null;
if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
args.Input.EntityUid != null &&
ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
{
targetEnt = args.Input.EntityUid;
}
if (action.ClientExclusive)
{
// TODO: abstract away from single event or maybe just RaiseLocalEvent?
if (comp.Event is {} ev)
{
ev.Target = coords;
ev.Entity = targetEnt;
}
PerformAction((user, user.Comp), (uid, action));
}
else
RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(targetEnt), GetNetCoordinates(coords)));
args.FoundTarget = true;
}
private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (args.Input.EntityUid is not { Valid: true } entity)
return;
// let world target component handle it
var (uid, comp) = ent;
if (comp.Event is not {} ev)
{
DebugTools.Assert(HasComp<WorldTargetActionComponent>(ent), $"Action {ToPrettyString(ent)} requires WorldTargetActionComponent for entity-world targeting");
return;
}
var action = args.Action;
var user = args.User;
if (!ValidateEntityTarget(user, entity, ent))
return;
if (action.ClientExclusive)
{
ev.Target = entity;
PerformAction((user, user.Comp), (uid, action));
}
else
{
RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(entity)));
}
args.FoundTarget = true;
}
public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);

View File

@ -6,6 +6,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
@ -22,10 +23,11 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly SharedRoleSystem _roles;
private readonly Font _font;
private readonly Font _fontBold;
private bool _overlayClassic;
private bool _overlaySymbols;
private AdminOverlayAntagFormat _overlayFormat;
private AdminOverlayAntagSymbolStyle _overlaySymbolStyle;
private bool _overlayPlaytime;
private bool _overlayStartingJob;
private float _ghostFadeDistance;
@ -33,9 +35,10 @@ internal sealed class AdminNameOverlay : Overlay
private int _overlayStackMax;
private float _overlayMergeDistance;
//TODO make this adjustable via GUI
//TODO make this adjustable via GUI?
private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
public AdminNameOverlay(
@ -45,20 +48,22 @@ internal sealed class AdminNameOverlay : Overlay
IResourceCache resourceCache,
EntityLookupSystem entityLookup,
IUserInterfaceManager userInterfaceManager,
IConfigurationManager config)
IConfigurationManager config,
SharedRoleSystem roles)
{
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager;
_roles = roles;
ZIndex = 200;
// Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack();
_fontBold = resourceCache.NotoStack(variation: "Bold");
config.OnValueChanged(CCVars.AdminOverlayClassic, (show) => { _overlayClassic = show; }, true);
config.OnValueChanged(CCVars.AdminOverlaySymbols, (show) => { _overlaySymbols = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayAntagFormat, (show) => { _overlayFormat = UpdateOverlayFormat(show); }, true);
config.OnValueChanged(CCVars.AdminOverlaySymbolStyle, (show) => { _overlaySymbolStyle = UpdateOverlaySymbolStyle(show); }, true);
config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayGhostHideDistance, (f) => { _ghostHideDistance = f; }, true);
@ -67,6 +72,22 @@ internal sealed class AdminNameOverlay : Overlay
config.OnValueChanged(CCVars.AdminOverlayMergeDistance, (f) => { _overlayMergeDistance = f; }, true);
}
private AdminOverlayAntagFormat UpdateOverlayFormat(string formatString)
{
if (!Enum.TryParse<AdminOverlayAntagFormat>(formatString, out var format))
format = AdminOverlayAntagFormat.Binary;
return format;
}
private AdminOverlayAntagSymbolStyle UpdateOverlaySymbolStyle(string symbolString)
{
if (!Enum.TryParse<AdminOverlayAntagSymbolStyle>(symbolString, out var symbolStyle))
symbolStyle = AdminOverlayAntagSymbolStyle.Off;
return symbolStyle;
}
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args)
@ -183,34 +204,56 @@ internal sealed class AdminNameOverlay : Overlay
currentOffset += lineoffset;
}
// Classic Antag Label
if (_overlayClassic && playerInfo.Antag)
// Determine antag symbol
string? symbol;
switch (_overlaySymbolStyle)
{
var symbol = _overlaySymbols ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
var label = _overlaySymbols
? Loc.GetString("player-tab-character-name-antag-symbol",
("symbol", symbol),
("name", _antagLabelClassic))
: _antagLabelClassic;
color = Color.OrangeRed;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
case AdminOverlayAntagSymbolStyle.Specific:
symbol = playerInfo.RoleProto.Symbol;
break;
case AdminOverlayAntagSymbolStyle.Basic:
symbol = Loc.GetString("player-tab-antag-prefix");
break;
default:
case AdminOverlayAntagSymbolStyle.Off:
symbol = string.Empty;
break;
}
// Role Type
else if (!_overlayClassic && _filter.Contains(playerInfo.RoleProto))
// Determine antag/role type name
string? text;
switch (_overlayFormat)
{
var symbol = _overlaySymbols && playerInfo.Antag ? playerInfo.RoleProto.Symbol : string.Empty;
var role = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var label = _overlaySymbols
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role))
: role;
color = playerInfo.RoleProto.Color;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
case AdminOverlayAntagFormat.Roletype:
color = playerInfo.RoleProto.Color;
symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
text = _filter.Contains(playerInfo.RoleProto)
? Loc.GetString(playerInfo.RoleProto.Name).ToUpper()
: string.Empty;
break;
case AdminOverlayAntagFormat.Subtype:
color = playerInfo.RoleProto.Color;
symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
text = _filter.Contains(playerInfo.RoleProto)
? _roles.GetRoleSubtypeLabel(playerInfo.RoleProto.Name, playerInfo.Subtype).ToUpper()
: string.Empty;
break;
default:
case AdminOverlayAntagFormat.Binary:
color = Color.OrangeRed;
symbol = playerInfo.Antag ? symbol : string.Empty;
text = playerInfo.Antag ? _antagLabelClassic : string.Empty;
break;
}
// Draw antag label
color.A = alpha;
var label = !string.IsNullOrEmpty(symbol)
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", text))
: text;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
//Save the coordinates and size of the text block, for stack merge check
drawnOverlays.Add((screenCoordinatesCenter, currentOffset));
}

View File

@ -0,0 +1,15 @@
namespace Content.Client.Administration;
public enum AdminOverlayAntagFormat
{
Binary,
Roletype,
Subtype
}
public enum AdminOverlayAntagSymbolStyle
{
Off,
Basic,
Specific
}

View File

@ -1,4 +1,5 @@
using Content.Client.Administration.Managers;
using Content.Shared.Roles;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
@ -15,6 +16,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
private AdminNameOverlay _adminNameOverlay = default!;
@ -30,7 +32,8 @@ namespace Content.Client.Administration.Systems
_resourceCache,
_entityLookup,
_userInterfaceManager,
_configurationManager);
_configurationManager,
_roles);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}
@ -46,7 +49,8 @@ namespace Content.Client.Administration.Systems
public void AdminOverlayOn()
{
if (_overlayManager.HasOverlay<AdminNameOverlay>()) return;
if (_overlayManager.HasOverlay<AdminNameOverlay>())
return;
_overlayManager.AddOverlay(_adminNameOverlay);
OverlayEnabled?.Invoke();
}

View File

@ -32,7 +32,7 @@ namespace Content.Client.Administration.Systems
var verb = new VvVerb()
{
Text = Loc.GetString("view-variables"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
Act = () => _clientConsoleHost.ExecuteCommand($"vv {GetNetEntity(args.Target)}"),
ClientExclusive = true // opening VV window is client-side. Don't ask server to run this verb.
};

View File

@ -7,6 +7,8 @@ namespace Content.Client.Administration.Systems;
public sealed class KillSignSystem : EntitySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
SubscribeLocalEvent<KillSignComponent, ComponentStartup>(KillSignAdded);
@ -18,10 +20,10 @@ public sealed class KillSignSystem : EntitySystem
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
if (!sprite.LayerMapTryGet(KillSignKey.Key, out var layer))
if (!_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var layer, false))
return;
sprite.RemoveLayer(layer);
_sprite.RemoveLayer((uid, sprite), layer);
}
private void KillSignAdded(EntityUid uid, KillSignComponent component, ComponentStartup args)
@ -29,15 +31,15 @@ public sealed class KillSignSystem : EntitySystem
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
if (sprite.LayerMapTryGet(KillSignKey.Key, out var _))
if (_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var _, false))
return;
var adj = sprite.Bounds.Height / 2 + ((1.0f/32) * 6.0f);
var adj = _sprite.GetLocalBounds((uid, sprite)).Height / 2 + ((1.0f / 32) * 6.0f);
var layer = sprite.AddLayer(new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "sign"));
sprite.LayerMapSet(KillSignKey.Key, layer);
var layer = _sprite.AddLayer((uid, sprite), new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "sign"));
_sprite.LayerMapSet((uid, sprite), KillSignKey.Key, layer);
sprite.LayerSetOffset(layer, new Vector2(0.0f, adj));
_sprite.LayerSetOffset((uid, sprite), layer, new Vector2(0.0f, adj));
sprite.LayerSetShader(layer, "unshaded");
}

View File

@ -1,59 +0,0 @@
using System.Threading;
using Content.Client.Stylesheets;
using Robust.Client.UserInterface.Controls;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Administration.UI;
public static class AdminUIHelpers
{
private static void ResetButton(Button button, ConfirmationData data)
{
data.Cancellation.Cancel();
button.ModulateSelfOverride = null;
button.Text = data.OriginalText;
}
public static bool RemoveConfirm(Button button, Dictionary<Button, ConfirmationData> confirmations)
{
if (confirmations.Remove(button, out var data))
{
ResetButton(button, data);
return true;
}
return false;
}
public static void RemoveAllConfirms(Dictionary<Button, ConfirmationData> confirmations)
{
foreach (var (button, confirmation) in confirmations)
{
ResetButton(button, confirmation);
}
confirmations.Clear();
}
public static bool TryConfirm(Button button, Dictionary<Button, ConfirmationData> confirmations)
{
if (RemoveConfirm(button, confirmations))
return true;
var data = new ConfirmationData(new CancellationTokenSource(), button.Text);
confirmations[button] = data;
Timer.Spawn(TimeSpan.FromSeconds(5), () =>
{
confirmations.Remove(button);
button.ModulateSelfOverride = null;
button.Text = data.OriginalText;
}, data.Cancellation.Token);
button.ModulateSelfOverride = StyleNano.ButtonColorCautionDefault;
button.Text = Loc.GetString("admin-player-actions-confirm");
return false;
}
}
public readonly record struct ConfirmationData(CancellationTokenSource Cancellation, string? OriginalText);

View File

@ -1,6 +1,7 @@
<Control
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<PanelContainer StyleClasses="BackgroundDark">
<SplitContainer Orientation="Vertical" ResizeMode="NotResizable">
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
@ -18,9 +19,9 @@
<Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" StyleClasses="OpenBoth" />
<controls:ConfirmButton Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" StyleClasses="OpenBoth" />
<controls:ConfirmButton Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
</BoxContainer>
</SplitContainer>

View File

@ -29,7 +29,6 @@ namespace Content.Client.Administration.UI.Bwoink
public AdminAHelpUIHandler AHelpHelper = default!;
private PlayerInfo? _currentPlayer;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public BwoinkControl()
{
@ -178,11 +177,6 @@ namespace Content.Client.Administration.UI.Bwoink
Kick.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(Kick, _confirmations))
{
return;
}
// TODO: Reason field
if (_currentPlayer is not null)
_console.ExecuteCommand($"kick \"{_currentPlayer.Username}\"");
@ -196,11 +190,6 @@ namespace Content.Client.Administration.UI.Bwoink
Respawn.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(Respawn, _confirmations))
{
return;
}
if (_currentPlayer is not null)
_console.ExecuteCommand($"respawn \"{_currentPlayer.Username}\"");
};

View File

@ -1,5 +1,6 @@
<Popup xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BorderThickness="2" BorderColor="#18181B" BackgroundColor="#25252a"/>
@ -19,7 +20,7 @@
<BoxContainer Orientation="Horizontal">
<Button Name="EditButton" Text="{Loc admin-notes-edit}"/>
<Control HorizontalExpand="True"/>
<Button Name="DeleteButton" Text="{Loc admin-notes-delete}" HorizontalAlignment="Right"/>
<controls:ConfirmButton Name="DeleteButton" Text="{Loc admin-notes-delete}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" HorizontalAlignment="Right"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>

View File

@ -19,12 +19,13 @@
<Label Name="SharedConnections"/>
<BoxContainer Align="Center">
<GridContainer Rows="5">
<GridContainer Rows="6">
<Button Name="NotesButton" Text="{Loc player-panel-show-notes}" SetWidth="136" Disabled="True"/>
<Button Name="AhelpButton" Text="{Loc player-panel-help}" Disabled="True"/>
<Button Name="FreezeButton" Text = "{Loc player-panel-freeze}" Disabled="True"/>
<controls:ConfirmButton Name="KickButton" Text="{Loc player-panel-kick}" Disabled="True"/>
<controls:ConfirmButton Name="DeleteButton" Text="{Loc player-panel-delete}" Disabled="True"/>
<Button Name="FollowButton" Text="{Loc player-panel-follow}"/>
<Button Name="ShowBansButton" Text="{Loc player-panel-show-bans}" SetWidth="136" Disabled="True"/>
<Button Name="LogsButton" Text="{Loc player-panel-logs}" Disabled="True"/>
<Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/>

View File

@ -2,7 +2,6 @@ using Content.Client.Administration.Managers;
using Content.Client.UserInterface.Controls;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Network;
using Robust.Shared.Utility;
@ -21,6 +20,7 @@ public sealed partial class PlayerPanel : FancyWindow
public event Action<string?>? OnKick;
public event Action<NetUserId?>? OnOpenBanPanel;
public event Action<NetUserId?, bool>? OnWhitelistToggle;
public event Action? OnFollow;
public event Action? OnFreezeAndMuteToggle;
public event Action? OnFreeze;
public event Action? OnLogs;
@ -37,7 +37,7 @@ public sealed partial class PlayerPanel : FancyWindow
RobustXamlLoader.Load(this);
_adminManager = adminManager;
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? "");
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? "");
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
@ -48,6 +48,7 @@ public sealed partial class PlayerPanel : FancyWindow
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
SetWhitelisted(!_isWhitelisted);
};
FollowButton.OnPressed += _ => OnFollow?.Invoke();
FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
LogsButton.OnPressed += _ => OnLogs?.Invoke();

View File

@ -39,6 +39,7 @@ public sealed class PlayerPanelEui : BaseEui
PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
PlayerPanel.OnOpenJobWhitelists += id => _console.ExecuteCommand($"jobwhitelists \"{id}\""); // DeltaV
PlayerPanel.OnFollow += () => SendMessage(new PlayerPanelFollowMessage());
PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());
}

View File

@ -19,10 +19,10 @@ namespace Content.Client.Administration.UI.SpawnExplosion;
public sealed partial class SpawnExplosionWindow : DefaultWindow
{
[Dependency] private readonly IClientConsoleHost _conHost = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _transform = default!;
private readonly SpawnExplosionEui _eui;
@ -38,6 +38,7 @@ public sealed partial class SpawnExplosionWindow : DefaultWindow
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_mapSystem = _entMan.System<SharedMapSystem>();
_transform = _entMan.System<TransformSystem>();
_eui = eui;
@ -87,7 +88,7 @@ public sealed partial class SpawnExplosionWindow : DefaultWindow
{
_mapData.Clear();
MapOptions.Clear();
foreach (var map in _mapManager.GetAllMapIds())
foreach (var map in _mapSystem.GetAllMapIds())
{
_mapData.Add(map);
MapOptions.AddItem(map.ToString());

View File

@ -17,6 +17,7 @@
<cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
<cc:CommandButton Command="adminlogs" Text="{Loc admin-player-actions-window-admin-logs}"/>
<cc:CommandButton Command="faxui" Text="{Loc admin-player-actions-window-admin-fax}"/>
<cc:CommandButton Command="achatwindow" Text="{Loc admin-player-actions-window-admin-chat}"/>
<!-- CD: records purge button -->
<cc:UICommandButton Command="purgecharacterrecords" Text="{Loc admin-player-actions-window-cd-record-purge}" WindowType="{x:Type cdAdmin:ModifyCharacterRecords}"/>
</GridContainer>

View File

@ -1,6 +1,7 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc admin-player-actions-window-title}" MinSize="425 272">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
@ -10,9 +11,9 @@
</BoxContainer>
<cc:PlayerListControl Name="PlayerList" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal">
<Button Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" Disabled="True"/>
<controls:ConfirmButton Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
<Button Name="SubmitAHelpButton" Text="{Loc admin-player-actions-ahelp}" Disabled="True"/>
<Button Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" Disabled="True"/>
<controls:ConfirmButton Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@ -14,7 +14,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
public sealed partial class PlayerActionsWindow : DefaultWindow
{
private PlayerInfo? _selectedPlayer;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public PlayerActionsWindow()
{
@ -28,9 +27,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
private void OnListOnOnSelectionChanged(PlayerInfo? obj)
{
if (_selectedPlayer != obj)
AdminUIHelpers.RemoveAllConfirms(_confirmations);
_selectedPlayer = obj;
var disableButtons = _selectedPlayer == null;
SubmitKickButton.Disabled = disableButtons;
@ -43,9 +39,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
if (_selectedPlayer == null)
return;
if (!AdminUIHelpers.TryConfirm(SubmitKickButton, _confirmations))
return;
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
$"kick \"{_selectedPlayer.Username}\" \"{CommandParsing.Escape(ReasonLine.Text)}\"");
}
@ -64,9 +57,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
if (_selectedPlayer == null)
return;
if (!AdminUIHelpers.TryConfirm(SubmitRespawnButton, _confirmations))
return;
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
$"respawn \"{_selectedPlayer.Username}\"");
}

View File

@ -14,6 +14,9 @@ namespace Content.Client.Administration.UI.Tabs.AdminbusTab
[UsedImplicitly]
public sealed partial class LoadBlueprintsWindow : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public LoadBlueprintsWindow()
{
RobustXamlLoader.Load(this);
@ -21,9 +24,9 @@ namespace Content.Client.Administration.UI.Tabs.AdminbusTab
protected override void EnteredTree()
{
var mapManager = IoCManager.Resolve<IMapManager>();
var mapSystem = _entityManager.System<SharedMapSystem>();
foreach (var mapId in mapManager.GetAllMapIds())
foreach (var mapId in mapSystem.GetAllMapIds())
{
MapOptions.AddItem(mapId.ToString(), (int) mapId);
}
@ -39,21 +42,19 @@ namespace Content.Client.Administration.UI.Tabs.AdminbusTab
private void Reset()
{
var entManager = IoCManager.Resolve<IEntityManager>();
var xformSystem = entManager.System<SharedTransformSystem>();
var playerManager = IoCManager.Resolve<IPlayerManager>();
var player = playerManager.LocalEntity;
var xformSystem = _entityManager.System<SharedTransformSystem>();
var player = _playerManager.LocalEntity;
var currentMap = MapId.Nullspace;
var position = Vector2.Zero;
var rotation = Angle.Zero;
if (entManager.TryGetComponent<TransformComponent>(player, out var xform))
if (_entityManager.TryGetComponent<TransformComponent>(player, out var xform))
{
currentMap = xform.MapID;
position = xformSystem.GetWorldPosition(xform);
if (entManager.TryGetComponent<TransformComponent>(xform.GridUid, out var gridXform))
if (_entityManager.TryGetComponent<TransformComponent>(xform.GridUid, out var gridXform))
{
rotation = xformSystem.GetWorldRotation(gridXform);
}

View File

@ -13,7 +13,6 @@ public sealed partial class ObjectsTabEntry : PanelContainer
public Action<NetEntity>? OnTeleport;
public Action<NetEntity>? OnDelete;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public ObjectsTabEntry(IClientAdminManager manager, string name, NetEntity nent, StyleBox styleBox)
{
@ -28,13 +27,6 @@ public sealed partial class ObjectsTabEntry : PanelContainer
DeleteButton.Disabled = !manager.CanCommand("delete");
TeleportButton.OnPressed += _ => OnTeleport?.Invoke(nent);
DeleteButton.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(DeleteButton, _confirmations))
{
return;
}
OnDelete?.Invoke(nent);
};
DeleteButton.OnPressed += _ => OnDelete?.Invoke(nent);
}
}

View File

@ -32,6 +32,10 @@ public sealed partial class PlayerTab : Control
private bool _ascending = true;
private bool _showDisconnected;
private AdminPlayerTabColorOption _playerTabColorSetting;
private AdminPlayerTabRoleTypeOption _playerTabRoleSetting;
private AdminPlayerTabSymbolOption _playerTabSymbolSetting;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public PlayerTab()
@ -44,9 +48,10 @@ public sealed partial class PlayerTab : Control
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
_config.OnValueChanged(CCVars.AdminPlayerlistSeparateSymbols, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerlistHighlightedCharacterColor, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerlistRoleTypeColor, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerTabRoleSetting, RoleSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabColorSetting, ColorSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabSymbolSetting, SymbolSettingChanged, true);
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
@ -113,8 +118,27 @@ public sealed partial class PlayerTab : Control
#region ListContainer
private void PlayerListSettingsChanged(bool _)
private void RoleSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabRoleTypeOption>(s, out var format))
format = AdminPlayerTabRoleTypeOption.Subtype;
_playerTabRoleSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
private void ColorSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabColorOption>(s, out var format))
format = AdminPlayerTabColorOption.Both;
_playerTabColorSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
private void SymbolSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabSymbolOption>(s, out var format))
format = AdminPlayerTabSymbolOption.Specific;
_playerTabSymbolSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
@ -140,7 +164,12 @@ public sealed partial class PlayerTab : Control
if (data is not PlayerListData { Info: var player})
return;
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
var entry = new PlayerTabEntry(
player,
new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor),
_playerTabColorSetting,
_playerTabRoleSetting,
_playerTabSymbolSetting);
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
}

View File

@ -1,42 +1,99 @@
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
[GenerateTypedNameReferences]
public sealed partial class PlayerTabEntry : PanelContainer
{
public NetEntity? PlayerEntity;
[Dependency] private readonly IEntityManager _entMan = default!;
public PlayerTabEntry(PlayerInfo player, StyleBoxFlat styleBoxFlat)
public PlayerTabEntry(
PlayerInfo player,
StyleBoxFlat styleBoxFlat,
AdminPlayerTabColorOption colorOption,
AdminPlayerTabRoleTypeOption roleSetting,
AdminPlayerTabSymbolOption symbolSetting)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
var config = IoCManager.Resolve<IConfigurationManager>();
var roles = _entMan.System<SharedRoleSystem>();
UsernameLabel.Text = player.Username;
if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled");
JobLabel.Text = player.StartingJob;
var separateAntagSymbols = config.GetCVar(CCVars.AdminPlayerlistSeparateSymbols);
var genericAntagSymbol = player.Antag ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
var roleSymbol = player.Antag ? player.RoleProto.Symbol : string.Empty;
var symbol = separateAntagSymbols ? roleSymbol : genericAntagSymbol;
var colorAntags = false;
var colorRoles = false;
switch (colorOption)
{
case AdminPlayerTabColorOption.Off:
break;
case AdminPlayerTabColorOption.Character:
colorAntags = true;
break;
case AdminPlayerTabColorOption.Roletype:
colorRoles = true;
break;
default:
case AdminPlayerTabColorOption.Both:
colorAntags = true;
colorRoles = true;
break;
}
var symbol = string.Empty;
switch (symbolSetting)
{
case AdminPlayerTabSymbolOption.Off:
break;
case AdminPlayerTabSymbolOption.Basic:
symbol = player.Antag ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
break;
default:
case AdminPlayerTabSymbolOption.Specific:
symbol = player.Antag ? player.RoleProto.Symbol : string.Empty;
break;
}
CharacterLabel.Text = Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", player.CharacterName));
if (player.Antag && config.GetCVar(CCVars.AdminPlayerlistHighlightedCharacterColor))
if (player.Antag && colorAntags)
CharacterLabel.FontColorOverride = player.RoleProto.Color;
if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]";
RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
if (config.GetCVar(CCVars.AdminPlayerlistRoleTypeColor))
var roletype = RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
var subtype = roles.GetRoleSubtypeLabel(player.RoleProto.Name, player.Subtype);
switch (roleSetting)
{
case AdminPlayerTabRoleTypeOption.RoleTypeSubtype:
RoleTypeLabel.Text = roletype != subtype
? roletype + " - " +subtype
: roletype;
break;
case AdminPlayerTabRoleTypeOption.SubtypeRoleType:
RoleTypeLabel.Text = roletype != subtype
? subtype + " - " + roletype
: roletype;
break;
case AdminPlayerTabRoleTypeOption.RoleType:
RoleTypeLabel.Text = roletype;
break;
default:
case AdminPlayerTabRoleTypeOption.Subtype:
RoleTypeLabel.Text = subtype;
break;
}
if (colorRoles)
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;
}
}

View File

@ -0,0 +1,24 @@
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
public enum AdminPlayerTabColorOption
{
Off,
Character,
Roletype,
Both
}
public enum AdminPlayerTabRoleTypeOption
{
RoleType,
Subtype,
RoleTypeSubtype,
SubtypeRoleType
}
public enum AdminPlayerTabSymbolOption
{
Off,
Basic,
Specific
}

View File

@ -1,13 +1,13 @@
<Control
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Margin="4"
MinSize="50 50">
<GridContainer
Columns="3">
<cc:CommandButton Command="startround" Text="{Loc administration-ui-round-tab-start-round}" />
<cc:CommandButton Command="endround" Text="{Loc administration-ui-round-tab-end-round}" />
<cc:CommandButton Command="restartround" Text="{Loc administration-ui-round-tab-restart-round}" />
<cc:CommandButton Command="restartroundnow" Text="{Loc administration-ui-round-tab-restart-round-now}" />
<controls:ConfirmButton Name="StartRound" Text="{Loc administration-ui-round-tab-start-round}" />
<controls:ConfirmButton Name="EndRound" Text="{Loc administration-ui-round-tab-end-round}" />
<controls:ConfirmButton Name="RestartRound" Text="{Loc administration-ui-round-tab-restart-round}" />
<controls:ConfirmButton Name="RestartRoundNow" Text="{Loc administration-ui-round-tab-restart-round-now}" />
</GridContainer>
</Control>

View File

@ -1,10 +1,24 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs
{
[GenerateTypedNameReferences]
public sealed partial class RoundTab : Control
{
[Dependency] private readonly IClientConsoleHost _console = default!;
public RoundTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
StartRound.OnPressed += _ => _console.ExecuteCommand("startround");
EndRound.OnPressed += _ => _console.ExecuteCommand("endround");
RestartRound.OnPressed += _ => _console.ExecuteCommand("restartround");
RestartRoundNow.OnPressed += _ => _console.ExecuteCommand("restartroundnow");
}
}
}

View File

@ -1,11 +1,12 @@
<Control
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Margin="4"
MinSize="50 50">
<GridContainer
Columns="4" >
<cc:CommandButton Command="shutdown" Text="{Loc server-shutdown}" />
<controls:ConfirmButton Name="ServerShutdownButton" Text="{Loc server-shutdown}" />
<cc:CommandButton Name="SetOocButton" Command="setooc" Text="{Loc server-ooc-toggle}" ToggleMode="True" />
<cc:CommandButton Name="SetLoocButton" Command="setlooc" Text="{Loc server-looc-toggle}" ToggleMode="True" />
</GridContainer>

View File

@ -1,5 +1,6 @@
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
@ -10,6 +11,7 @@ namespace Content.Client.Administration.UI.Tabs
public sealed partial class ServerTab : Control
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IClientConsoleHost _console = default!;
public ServerTab()
{
@ -18,6 +20,8 @@ namespace Content.Client.Administration.UI.Tabs
_config.OnValueChanged(CCVars.OocEnabled, OocEnabledChanged, true);
_config.OnValueChanged(CCVars.LoocEnabled, LoocEnabledChanged, true);
ServerShutdownButton.OnPressed += _ => _console.ExecuteCommand("shutdown");
}
private void OocEnabledChanged(bool value)

View File

@ -8,6 +8,8 @@ namespace Content.Client.AlertLevel;
public sealed class AlertLevelDisplaySystem : EntitySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
@ -21,26 +23,26 @@ public sealed class AlertLevelDisplaySystem : EntitySystem
{
return;
}
var layer = args.Sprite.LayerMapReserveBlank(AlertLevelDisplay.Layer);
var layer = _sprite.LayerMapReserve((uid, args.Sprite), AlertLevelDisplay.Layer);
if (args.AppearanceData.TryGetValue(AlertLevelDisplay.Powered, out var poweredObject))
{
args.Sprite.LayerSetVisible(layer, poweredObject is true);
_sprite.LayerSetVisible((uid, args.Sprite), layer, poweredObject is true);
}
if (!args.AppearanceData.TryGetValue(AlertLevelDisplay.CurrentLevel, out var level))
{
args.Sprite.LayerSetState(layer, alertLevelDisplay.AlertVisuals.Values.First());
_sprite.LayerSetRsiState((uid, args.Sprite), layer, alertLevelDisplay.AlertVisuals.Values.First());
return;
}
if (alertLevelDisplay.AlertVisuals.TryGetValue((string) level, out var visual))
if (alertLevelDisplay.AlertVisuals.TryGetValue((string)level, out var visual))
{
args.Sprite.LayerSetState(layer, visual);
_sprite.LayerSetRsiState((uid, args.Sprite), layer, visual);
}
else
{
args.Sprite.LayerSetState(layer, alertLevelDisplay.AlertVisuals.Values.First());
_sprite.LayerSetRsiState((uid, args.Sprite), layer, alertLevelDisplay.AlertVisuals.Values.First());
}
}
}

View File

@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Alert;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@ -15,6 +16,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
public event EventHandler? ClearAlerts;
public event EventHandler<IReadOnlyDictionary<AlertKey, AlertState>>? SyncAlerts;
@ -27,6 +29,12 @@ public sealed class ClientAlertsSystem : AlertsSystem
SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
}
protected override void HandledAlert()
{
_ui.ClickSound();
}
protected override void LoadPrototypes()
{
base.LoadPrototypes();

View File

@ -15,6 +15,7 @@ public sealed class EntityPickupAnimationSystem : EntitySystem
{
[Dependency] private readonly AnimationPlayerSystem _animations = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly TransformSystem _transform = default!;
public override void Initialize()
@ -56,8 +57,8 @@ public sealed class EntityPickupAnimationSystem : EntitySystem
}
var sprite = Comp<SpriteComponent>(animatableClone);
sprite.CopyFrom(sprite0);
sprite.Visible = true;
_sprite.CopySprite((uid, sprite0), (animatableClone, sprite));
_sprite.SetVisible((animatableClone, sprite), true);
var animations = Comp<AnimationPlayerComponent>(animatableClone);

View File

@ -11,6 +11,7 @@ public sealed class AnomalySystem : SharedAnomalySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly FloatingVisualizerSystem _floating = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
/// <inheritdoc/>
public override void Initialize()
@ -49,12 +50,12 @@ public sealed class AnomalySystem : SharedAnomalySystem
if (HasComp<AnomalySupercriticalComponent>(uid))
pulsing = true;
if (!sprite.LayerMapTryGet(AnomalyVisualLayers.Base, out var layer) ||
!sprite.LayerMapTryGet(AnomalyVisualLayers.Animated, out var animatedLayer))
if (!_sprite.LayerMapTryGet((uid, sprite), AnomalyVisualLayers.Base, out var layer, false) ||
!_sprite.LayerMapTryGet((uid, sprite), AnomalyVisualLayers.Animated, out var animatedLayer, false))
return;
sprite.LayerSetVisible(layer, !pulsing);
sprite.LayerSetVisible(animatedLayer, pulsing);
_sprite.LayerSetVisible((uid, sprite), layer, !pulsing);
_sprite.LayerSetVisible((uid, sprite), animatedLayer, pulsing);
}
public override void Update(float frameTime)
@ -63,16 +64,16 @@ public sealed class AnomalySystem : SharedAnomalySystem
var query = EntityQueryEnumerator<AnomalySupercriticalComponent, SpriteComponent>();
while (query.MoveNext(out var super, out var sprite))
while (query.MoveNext(out var uid, out var super, out var sprite))
{
var completion = 1f - (float) ((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
var completion = 1f - (float)((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
var scale = completion * (super.MaxScaleAmount - 1f) + 1f;
sprite.Scale = new Vector2(scale, scale);
_sprite.SetScale((uid, sprite), new Vector2(scale, scale));
var transparency = (byte) (65 * (1f - completion) + 190);
var transparency = (byte)(65 * (1f - completion) + 190);
if (transparency < sprite.Color.AByte)
{
sprite.Color = sprite.Color.WithAlpha(transparency);
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(transparency));
}
}
}
@ -82,7 +83,7 @@ public sealed class AnomalySystem : SharedAnomalySystem
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
sprite.Scale = Vector2.One;
sprite.Color = sprite.Color.WithAlpha(1f);
_sprite.SetScale((ent.Owner, sprite), Vector2.One);
_sprite.SetColor((ent.Owner, sprite), sprite.Color.WithAlpha(1f));
}
}

View File

@ -7,6 +7,8 @@ namespace Content.Client.Anomaly.Effects;
public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
SubscribeLocalEvent<InnerBodyAnomalyComponent, AfterAutoHandleStateEvent>(OnAfterHandleState);
@ -21,21 +23,20 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
if (ent.Comp.FallbackSprite is null)
return;
if (!sprite.LayerMapTryGet(ent.Comp.LayerMap, out var index))
index = sprite.LayerMapReserveBlank(ent.Comp.LayerMap);
var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap);
if (TryComp<BodyComponent>(ent, out var body) &&
body.Prototype is not null &&
ent.Comp.SpeciesSprites.TryGetValue(body.Prototype.Value, out var speciesSprite))
{
sprite.LayerSetSprite(index, speciesSprite);
_sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite);
}
else
{
sprite.LayerSetSprite(index, ent.Comp.FallbackSprite);
_sprite.LayerSetSprite((ent.Owner, sprite), index, ent.Comp.FallbackSprite);
}
sprite.LayerSetVisible(index, true);
_sprite.LayerSetVisible((ent.Owner, sprite), index, true);
sprite.LayerSetShader(index, "unshaded");
}
@ -44,7 +45,7 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
var index = sprite.LayerMapGet(ent.Comp.LayerMap);
sprite.LayerSetVisible(index, false);
var index = _sprite.LayerMapGet((ent.Owner, sprite), ent.Comp.LayerMap);
_sprite.LayerSetVisible((ent.Owner, sprite), index, false);
}
}

View File

@ -20,6 +20,7 @@ public sealed class AnomalyScannerBoundUserInterface : BoundUserInterface
_menu = new AnomalyScannerMenu();
_menu.OpenCentered();
_menu.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@ -0,0 +1,203 @@
using Content.Client.Construction;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Construction.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Placement;
using Robust.Client.Placement.Modes;
using Robust.Client.Utility;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Numerics;
using static Robust.Client.Placement.PlacementManager;
namespace Content.Client.Atmos;
/// <summary>
/// Allows users to place atmos pipes on different layers depending on how the mouse cursor is positioned within a grid tile.
/// </summary>
/// <remarks>
/// This placement mode is not on the engine because it is content specific.
/// </remarks>
public sealed class AlignAtmosPipeLayers : SnapgridCenter
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _transformSystem;
private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem;
private readonly SpriteSystem _spriteSystem;
private const float SearchBoxSize = 2f;
private EntityCoordinates _unalignedMouseCoords = default;
private const float MouseDeadzoneRadius = 0.25f;
private Color _guideColor = new Color(0, 0, 0.5785f);
private const float GuideRadius = 0.1f;
private const float GuideOffset = 0.21875f;
public AlignAtmosPipeLayers(PlacementManager pMan) : base(pMan)
{
IoCManager.InjectDependencies(this);
_mapSystem = _entityManager.System<SharedMapSystem>();
_transformSystem = _entityManager.System<SharedTransformSystem>();
_pipeLayersSystem = _entityManager.System<SharedAtmosPipeLayersSystem>();
_spriteSystem = _entityManager.System<SpriteSystem>();
}
/// <inheritdoc/>
public override void Render(in OverlayDrawArgs args)
{
var gridUid = _entityManager.System<SharedTransformSystem>().GetGrid(MouseCoords);
if (gridUid == null || Grid == null)
return;
// Draw guide circles for each pipe layer if we are not in line/grid placing mode
if (pManager.PlacementType == PlacementTypes.None)
{
var gridRotation = _transformSystem.GetWorldRotation(gridUid.Value);
var worldPosition = _mapSystem.LocalToWorld(gridUid.Value, Grid, MouseCoords.Position);
var direction = (_eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
var multi = (direction == Direction.North || direction == Direction.South) ? -1f : 1f;
args.WorldHandle.DrawCircle(worldPosition, GuideRadius, _guideColor);
args.WorldHandle.DrawCircle(worldPosition + gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
args.WorldHandle.DrawCircle(worldPosition - gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
}
base.Render(args);
}
/// <inheritdoc/>
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
{
_unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
base.AlignPlacementMode(mouseScreen);
// Exit early if we are in line/grid placing mode
if (pManager.PlacementType != PlacementTypes.None)
return;
MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
var gridId = _transformSystem.GetGrid(MouseCoords);
if (!_entityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
return;
var gridRotation = _transformSystem.GetWorldRotation(gridId.Value);
CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
float tileSize = mapGrid.TileSize;
GridDistancing = tileSize;
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
// Calculate the position of the mouse cursor with respect to the center of the tile to determine which layer to use
var mouseCoordsDiff = _unalignedMouseCoords.Position - MouseCoords.Position;
var layer = AtmosPipeLayer.Primary;
if (mouseCoordsDiff.Length() > MouseDeadzoneRadius)
{
// Determine the direction of the mouse is relative to the center of the tile, adjusting for the player eye and grid rotation
var direction = (new Angle(mouseCoordsDiff) + _eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
layer = (direction == Direction.North || direction == Direction.East) ? AtmosPipeLayer.Secondary : AtmosPipeLayer.Tertiary;
}
// Update the construction menu placer
if (pManager.Hijack != null)
UpdateHijackedPlacer(layer, mouseScreen);
// Otherwise update the debug placer
else
UpdatePlacer(layer);
}
private void UpdateHijackedPlacer(AtmosPipeLayer layer, ScreenCoordinates mouseScreen)
{
// Try to get alternative prototypes from the construction prototype
var constructionSystem = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentConstructionSystem;
var altPrototypes = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.AlternativePrototypes;
if (constructionSystem == null || altPrototypes == null || (int)layer >= altPrototypes.Length)
return;
var newProtoId = altPrototypes[(int)layer];
if (!_protoManager.TryIndex(newProtoId, out var newProto))
return;
if (newProto.Type != ConstructionType.Structure)
{
pManager.Clear();
return;
}
if (newProto.ID == (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.ID)
return;
// Start placing
pManager.BeginPlacing(new PlacementInformation()
{
IsTile = false,
PlacementOption = newProto.PlacementMode,
}, new ConstructionPlacementHijack(constructionSystem, newProto));
if (pManager.CurrentMode is AlignAtmosPipeLayers { } newMode)
newMode.RefreshGrid(mouseScreen);
// Update construction guide
constructionSystem.GetGuide(newProto);
}
private void UpdatePlacer(AtmosPipeLayer layer)
{
// Try to get alternative prototypes from the entity atmos pipe layer component
if (pManager.CurrentPermission?.EntityType == null)
return;
if (!_protoManager.TryIndex<EntityPrototype>(pManager.CurrentPermission.EntityType, out var currentProto))
return;
if (!currentProto.TryGetComponent<AtmosPipeLayersComponent>(out var atmosPipeLayers, _entityManager.ComponentFactory))
return;
if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
return;
if (_protoManager.TryIndex<EntityPrototype>(newProtoId, out var newProto))
{
// Update the placed prototype
pManager.CurrentPermission.EntityType = newProtoId;
// Update the appearance of the ghost sprite
if (newProto.TryGetComponent<SpriteComponent>(out var sprite, _entityManager.ComponentFactory))
{
var textures = new List<IDirectionalTextureProvider>();
foreach (var spriteLayer in sprite.AllLayers)
{
if (spriteLayer.ActualRsi?.Path != null && spriteLayer.RsiState.Name != null)
textures.Add(_spriteSystem.RsiStateLike(new SpriteSpecifier.Rsi(spriteLayer.ActualRsi.Path, spriteLayer.RsiState.Name)));
}
pManager.CurrentTextures = textures;
}
}
}
private void RefreshGrid(ScreenCoordinates mouseScreen)
{
base.AlignPlacementMode(mouseScreen);
}
}

View File

@ -17,6 +17,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
public int? FocusNetId = null;
private const int ChunkSize = 4;
private const float ScaleModifier = 4f;
private readonly float[] _layerFraction = { 0.5f, 0.75f, 0.25f };
private const float LineThickness = 0.05f;
private readonly Color _basePipeNetColor = Color.LightGray;
private readonly Color _unfocusedPipeNetColor = Color.DimGray;
@ -95,23 +99,23 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
foreach (var chunkedLine in atmosPipeNetwork)
{
var leftTop = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
- offset);
var rightTop = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
- offset);
var leftBottom = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
- offset);
var rightBottom = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
- offset);
if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV))
@ -142,7 +146,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
if (chunks == null || grid == null)
return decodedOutput;
// Clear stale look up table values
// Clear stale look up table values
_horizLines.Clear();
_horizLinesReversed.Clear();
_vertLines.Clear();
@ -158,7 +162,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
{
var list = new List<AtmosMonitoringConsoleLine>();
foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData)
foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData)
{
// Determine the correct coloration for the pipe
var color = Color.FromHex(hexColor) * _basePipeNetColor;
@ -191,6 +195,9 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
_vertLinesReversed[color] = vertLinesReversed;
}
var layerFraction = _layerFraction[(int)layer];
var origin = new Vector2(grid.TileSize * layerFraction, -grid.TileSize * layerFraction);
// Loop over the chunk
for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++)
{
@ -208,21 +215,22 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
// Calculate the draw point offsets
var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 1f) : origin;
var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 0f) : origin;
var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
new Vector2(grid.TileSize * 1f, -grid.TileSize * layerFraction) : origin;
var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
new Vector2(grid.TileSize * 0f, -grid.TileSize * layerFraction) : origin;
// Since we can have pipe lines that have a length of a half tile,
// double the vectors and convert to vector2i so we can merge them
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed);
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed);
// Scale up the vectors and convert to vector2i so we can merge them
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, ScaleModifier),
ConvertVector2ToVector2i(tile + horizLineTerminus, ScaleModifier), horizLines, horizLinesReversed);
AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, ScaleModifier),
ConvertVector2ToVector2i(tile + vertLineTerminus, ScaleModifier), vertLines, vertLinesReversed);
}
}
}
@ -235,7 +243,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
foreach (var (origin, terminal) in horizLines)
decodedOutput.Add(new AtmosMonitoringConsoleLine
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
(ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
}
foreach (var (color, vertLines) in _vertLines)
@ -245,7 +253,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
foreach (var (origin, terminal) in vertLines)
decodedOutput.Add(new AtmosMonitoringConsoleLine
(ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
(ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
}
return decodedOutput;

View File

@ -15,7 +15,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
{
Dictionary<Vector2i, Dictionary<(int, string), ulong>> modifiedChunks;
Dictionary<Vector2i, Dictionary<AtmosMonitoringConsoleSubnet, ulong>> modifiedChunks;
Dictionary<NetEntity, AtmosDeviceNavMapData> atmosDevices;
switch (args.Current)
@ -54,7 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
foreach (var (origin, chunk) in modifiedChunks)
{
var newChunk = new AtmosPipeChunk(origin);
newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk);
newChunk.AtmosPipeData = new Dictionary<AtmosMonitoringConsoleSubnet, ulong>(chunk);
component.AtmosPipeChunks[origin] = newChunk;
}

View File

@ -13,6 +13,7 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
namespace Content.Client.Atmos.Consoles;
@ -33,6 +34,8 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
private ProtoId<NavMapBlipPrototype> _navMapConsoleProtoId = "NavMapConsole";
private ProtoId<NavMapBlipPrototype> _gasPipeSensorProtoId = "GasPipeSensor";
private readonly Vector2[] _pipeLayerOffsets = { new Vector2(0f, 0f), new Vector2(0.25f, 0.25f), new Vector2(-0.25f, -0.25f) };
public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
{
RobustXamlLoader.Load(this);
@ -53,7 +56,7 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
consoleCoords = xform.Coordinates;
NavMap.MapUid = xform.GridUid;
// Assign station name
// Assign station name
if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
stationName = stationMetaData.EntityName;
@ -238,6 +241,10 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
var blinks = proto.Blinks || _focusEntity == metaData.NetEntity;
var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
if (proto.Placement == NavMapBlipPlacement.Offset && metaData.PipeLayer > 0)
coords = coords.Offset(_pipeLayerOffsets[(int)metaData.PipeLayer]);
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale);
NavMap.TrackedEntities[metaData.NetEntity] = blip;
}

View File

@ -1,6 +1,7 @@
using Content.Client.SubFloor;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@ -8,9 +9,10 @@ using Robust.Client.GameObjects;
namespace Content.Client.Atmos.EntitySystems;
[UsedImplicitly]
public sealed class AtmosPipeAppearanceSystem : EntitySystem
public sealed partial class AtmosPipeAppearanceSystem : SharedAtmosPipeAppearanceSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@ -25,25 +27,37 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
if (!TryComp(uid, out SpriteComponent? sprite))
return;
foreach (PipeConnectionLayer layerKey in Enum.GetValues(typeof(PipeConnectionLayer)))
var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out _);
foreach (var layerKey in Enum.GetValues<PipeConnectionLayer>())
{
sprite.LayerMapReserveBlank(layerKey);
var layer = sprite.LayerMapGet(layerKey);
sprite.LayerSetRSI(layer, component.Sprite.RsiPath);
sprite.LayerSetState(layer, component.Sprite.RsiState);
sprite.LayerSetDirOffset(layer, ToOffset(layerKey));
for (byte i = 0; i < numberOfPipeLayers; i++)
{
var layerName = layerKey.ToString() + i.ToString();
var layer = _sprite.LayerMapReserve((uid, sprite), layerName);
_sprite.LayerSetRsi((uid, sprite), layer, component.Sprite[i].RsiPath);
_sprite.LayerSetRsiState((uid, sprite), layer, component.Sprite[i].RsiState);
_sprite.LayerSetDirOffset((uid, sprite), layer, ToOffset(layerKey));
}
}
}
private void HideAllPipeConnection(SpriteComponent sprite)
private void HideAllPipeConnection(Entity<SpriteComponent> entity, AtmosPipeLayersComponent? atmosPipeLayers, int numberOfPipeLayers)
{
foreach (PipeConnectionLayer layerKey in Enum.GetValues(typeof(PipeConnectionLayer)))
{
if (!sprite.LayerMapTryGet(layerKey, out var key))
continue;
var sprite = entity.Comp;
var layer = sprite[key];
layer.Visible = false;
foreach (var layerKey in Enum.GetValues<PipeConnectionLayer>())
{
for (byte i = 0; i < numberOfPipeLayers; i++)
{
var layerName = layerKey.ToString() + i.ToString();
if (!_sprite.LayerMapTryGet(entity.AsNullable(), layerName, out var key, false))
continue;
var layer = sprite[key];
layer.Visible = false;
}
}
}
@ -59,33 +73,45 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
return;
}
if (!_appearance.TryGetData<PipeDirection>(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component))
var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out var atmosPipeLayers);
if (!_appearance.TryGetData<int>(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component))
{
HideAllPipeConnection(args.Sprite);
HideAllPipeConnection((uid, args.Sprite), atmosPipeLayers, numberOfPipeLayers);
return;
}
if (!_appearance.TryGetData<Color>(uid, PipeColorVisuals.Color, out var color, args.Component))
color = Color.White;
// transform connected directions to local-coordinates
var connectedDirections = worldConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation);
foreach (PipeConnectionLayer layerKey in Enum.GetValues(typeof(PipeConnectionLayer)))
for (byte i = 0; i < numberOfPipeLayers; i++)
{
if (!args.Sprite.LayerMapTryGet(layerKey, out var key))
continue;
// Extract the cardinal pipe orientations for the current pipe layer
// '15' is the four bit mask that is used to extract the pipe orientations of interest from 'worldConnectedDirections'
// Fun fact: a collection of four bits is called a 'nibble'! They aren't natively supported :(
var pipeLayerConnectedDirections = (PipeDirection)(15 & (worldConnectedDirections >> (PipeDirectionHelpers.PipeDirections * i)));
var layer = args.Sprite[key];
var dir = (PipeDirection) layerKey;
var visible = connectedDirections.HasDirection(dir);
// Transform the connected directions to local-coordinates
var connectedDirections = pipeLayerConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation);
layer.Visible &= visible;
foreach (var layerKey in Enum.GetValues<PipeConnectionLayer>())
{
var layerName = layerKey.ToString() + i.ToString();
if (!visible)
continue;
if (!_sprite.LayerMapTryGet((uid, args.Sprite), layerName, out var key, false))
continue;
layer.Color = color;
var layer = args.Sprite[key];
var dir = (PipeDirection)layerKey;
var visible = connectedDirections.HasDirection(dir);
layer.Visible &= visible;
if (!visible)
continue;
layer.Color = color;
}
}
}

View File

@ -0,0 +1,56 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// The system responsible for updating the appearance of layered gas pipe
/// </summary>
public sealed partial class AtmosPipeLayersSystem : SharedAtmosPipeLayersSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IReflectionManager _reflection = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AtmosPipeLayersComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(Entity<AtmosPipeLayersComponent> ent, ref AppearanceChangeEvent ev)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
if (_appearance.TryGetData<string>(ent, AtmosPipeLayerVisuals.Sprite, out var spriteRsi) &&
_resourceCache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / spriteRsi, out RSIResource? resource))
{
_sprite.SetBaseRsi((ent, sprite), resource.RSI);
}
if (_appearance.TryGetData<Dictionary<string, string>>(ent, AtmosPipeLayerVisuals.SpriteLayers, out var pipeState))
{
foreach (var (layerKey, rsiPath) in pipeState)
{
if (TryParseKey(layerKey, out var @enum))
_sprite.LayerSetRsi((ent, sprite), @enum, new ResPath(rsiPath));
else
_sprite.LayerSetRsi((ent, sprite), layerKey, new ResPath(rsiPath));
}
}
}
private bool TryParseKey(string keyString, [NotNullWhen(true)] out Enum? @enum)
{
return _reflection.TryParseEnumReference(keyString, out @enum);
}
}

View File

@ -2,6 +2,7 @@ using Content.Client.Atmos.Components;
using Content.Shared.Atmos;
using Robust.Client.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Client.Atmos.EntitySystems;
@ -31,9 +32,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
// Need LayerMapTryGet because Init fails if there's no existing sprite / appearancecomp
// which means in some setups (most frequently no AppearanceComp) the layer never exists.
if (TryComp<SpriteComponent>(uid, out var sprite) &&
sprite.LayerMapTryGet(FireVisualLayers.Fire, out var layer))
SpriteSystem.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var layer, false))
{
sprite.RemoveLayer(layer);
SpriteSystem.RemoveLayer((uid, sprite), layer);
}
}
@ -42,11 +43,11 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
if (!TryComp<SpriteComponent>(uid, out var sprite) || !TryComp(uid, out AppearanceComponent? appearance))
return;
sprite.LayerMapReserveBlank(FireVisualLayers.Fire);
sprite.LayerSetVisible(FireVisualLayers.Fire, false);
SpriteSystem.LayerMapReserve((uid, sprite), FireVisualLayers.Fire);
SpriteSystem.LayerSetVisible((uid, sprite), FireVisualLayers.Fire, false);
sprite.LayerSetShader(FireVisualLayers.Fire, "unshaded");
if (component.Sprite != null)
sprite.LayerSetRSI(FireVisualLayers.Fire, component.Sprite);
SpriteSystem.LayerSetRsi((uid, sprite), FireVisualLayers.Fire, new ResPath(component.Sprite));
UpdateAppearance(uid, component, sprite, appearance);
}
@ -59,12 +60,12 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
private void UpdateAppearance(EntityUid uid, FireVisualsComponent component, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!sprite.LayerMapTryGet(FireVisualLayers.Fire, out var index))
if (!SpriteSystem.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var index, false))
return;
AppearanceSystem.TryGetData<bool>(uid, FireVisuals.OnFire, out var onFire, appearance);
AppearanceSystem.TryGetData<float>(uid, FireVisuals.FireStacks, out var fireStacks, appearance);
sprite.LayerSetVisible(index, onFire);
SpriteSystem.LayerSetVisible((uid, sprite), index, onFire);
if (!onFire)
{
@ -78,9 +79,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem<FireVisualsComponent
}
if (fireStacks > component.FireStackAlternateState && !string.IsNullOrEmpty(component.AlternateState))
sprite.LayerSetState(index, component.AlternateState);
SpriteSystem.LayerSetRsiState((uid, sprite), index, component.AlternateState);
else
sprite.LayerSetState(index, component.NormalState);
SpriteSystem.LayerSetRsiState((uid, sprite), index, component.NormalState);
component.LightEntity ??= Spawn(null, new EntityCoordinates(uid, default));
var light = EnsureComp<PointLightComponent>(component.LightEntity.Value);

View File

@ -0,0 +1,29 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
namespace Content.Client.Atmos.EntitySystems;
public sealed class GasTankSystem : SharedGasTankSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasTankComponent, AfterAutoHandleStateEvent>(OnGasTankState);
}
private void OnGasTankState(Entity<GasTankComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
{
bui.Update<GasTankBoundUserInterfaceState>();
}
}
public override void UpdateUserInterface(Entity<GasTankComponent> ent)
{
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
{
bui.Update<GasTankBoundUserInterfaceState>();
}
}
}

View File

@ -9,7 +9,7 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
{
protected override void OnAppearanceChange(EntityUid uid, AtmosAlarmableVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null || !args.Sprite.LayerMapTryGet(component.LayerMap, out var layer))
if (args.Sprite == null || !SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.LayerMap, out var layer, false))
return;
if (!args.AppearanceData.TryGetValue(PowerDeviceVisuals.Powered, out var poweredObject) ||
@ -22,8 +22,8 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
{
foreach (var visLayer in component.HideOnDepowered)
{
if (args.Sprite.LayerMapTryGet(visLayer, out var powerVisibilityLayer))
args.Sprite.LayerSetVisible(powerVisibilityLayer, powered);
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), visLayer, out var powerVisibilityLayer, false))
SpriteSystem.LayerSetVisible((uid, args.Sprite), powerVisibilityLayer, powered);
}
}
@ -31,8 +31,8 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
{
foreach (var (setLayer, powerState) in component.SetOnDepowered)
{
if (args.Sprite.LayerMapTryGet(setLayer, out var setStateLayer))
args.Sprite.LayerSetState(setStateLayer, new RSI.StateId(powerState));
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), setLayer, out var setStateLayer, false))
SpriteSystem.LayerSetRsiState((uid, args.Sprite), setStateLayer, new RSI.StateId(powerState));
}
}
@ -41,7 +41,7 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
&& powered
&& component.AlarmStates.TryGetValue(alarmType, out var state))
{
args.Sprite.LayerSetState(layer, new RSI.StateId(state));
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, new RSI.StateId(state));
}
}
}

View File

@ -74,7 +74,13 @@
<!-- Mode buttons -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'air-alarm-ui-window-mode-label'}" Margin="0 0 2 0" />
<OptionButton Name="CModeButton" HorizontalExpand="True" />
<Control HorizontalExpand="True">
<OptionButton Name="CModeButton" />
<ui:StripeBack Name="CModeSelectLocked">
<RichTextLabel Text="{Loc 'air-alarm-ui-window-mode-select-locked-label'}"
HorizontalAlignment="Center" />
</ui:StripeBack>
</Control>
<CheckBox Name="AutoModeCheckBox" Text="{Loc 'air-alarm-ui-window-auto-mode-label'}" />
</BoxContainer>
</BoxContainer>

View File

@ -110,6 +110,8 @@ public sealed partial class AirAlarmWindow : FancyWindow
{
UpdateDeviceData(addr, dev);
}
_modes.Visible = !state.PanicWireCut;
CModeSelectLocked.Visible = state.PanicWireCut;
}
public void UpdateModeSelector(AirAlarmMode mode)

View File

@ -76,6 +76,7 @@ public sealed partial class ScrubberControl : BoxContainer
_data.PumpDirection = (ScrubberPumpDirection) args.Id;
ScrubberDataChanged?.Invoke(_address, _data);
};
_pumpDirection.Disabled = data.AirAlarmPanicWireCut;
_copySettings.OnPressed += _ =>
{
@ -114,6 +115,7 @@ public sealed partial class ScrubberControl : BoxContainer
_data.PumpDirection = data.PumpDirection;
_pumpDirection.Select((int) _data.PumpDirection);
_pumpDirection.Disabled = data.AirAlarmPanicWireCut;
_data.VolumeRate = data.VolumeRate;
_volumeRate.Value = _data.VolumeRate;

View File

@ -21,6 +21,7 @@ namespace Content.Client.Atmos.Overlays
{
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _xformSys;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
@ -51,6 +52,7 @@ namespace Content.Client.Atmos.Overlays
{
_entManager = entManager;
_mapManager = IoCManager.Resolve<IMapManager>();
_mapSystem = entManager.System<SharedMapSystem>();
_xformSys = xformSys;
_shader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
ZIndex = GasOverlayZIndex;
@ -163,7 +165,7 @@ namespace Content.Client.Atmos.Overlays
xformQuery,
_xformSys);
var mapUid = _mapManager.GetMapEntityId(args.MapId);
var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
if (_entManager.TryGetComponent<MapAtmosphereComponent>(mapUid, out var atmos))
DrawMapOverlay(drawHandle, args, mapUid, atmos);

View File

@ -0,0 +1,8 @@
using Content.Shared.Atmos.Piping.Binary.Systems;
namespace Content.Client.Atmos.Piping.Binary.Systems;
public sealed class GasValveSystem : SharedGasValveSystem
{
}

View File

@ -0,0 +1,29 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Binary.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Atmos.Piping.Binary.Systems;
public sealed class GasVolumePumpSystem : SharedGasVolumePumpSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasVolumePumpComponent, AfterAutoHandleStateEvent>(OnPumpState);
}
protected override void UpdateUi(Entity<GasVolumePumpComponent> entity)
{
if (_ui.TryGetOpenUi(entity.Owner, GasVolumePumpUiKey.Key, out var bui))
{
bui.Update();
}
}
private void OnPumpState(Entity<GasVolumePumpComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
}

View File

@ -0,0 +1,32 @@
using Content.Client.Atmos.UI;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Piping.Unary.Systems;
using Content.Shared.NodeContainer;
namespace Content.Client.Atmos.Piping.Unary.Systems;
public sealed class GasCanisterSystem : SharedGasCanisterSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasCanisterComponent, AfterAutoHandleStateEvent>(OnGasState);
}
private void OnGasState(Entity<GasCanisterComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(ent.Owner, GasCanisterUiKey.Key, out var bui))
{
bui.Update<GasCanisterBoundUserInterfaceState>();
}
}
protected override void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null)
{
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(uid, GasCanisterUiKey.Key, out var bui))
{
bui.Update<GasCanisterBoundUserInterfaceState>();
}
}
}

View File

@ -0,0 +1,29 @@
using Content.Client.Atmos.UI;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Piping.Unary.Systems;
namespace Content.Client.Atmos.Piping.Unary.Systems;
public sealed class GasThermoMachineSystem : SharedGasThermoMachineSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasThermoMachineComponent, AfterAutoHandleStateEvent>(OnGasAfterState);
}
private void OnGasAfterState(Entity<GasThermoMachineComponent> ent, ref AfterAutoHandleStateEvent args)
{
DirtyUI(ent.Owner, ent.Comp);
}
protected override void DirtyUI(EntityUid uid, GasThermoMachineComponent? thermoMachine, UserInterfaceComponent? ui = null)
{
if (_ui.TryGetOpenUi<GasThermomachineBoundUserInterface>(uid, ThermomachineUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@ -1,4 +1,4 @@
using Robust.Client.GameObjects;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
@ -18,6 +18,7 @@ namespace Content.Client.Atmos.UI
base.Open();
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
_window.OnClose += Close;
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
@ -29,15 +30,6 @@ namespace Content.Client.Atmos.UI
_window.Populate(cast);
}
/// <summary>
/// Closes UI and tells the server to disable the analyzer
/// </summary>
private void OnClose()
{
SendMessage(new GasAnalyzerDisableMessage());
Close();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@ -136,6 +136,7 @@ namespace Content.Client.Atmos.UI
else
{
// oh shit of fuck its more than 4 this ui isn't gonna look pretty anymore
CDeviceMixes.RemoveAllChildren();
for (var i = 1; i < msg.NodeGasMixes.Length; i++)
{
GenerateGasDisplay(msg.NodeGasMixes[i], CDeviceMixes);

View File

@ -1,4 +1,7 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.IdentityManagement;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
@ -32,22 +35,22 @@ namespace Content.Client.Atmos.UI
private void OnTankEjectPressed()
{
SendMessage(new GasCanisterHoldingTankEjectMessage());
SendPredictedMessage(new GasCanisterHoldingTankEjectMessage());
}
private void OnReleasePressureSet(float value)
{
SendMessage(new GasCanisterChangeReleasePressureMessage(value));
SendPredictedMessage(new GasCanisterChangeReleasePressureMessage(value));
}
private void OnReleaseValveOpenPressed()
{
SendMessage(new GasCanisterChangeReleaseValveMessage(true));
SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(true));
}
private void OnReleaseValveClosePressed()
{
SendMessage(new GasCanisterChangeReleaseValveMessage(false));
SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(false));
}
/// <summary>
@ -57,17 +60,21 @@ namespace Content.Client.Atmos.UI
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast)
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast || !EntMan.TryGetComponent(Owner, out GasCanisterComponent? component))
return;
_window.SetCanisterLabel(cast.CanisterLabel);
var canisterLabel = Identity.Name(Owner, EntMan);
var tankLabel = component.GasTankSlot.Item != null ? Identity.Name(component.GasTankSlot.Item.Value, EntMan) : null;
_window.SetCanisterLabel(canisterLabel);
_window.SetCanisterPressure(cast.CanisterPressure);
_window.SetPortStatus(cast.PortStatus);
_window.SetTankLabel(cast.TankLabel);
_window.SetTankLabel(tankLabel);
_window.SetTankPressure(cast.TankPressure);
_window.SetReleasePressureRange(cast.ReleasePressureMin, cast.ReleasePressureMax);
_window.SetReleasePressure(cast.ReleasePressure);
_window.SetReleaseValve(cast.ReleaseValve);
_window.SetReleasePressureRange(component.MinReleasePressure, component.MaxReleasePressure);
_window.SetReleasePressure(component.ReleasePressure);
_window.SetReleaseValve(component.ReleaseValve);
}
protected override void Dispose(bool disposing)

View File

@ -13,9 +13,6 @@ namespace Content.Client.Atmos.UI;
[UsedImplicitly]
public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private const float MaxPressure = Atmospherics.MaxOutputPressure;
[ViewVariables]
private GasPressurePumpWindow? _window;

View File

@ -1,5 +1,8 @@
using Content.Shared.Atmos;
using Content.Client.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Piping.Unary.Systems;
using Content.Shared.Power.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
@ -36,6 +39,8 @@ namespace Content.Client.Atmos.UI
_window.ToggleStatusButton.OnPressed += _ => OnToggleStatusButtonPressed();
_window.TemperatureSpinbox.OnValueChanged += _ => OnTemperatureChanged(_window.TemperatureSpinbox.Value);
_window.Entity = Owner;
Update();
}
private void OnToggleStatusButtonPressed()
@ -43,7 +48,7 @@ namespace Content.Client.Atmos.UI
if (_window is null) return;
_window.SetActive(!_window.Active);
SendMessage(new GasThermomachineToggleMessage());
SendPredictedMessage(new GasThermomachineToggleMessage());
}
private void OnTemperatureChanged(float value)
@ -60,25 +65,32 @@ namespace Content.Client.Atmos.UI
return;
}
SendMessage(new GasThermomachineChangeTemperatureMessage(actual));
SendPredictedMessage(new GasThermomachineChangeTemperatureMessage(actual));
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
if (_window == null || state is not GasThermomachineBoundUserInterfaceState cast)
if (_window == null || !EntMan.TryGetComponent(Owner, out GasThermoMachineComponent? thermo))
return;
_minTemp = cast.MinTemperature;
_maxTemp = cast.MaxTemperature;
_isHeater = cast.IsHeater;
var system = EntMan.System<SharedGasThermoMachineSystem>();
_minTemp = thermo.MinTemperature;
_maxTemp = thermo.MaxTemperature;
_isHeater = system.IsHeater(thermo);
_window.SetTemperature(thermo.TargetTemperature);
var receiverSys = EntMan.System<PowerReceiverSystem>();
SharedApcPowerReceiverComponent? receiver = null;
receiverSys.ResolveApc(Owner, ref receiver);
// Also set in frameupdates.
if (receiver != null)
{
_window.SetActive(!receiver.PowerDisabled);
}
_window.SetTemperature(cast.Temperature);
_window.SetActive(cast.Enabled);
_window.Title = _isHeater switch
{
false => Loc.GetString("comp-gas-thermomachine-ui-title-freezer"),

View File

@ -1,5 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="300 120" Title="{Loc comp-gas-thermomachine-ui-title-freezer}">
<BoxContainer Name="VboxContainer" Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
@ -11,4 +12,4 @@
<Label Text="{Loc comp-gas-thermomachine-ui-temperature}"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@ -1,19 +1,26 @@
using Content.Client.Power.Components;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Atmos.UI;
[GenerateTypedNameReferences]
public sealed partial class GasThermomachineWindow : DefaultWindow
public sealed partial class GasThermomachineWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
public bool Active = true;
public FloatSpinBox TemperatureSpinbox;
public EntityUid Entity;
public GasThermomachineWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
SpinboxHBox.AddChild(
@ -27,12 +34,10 @@ public sealed partial class GasThermomachineWindow : DefaultWindow
if (active)
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-thermomachine-ui-status-enabled");
ToggleStatusButton.Pressed = true;
}
else
{
ToggleStatusButton.Text = Loc.GetString("comp-gas-thermomachine-ui-status-disabled");
ToggleStatusButton.Pressed = false;
}
}
@ -40,4 +45,14 @@ public sealed partial class GasThermomachineWindow : DefaultWindow
{
TemperatureSpinbox.Value = temperature;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_entManager.TryGetComponent(Entity, out ApcPowerReceiverComponent? receiver))
{
SetActive(!receiver.PowerDisabled);
}
}
}

View File

@ -1,8 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.Atmos.UI
@ -14,7 +13,7 @@ namespace Content.Client.Atmos.UI
public sealed class GasVolumePumpBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private const float MaxTransferRate = Atmospherics.MaxTransferRate;
private float _maxTransferRate;
[ViewVariables]
private GasVolumePumpWindow? _window;
@ -29,38 +28,41 @@ namespace Content.Client.Atmos.UI
_window = this.CreateWindow<GasVolumePumpWindow>();
if (EntMan.TryGetComponent(Owner, out GasVolumePumpComponent? pump))
{
_maxTransferRate = pump.MaxTransferRate;
}
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
_window.PumpTransferRateChanged += OnPumpTransferRatePressed;
Update();
}
private void OnToggleStatusButtonPressed()
{
if (_window is null) return;
SendMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
SendPredictedMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
}
private void OnPumpTransferRatePressed(string value)
{
var rate = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f;
if (rate > MaxTransferRate)
rate = MaxTransferRate;
rate = Math.Clamp(rate, 0f, _maxTransferRate);
SendMessage(new GasVolumePumpChangeTransferRateMessage(rate));
SendPredictedMessage(new GasVolumePumpChangeTransferRateMessage(rate));
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
if (_window == null || state is not GasVolumePumpBoundUserInterfaceState cast)
base.Update();
if (_window is null || !EntMan.TryGetComponent(Owner, out GasVolumePumpComponent? pump))
return;
_window.Title = cast.PumpLabel;
_window.SetPumpStatus(cast.Enabled);
_window.SetTransferRate(cast.TransferRate);
_window.Title = Identity.Name(Owner, EntMan);
_window.SetPumpStatus(pump.Enabled);
_window.SetTransferRate(pump.TransferRate);
}
}
}

View File

@ -1,5 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="200 120" Title="Volume Pump">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
@ -19,4 +20,4 @@
<Button Name="SetTransferRateButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" HorizontalAlignment="Right" Disabled="True"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using Content.Client.Atmos.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Prototypes;
using Robust.Client.AutoGenerated;
@ -16,7 +17,7 @@ namespace Content.Client.Atmos.UI
/// Client-side UI used to control a gas volume pump.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class GasVolumePumpWindow : DefaultWindow
public sealed partial class GasVolumePumpWindow : FancyWindow
{
public bool PumpStatus = true;

View File

@ -2,35 +2,35 @@ using Robust.Client.GameObjects;
using Content.Shared.Atmos.Visuals;
using Content.Client.Power;
namespace Content.Client.Atmos.Visualizers
namespace Content.Client.Atmos.Visualizers;
/// <summary>
/// Controls client-side visuals for portable scrubbers.
/// </summary>
public sealed class PortableScrubberSystem : VisualizerSystem<PortableScrubberVisualsComponent>
{
/// <summary>
/// Controls client-side visuals for portable scrubbers.
/// </summary>
public sealed class PortableScrubberSystem : VisualizerSystem<PortableScrubberVisualsComponent>
protected override void OnAppearanceChange(EntityUid uid, PortableScrubberVisualsComponent component, ref AppearanceChangeEvent args)
{
protected override void OnAppearanceChange(EntityUid uid, PortableScrubberVisualsComponent component, ref AppearanceChangeEvent args)
if (args.Sprite == null)
return;
if (AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsFull, out var isFull, args.Component)
&& AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsRunning, out var isRunning, args.Component))
{
if (args.Sprite == null)
return;
var runningState = isRunning ? component.RunningState : component.IdleState;
SpriteSystem.LayerSetRsiState((uid, args.Sprite), PortableScrubberVisualLayers.IsRunning, runningState);
if (AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsFull, out var isFull, args.Component)
&& AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsRunning, out var isRunning, args.Component))
{
var runningState = isRunning ? component.RunningState : component.IdleState;
args.Sprite.LayerSetState(PortableScrubberVisualLayers.IsRunning, runningState);
var fullState = isFull ? component.FullState : component.ReadyState;
SpriteSystem.LayerSetRsiState((uid, args.Sprite), PowerDeviceVisualLayers.Powered, fullState);
}
var fullState = isFull ? component.FullState : component.ReadyState;
args.Sprite.LayerSetState(PowerDeviceVisualLayers.Powered, fullState);
}
if (AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsDraining, out var isDraining, args.Component))
{
args.Sprite.LayerSetVisible(PortableScrubberVisualLayers.IsDraining, isDraining);
}
if (AppearanceSystem.TryGetData<bool>(uid, PortableScrubberVisuals.IsDraining, out var isDraining, args.Component))
{
SpriteSystem.LayerSetVisible((uid, args.Sprite), PortableScrubberVisualLayers.IsDraining, isDraining);
}
}
}
public enum PortableScrubberVisualLayers : byte
{
IsRunning,

View File

@ -2,16 +2,16 @@ using Robust.Shared.Console;
namespace Content.Client.Audio;
public sealed class AmbientOverlayCommand : IConsoleCommand
public sealed class AmbientOverlayCommand : LocalizedEntityCommands
{
public string Command => "showambient";
public string Description => "Shows all AmbientSoundComponents in the viewport";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AmbientSoundSystem>();
system.OverlayEnabled ^= true;
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
shell.WriteLine($"Ambient sound overlay set to {system.OverlayEnabled}");
public override string Command => "showambient";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_ambient.OverlayEnabled ^= true;
shell.WriteLine(Loc.GetString($"cmd-showambient-status", ("status", _ambient.OverlayEnabled)));
}
}

View File

@ -12,6 +12,7 @@ public sealed class JukeboxSystem : SharedJukeboxSystem
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@ -64,7 +65,7 @@ public sealed class JukeboxSystem : SharedJukeboxSystem
visualState = JukeboxVisualState.On;
}
UpdateAppearance(uid, visualState, component, sprite);
UpdateAppearance((uid, sprite), visualState, component);
}
private void OnAppearanceChange(EntityUid uid, JukeboxComponent component, ref AppearanceChangeEvent args)
@ -78,25 +79,25 @@ public sealed class JukeboxSystem : SharedJukeboxSystem
visualState = JukeboxVisualState.On;
}
UpdateAppearance(uid, visualState, component, args.Sprite);
UpdateAppearance((uid, args.Sprite), visualState, component);
}
private void UpdateAppearance(EntityUid uid, JukeboxVisualState visualState, JukeboxComponent component, SpriteComponent sprite)
private void UpdateAppearance(Entity<SpriteComponent> entity, JukeboxVisualState visualState, JukeboxComponent component)
{
SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
SetLayerState(JukeboxVisualLayers.Base, component.OffState, entity);
switch (visualState)
{
case JukeboxVisualState.On:
SetLayerState(JukeboxVisualLayers.Base, component.OnState, sprite);
SetLayerState(JukeboxVisualLayers.Base, component.OnState, entity);
break;
case JukeboxVisualState.Off:
SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
SetLayerState(JukeboxVisualLayers.Base, component.OffState, entity);
break;
case JukeboxVisualState.Select:
PlayAnimation(uid, JukeboxVisualLayers.Base, component.SelectState, 1.0f, sprite);
PlayAnimation(entity.Owner, JukeboxVisualLayers.Base, component.SelectState, 1.0f, entity);
break;
}
}
@ -109,7 +110,7 @@ public sealed class JukeboxSystem : SharedJukeboxSystem
if (!_animationPlayer.HasRunningAnimation(uid, state))
{
var animation = GetAnimation(layer, state, animationTime);
sprite.LayerSetVisible(layer, true);
_sprite.LayerSetVisible((uid, sprite), layer, true);
_animationPlayer.Play(uid, animation, state);
}
}
@ -133,13 +134,13 @@ public sealed class JukeboxSystem : SharedJukeboxSystem
};
}
private void SetLayerState(JukeboxVisualLayers layer, string? state, SpriteComponent sprite)
private void SetLayerState(JukeboxVisualLayers layer, string? state, Entity<SpriteComponent> sprite)
{
if (string.IsNullOrEmpty(state))
return;
sprite.LayerSetVisible(layer, true);
sprite.LayerSetAutoAnimated(layer, true);
sprite.LayerSetState(layer, state);
_sprite.LayerSetVisible(sprite.AsNullable(), layer, true);
_sprite.LayerSetAutoAnimated(sprite.AsNullable(), layer, true);
_sprite.LayerSetRsiState(sprite.AsNullable(), layer, state);
}
}

View File

@ -41,12 +41,12 @@ public sealed class BarSignSystem : VisualizerSystem<BarSignComponent>
&& sign.Current != null
&& _prototypeManager.TryIndex(sign.Current, out var proto))
{
sprite.LayerSetSprite(0, proto.Icon);
SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
sprite.LayerSetShader(0, "unshaded");
}
else
{
sprite.LayerSetState(0, "empty");
SpriteSystem.LayerSetRsiState((id, sprite), 0, "empty");
sprite.LayerSetShader(0, null, null);
}
}

View File

@ -7,6 +7,8 @@ namespace Content.Client.Beam;
public sealed class BeamSystem : SharedBeamSystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
@ -21,11 +23,11 @@ public sealed class BeamSystem : SharedBeamSystem
if (TryComp<SpriteComponent>(beam, out var sprites))
{
sprites.Rotation = args.UserAngle;
_sprite.SetRotation((beam, sprites), args.UserAngle);
if (args.BodyState != null)
{
sprites.LayerSetState(0, args.BodyState);
_sprite.LayerSetRsiState((beam, sprites), 0, args.BodyState);
sprites.LayerSetShader(0, args.Shader);
}
}

View File

@ -0,0 +1,8 @@
using Content.Shared.Bed;
namespace Content.Client.Bed;
public sealed class BedSystem : SharedBedSystem
{
}

View File

@ -10,7 +10,7 @@ public sealed class StasisBedSystem : VisualizerSystem<StasisBedVisualsComponent
if (args.Sprite != null
&& AppearanceSystem.TryGetData<bool>(uid, StasisBedVisuals.IsOn, out var isOn, args.Component))
{
args.Sprite.LayerSetVisible(StasisBedVisualLayers.IsOn, isOn);
SpriteSystem.LayerSetVisible((uid, args.Sprite), StasisBedVisualLayers.IsOn, isOn);
}
}
}

View File

@ -0,0 +1,24 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
public sealed class InternalsSystem : SharedInternalsSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InternalsComponent, AfterAutoHandleStateEvent>(OnInternalsAfterState);
}
private void OnInternalsAfterState(Entity<InternalsComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.GasTankEntity != null && _ui.TryGetOpenUi(ent.Comp.GasTankEntity.Value, SharedGasTankUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@ -1,6 +1,7 @@
using Content.Client.Botany.Components;
using Content.Shared.Botany;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
namespace Content.Client.Botany;
@ -17,8 +18,8 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem<PlantHolderVi
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
sprite.LayerMapReserveBlank(PlantHolderLayers.Plant);
sprite.LayerSetVisible(PlantHolderLayers.Plant, false);
SpriteSystem.LayerMapReserve((uid, sprite), PlantHolderLayers.Plant);
SpriteSystem.LayerSetVisible((uid, sprite), PlantHolderLayers.Plant, false);
}
protected override void OnAppearanceChange(EntityUid uid, PlantHolderVisualsComponent component, ref AppearanceChangeEvent args)
@ -31,12 +32,12 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem<PlantHolderVi
{
var valid = !string.IsNullOrWhiteSpace(state);
args.Sprite.LayerSetVisible(PlantHolderLayers.Plant, valid);
SpriteSystem.LayerSetVisible((uid, args.Sprite), PlantHolderLayers.Plant, valid);
if (valid)
{
args.Sprite.LayerSetRSI(PlantHolderLayers.Plant, rsi);
args.Sprite.LayerSetState(PlantHolderLayers.Plant, state);
SpriteSystem.LayerSetRsi((uid, args.Sprite), PlantHolderLayers.Plant, new ResPath(rsi));
SpriteSystem.LayerSetRsiState((uid, args.Sprite), PlantHolderLayers.Plant, state);
}
}
}

View File

@ -15,7 +15,7 @@ public sealed class PotencyVisualsSystem : VisualizerSystem<PotencyVisualsCompon
if (AppearanceSystem.TryGetData<float>(uid, ProduceVisuals.Potency, out var potency, args.Component))
{
var scale = MathHelper.Lerp(component.MinimumScale, component.MaximumScale, potency / 100);
args.Sprite.Scale = new Vector2(scale, scale);
SpriteSystem.SetScale((uid, args.Sprite), new Vector2(scale, scale));
}
}
}

View File

@ -13,6 +13,7 @@ internal sealed class BuckleSystem : SharedBuckleSystem
[Dependency] private readonly RotationVisualizerSystem _rotationVisualizerSystem = default!;
[Dependency] private readonly IEyeManager _eye = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@ -69,11 +70,11 @@ internal sealed class BuckleSystem : SharedBuckleSystem
{
// This will only assign if empty, it won't get overwritten by new depth on multiple calls, which do happen easily
buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth;
buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
_sprite.SetDrawDepth((buckledEntity, buckledSprite), strapSprite.DrawDepth - 1);
}
else if (buckle.OriginalDrawDepth.HasValue)
{
buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value;
_sprite.SetDrawDepth((buckledEntity, buckledSprite), buckle.OriginalDrawDepth.Value);
buckle.OriginalDrawDepth = null;
}
}
@ -97,7 +98,7 @@ internal sealed class BuckleSystem : SharedBuckleSystem
return;
ent.Comp.OriginalDrawDepth ??= buckledSprite.DrawDepth;
buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
_sprite.SetDrawDepth((ent.Owner, buckledSprite), strapSprite.DrawDepth - 1);
}
/// <summary>
@ -111,7 +112,7 @@ internal sealed class BuckleSystem : SharedBuckleSystem
if (!ent.Comp.OriginalDrawDepth.HasValue)
return;
buckledSprite.DrawDepth = ent.Comp.OriginalDrawDepth.Value;
_sprite.SetDrawDepth((ent.Owner, buckledSprite), ent.Comp.OriginalDrawDepth.Value);
ent.Comp.OriginalDrawDepth = null;
}

View File

@ -13,6 +13,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private EntityQuery<BodyComponent> _bodyQuery;
@ -74,7 +75,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp<SpriteComponent>(ent, out var sprite))
continue;
sprite.Offset = new Vector2(0, 1);
_sprite.SetOffset((ent, sprite), new Vector2(0, 1));
_transform.SetParent(ent, entTransform, mob);
}

View File

@ -138,6 +138,11 @@ namespace Content.Client.Cargo.BUI
AccountName = cState.Name;
if (_menu == null)
return;
_menu.ProductCatalogue = cState.Products;
_menu?.UpdateStation(station);
Populate(cState.Orders);
}

View File

@ -10,6 +10,7 @@ namespace Content.Client.Cargo.Systems;
public sealed partial class CargoSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private static readonly Animation CargoTelepadBeamAnimation = new()
{
@ -75,18 +76,17 @@ public sealed partial class CargoSystem
switch (state)
{
case CargoTelepadState.Teleporting:
if (_player.HasRunningAnimation(uid, TelepadBeamKey))
return;
_player.Stop(uid, player, TelepadIdleKey);
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
_player.Stop((uid, player), TelepadIdleKey);
if (!_player.HasRunningAnimation(uid, TelepadBeamKey))
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
break;
case CargoTelepadState.Unpowered:
sprite.LayerSetVisible(CargoTelepadLayers.Beam, false);
_sprite.LayerSetVisible((uid, sprite), CargoTelepadLayers.Beam, false);
_player.Stop(uid, player, TelepadBeamKey);
_player.Stop(uid, player, TelepadIdleKey);
break;
default:
sprite.LayerSetVisible(CargoTelepadLayers.Beam, true);
_sprite.LayerSetVisible((uid, sprite), CargoTelepadLayers.Beam, true);
if (_player.HasRunningAnimation(uid, player, TelepadIdleKey) ||
_player.HasRunningAnimation(uid, player, TelepadBeamKey))

View File

@ -38,9 +38,10 @@
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<Control MinHeight="5" Name="OrdersSpacer"/>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="1">
SizeFlagsStretchRatio="1"
Name="Orders">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000" />
</PanelContainer.PanelOverride>

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