Compare commits

..

3 Commits

Author SHA1 Message Date
comasqw
a84a79ba64 small optimizations 2024-12-14 00:12:01 +04:00
comasqw
c4445c8a87 Create README.md 2024-12-13 21:48:10 +04:00
comasqw
1f1b9132d7 color replacer tool 2024-12-13 21:40:49 +04:00
19605 changed files with 1493958 additions and 1867915 deletions

View File

@@ -77,8 +77,6 @@ csharp_style_expression_bodied_methods = false:suggestion
#csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_namespace_declarations = file_scoped:suggestion
# Pattern matching preferences
#csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
#csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
@@ -129,7 +127,6 @@ 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
@@ -347,9 +344,6 @@ resharper_keep_existing_attribute_arrangement = true
resharper_wrap_chained_binary_patterns = chop_if_long
resharper_wrap_chained_method_calls = chop_if_long
resharper_csharp_trailing_comma_in_multiline_lists = true
resharper_csharp_qualified_using_at_nested_scope = false
resharper_csharp_prefer_qualified_reference = false
resharper_csharp_allow_alias = false
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
indent_size = 2

5
.envrc
View File

@@ -1,5 +1,4 @@
set -e
if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM="
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
fi
use flake

66
.github/CODEOWNERS vendored
View File

@@ -1,2 +1,64 @@
# TheShuEd
* @TheShuEd
# Last match in file takes precedence.
# Sorting by path instead of by who added it one day :(
# this isn't how codeowners rules work pls read the first comment instead of trying to force a sorting order
/Resources/ConfigPresets/WizardsDen/ @Chief-Engineer
# Moony's Gargantuan List Of Things She Cares About, or MGLOTSCA for short.
# You need to add your name to these entries, not make a new one, if you care about them.
/Content.*/Toolshed/ @moonheart08
**/Toolshed/** @moonheart08
*Command.cs @moonheart08
/Content.*/Administration/ @moonheart08 @DrSmugleaf @Chief-Engineer
/Content.*/Station/ @moonheart08
/Content.*/Maps/ @moonheart08
/Content.*/GameTicking/ @moonheart08 @EmoGarbage404
/Resources/ServerInfo/ @moonheart08 @Chief-Engineer
/Resources/ServerInfo/Guidebook/ @moonheart08 @EmoGarbage404
/Resources/ServerInfo/Guidebook/ServerRules/ @Chief-Engineer
/Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer
/Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer
/Resources/Prototypes/Maps/** @Emisse
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
/Resources/Prototypes/Guidebook/rules.yml @Chief-Engineer
/Content.*/Body/ @DrSmugleaf
/Content.YAMLLinter @DrSmugleaf
/Content.Shared/Damage/ @DrSmugleaf
/Content.*/Anomaly/ @EmoGarbage404 @TheShuEd
/Content.*/Lathe/ @EmoGarbage404
/Content.*/Materials/ @EmoGarbage404
/Content.*/Mech/ @EmoGarbage404
/Content.*/Research/ @EmoGarbage404
/Content.*/Stack/ @EmoGarbage404
/Content.*/Xenoarchaeology/ @EmoGarbage404
/Content.*/Zombies/ @EmoGarbage404
/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @EmoGarbage404 @TheShuEd
/Resources/Prototypes/Research/ @EmoGarbage404
/Content.*/Forensics/ @ficcialfaint
# SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf
/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @Chief-Engineer
/Pow3r/ @PJB3005
/Content.Server/Power/Pow3r/ @PJB3005
# notafet
/Content.*/Atmos/ @Partmedia
/Content.*/Botany/ @Partmedia
#Jezi
/Content.*/Medical @Jezithyr
/Content.*/Body @Jezithyr
# Sloth
/Content.*/Audio @metalgearsloth
/Content.*/Movement @metalgearsloth
/Content.*/NPC @metalgearsloth
/Content.*/Shuttles @metalgearsloth
/Content.*/Weapons @metalgearsloth

22
.github/FUNDING.yml vendored
View File

@@ -1,14 +1,14 @@
# These are supported funding model platforms
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username
#open_collective: # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
#liberapay: # Replace with a single Liberapay username
#issuehunt: # Replace with a single IssueHunt username
#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
#polar: # Replace with a single Polar username
#buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
custom: ['https://boosty.to/theshued']

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
contact_links:
- name: Report a Security Vulnerability
url: https://github.com/space-wizards/space-station-14/blob/master/SECURITY.md
about: Please report security vulnerabilities privately so we can fix them before they are publicly disclosed.
- name: Request a Feature
url: https://discord.gg/rGvu9hKffJ
about: Submit feature requests on our Discord server (https://discord.gg/rGvu9hKffJ).

View File

@@ -1,28 +0,0 @@
name: "Request content or suggest an idea"
description: "Suggest content, mechanics, or other changes that you think would benefit the project."
labels: [suggestion]
type: Suggestion
body:
- type: textarea
id: concept
attributes:
label: Concept
placeholder: |
What is the nature of your proposal?
Example:
Add a new role ____ that performs tasks related to ____ and ____.
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Why is this necessary?
placeholder: |
Explain how it would improve the game, or what problems it would fix. Suggestions without proper reasoning and reasons for the addition may be closed.
Example:
Currently, ____ often occurs in the round, which is why ____ is missing. This role could close this need by doing _____.
validations:
required: true

View File

@@ -1,28 +0,0 @@
name: "Запросить контент или предложить идею"
description: "Предложите контент, механику или другие изменения, которые вы считаете принесут пользу проекту."
labels: [suggestion]
type: Suggestion
body:
- type: textarea
id: concept
attributes:
label: Концепция
placeholder: |
В чем заключается ваше предложение?
Пример:
Добавить новую роль ____, которая выполняет задачи связанные с ____ и ____.
validations:
required: true
- type: textarea
id: motivation
attributes:
label: Зачем это нужно?
placeholder: |
Объясните, как это улучшит игру, или какие проблемы это исправит. Предложения без должной аргументации и причины добавления могут быть закрыты.
Пример:
На текущий момент, в раунде часто происходит ____, из за чего не хватает ____. Эта роль могла бы закрыть эту потребность, выполняя _____.
validations:
required: true

20
.github/ISSUE_TEMPLATE/issue_report.md vendored Normal file
View File

@@ -0,0 +1,20 @@
---
name: Report an Issue
about: "..."
title: ''
labels: ''
assignees: ''
---
## Description
<!-- Explain your issue in detail. Issues without proper explanation are liable to be closed by maintainers. -->
**Reproduction**
<!-- Include the steps to reproduce if applicable. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Additional context**
<!-- Add any other context about the problem here. Anything you think is related to the issue. -->

View File

@@ -1,49 +0,0 @@
name: "Report an Issue"
description: "Report a bug or problem you have found"
type: Bug
body:
- type: textarea
id: description
attributes:
label: "📄 Description"
description: |
Explain your issue in detail. Include what you expected to happen.
placeholder: |
When I click the "Save" button, nothing happens.
validations:
required: true
- type: textarea
id: steps
attributes:
label: "🔁 Reproduction Steps"
description: |
What steps lead to the problem? Try to list them clearly and precisely.
placeholder: |
1. Open the app
2. Go to Settings
3. Click "Save"
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: "🖼️ Screenshots"
description: |
If applicable, add screenshots to help explain your problem.
placeholder: |
You can paste images here.
validations:
required: false
- type: textarea
id: context
attributes:
label: " Additional Context"
description: |
Any other context about the problem. Logs, version numbers, configs, etc.
placeholder: |
Occurs only on Android 11, using version 1.2.3.
validations:
required: false

View File

@@ -1,50 +0,0 @@
name: "Сообщить об ошибке"
description: "Сообщите о найденной ошибке или проблеме"
type: Bug
body:
- type: textarea
id: description
attributes:
label: "📄 Описание"
description: |
Подробно опишите вашу проблему. Укажите, что должно было произойти.
placeholder: |
Когда я нажимаю на кнопку "Сохранить", ничего не происходит, и персонаж не сохраняется.
validations:
required: true
- type: textarea
id: steps
attributes:
label: "🔁 Шаги воспроизведения"
description: |
Какие шаги приводят к этой ошибке? Укажите их по порядку.
placeholder: |
1. Открыть приложение
2. Зайти в меню создания персонажа
3. Изменить цвет волос
4. Нажать "Сохранить"
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: "🖼️ Скриншоты"
description: |
При наличии, добавьте скриншоты или изображения.
placeholder: |
Вы можете вставить изображения прямо сюда.
validations:
required: false
- type: textarea
id: context
attributes:
label: "ℹ️Дополнительная информация"
description: |
Дополнительные сведения: логи, версии, настройки и всё, что может быть связано.
placeholder: |
Возникает только на MacOS
validations:
required: false

View File

@@ -0,0 +1,18 @@
---
name: Toolshed feature request
about: Suggest a feature for Toolshed (for game admins/developers)
title: "[TOOLSHED REQUEST]"
labels: Toolshed
assignees: moonheart08
---
**Is your feature request related to a problem/bug? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the command you'd like**
A clear and concise description of what you want and what it should do.
If you're a technical user (i.e. programmer) including type signatures is helpful.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,23 +1,17 @@
## About the PR
<!-- What did you change in this PR? -->
<!-- Что вы изменили в своем пулл реквесте? -->
## Why / Balance
<!-- Why was it changed? Link any discussions or issues here. Please discuss how this would affect game balance. -->
<!-- Зачем нужно это изменение? Прикрепите любые обсуждения или проблемы здесь. Опишите, как это повлияет на текущий баланс игры. -->
## Media
<!--
PRs which make ingame changes (adding clothing, items, new features, etc) are required to have media attached that showcase the changes.
Small fixes/refactors are exempt.
-->
**Changelog**
<!-- Add a Changelog entry to make players aware of new features or changes that could affect gameplay.
Make sure to read the guidelines and take this Changelog template out of the comment block in order for it to show up.
Changelog must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog.
:cl:
- add: Added fun!
- remove: Removed fun!
- tweak: Changed fun!
- fix: Fixed fun!
<!--
Пулл реквесты, которые несут за собой игровые изменения (добавления одежды, предметов и так далее) требуют чтобы вы прикрепили скриншоты или видеоролики, демонстрирующие эти изменения.
Небольшие исправления не считаются.
-->

22
.github/labeler.yml vendored
View File

@@ -1,34 +1,22 @@
"Sprites":
"Changes: Sprites":
- changed-files:
- any-glob-to-any-file: '**/*.rsi/*.png'
"Mapping":
"Changes: Map":
- changed-files:
- any-glob-to-any-file:
- 'Resources/Maps/**/*.yml'
- 'Resources/Prototypes/Maps/**/*.yml'
"UI":
"Changes: UI":
- changed-files:
- any-glob-to-any-file: '**/*.xaml*'
"Shaders":
"Changes: Shaders":
- changed-files:
- any-glob-to-any-file: '**/*.swsl'
"Audio":
- changed-files:
- any-glob-to-any-file: '**/*.ogg'
"C#":
- changed-files:
- any-glob-to-any-file: '**/*.cs'
"YML":
- changed-files:
- any-glob-to-any-file: '**/*.yml'
"No C#":
"Changes: No C#":
- changed-files:
# Equiv to any-glob-to-all as long as this has one matcher. If ALL changed files are not C# files, then apply label.
- all-globs-to-all-files: "!**/*.cs"

View File

@@ -1,4 +1,4 @@
name: Edge Publish
name: Publish
concurrency:
group: publish
@@ -10,15 +10,6 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Python dependencies
run: sudo apt-get install -y python3-paramiko python3-lxml
- name: Send POST-request
uses: appleboy/ssh-action@master
with:
@@ -27,10 +18,3 @@ jobs:
password: ${{ secrets.BUILD_PASS }}
port: 22
script: sh update.sh &> /dev/null
- 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 }}

View File

@@ -11,7 +11,7 @@ jobs:
name: Run Benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
with:
submodules: 'recursive'
- name: Get Engine version

View File

@@ -8,7 +8,7 @@ jobs:
docfx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
- name: Setup submodule
run: |
git submodule update --init --recursive
@@ -19,15 +19,15 @@ jobs:
cd RobustToolbox/
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build --no-restore
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
- name: Build DocFX
uses: nikeee/docfx-action@v1.0.0

View File

@@ -2,11 +2,11 @@
on:
push:
branches: [ master, staging, stable ]
branches: [ master, staging, trying ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
branches: [ master ]
jobs:
build:
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
uses: actions/checkout@v3.6.0
- name: Setup Submodule
run: |
@@ -34,15 +34,15 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build Content.MapRenderer --configuration Release --no-restore /m
run: dotnet build Content.MapRenderer --configuration Release --no-restore /p:WarningsAsErrors=nullable /m
- name: Run Map Renderer
run: dotnet run --project Content.MapRenderer Dev

View File

@@ -2,11 +2,11 @@ name: Build & Test Debug
on:
push:
branches: [ master, staging, stable ]
branches: [ master, staging, trying ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
branches: [ master ]
jobs:
build:
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
uses: actions/checkout@v3.6.0
- name: Setup Submodule
run: |
@@ -34,15 +34,15 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build --configuration DebugOpt --no-restore /m
run: dotnet build --configuration DebugOpt --no-restore /p:WarningsAsErrors=nullable /m
- name: Run Content.Tests
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0

View File

@@ -10,6 +10,6 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
- name: Check for CRLF
run: Tools/check_crlf.py

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: superbrothers/close-pull-request@v3
with:
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."
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."
# If you prefer to just comment on the pr and not close it, uncomment the bellow and comment the above

View File

@@ -2,7 +2,7 @@
on:
pull_request_target:
types: [review_requested, opened]
types: [review_requested]
jobs:
add_label:

View File

@@ -14,7 +14,7 @@ jobs:
{
"0": "XS",
"10": "S",
"100": "M",
"1000": "L",
"5000": "XL"
"30": "M",
"100": "L",
"1000": "XL"
}

View File

@@ -1,46 +0,0 @@
name: Publish Testing
concurrency:
group: publish-testing
cancel-in-progress: true
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
with:
submodules: 'recursive'
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
- name: Get Engine Tag
run: |
cd RobustToolbox
git fetch --depth=1
- name: Install dependencies
run: dotnet restore
- name: Build Packaging
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- name: Publish version
run: Tools/publish_multi_request.py --fork-id wizards-testing
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}

View File

@@ -2,7 +2,6 @@ name: Publish
concurrency:
group: publish
cancel-in-progress: true
on:
workflow_dispatch:
@@ -14,20 +13,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fail if we are attempting to run on the master branch
if: ${{GITHUB.REF_NAME == 'master' && github.repository == 'space-wizards/space-station-14'}}
run: exit 1
- name: Install dependencies
run: sudo apt-get install -y python3-paramiko python3-lxml
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
with:
submodules: 'recursive'
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 8.0.x
- name: Get Engine Tag
run: |
@@ -41,7 +36,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
@@ -53,14 +48,12 @@ 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

@@ -11,14 +11,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
uses: actions/checkout@v3.6.0
- name: Get changed files
id: files
uses: Ana06/get-changed-files@v2.3.0
with:
format: 'space-delimited'
filter: |
filter: |
**.rsi
**.png

View File

@@ -2,7 +2,7 @@
on:
push:
branches: [ master, staging, stable ]
branches: [ master, staging, trying ]
paths:
- '**.cs'
- '**.csproj'
@@ -16,7 +16,7 @@ on:
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
uses: actions/checkout@v3.6.0
- name: Setup Submodule
run: |
@@ -49,9 +49,9 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
@@ -60,7 +60,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release

View File

@@ -4,19 +4,19 @@ on:
workflow_dispatch:
schedule:
- cron: 0 0 * * 0
jobs:
get_credits:
runs-on: ubuntu-latest
# Hey there fork dev! If you like to include your own contributors in this then you can probably just change this to your own repo
# Do this in dump_github_contributors.ps1 too into your own repo
if: github.repository == 'space-wizards/space-station-14'
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
with:
ref: master
- name: Get this week's Contributors
shell: pwsh
env:
@@ -25,25 +25,25 @@ jobs:
# TODO
#- name: Get this week's Patreons
# run: Tools/script2dumppatreons > Resources/Credits/Patrons.yml
# run: Tools/script2dumppatreons > Resources/Credits/Patrons.yml
# MAKE SURE YOU ENABLED "Allow GitHub Actions to create and approve pull requests" IN YOUR ACTIONS, OTHERWISE IT WILL MOST LIKELY FAIL
# For this you can use a pat token of an account with direct push access to the repo if you have protected branches.
# For this you can use a pat token of an account with direct push access to the repo if you have protected branches.
# Uncomment this and comment the other line if you do this.
# https://github.com/stefanzweifel/git-auto-commit-action#push-to-protected-branches
#- name: Commit new credit files
# uses: stefanzweifel/git-auto-commit-action@v4
# with:
# commit_message: Update Credits
# commit_author: PJBot <pieterjan.briers+bot@gmail.com>
# This will make a PR
- name: Set current date as env variable
run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:

View File

@@ -1,7 +1,7 @@
name: RGA schema validator
on:
push:
branches: [ master, staging, stable ]
branches: [ master, staging, trying ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
@@ -12,7 +12,7 @@ jobs:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
- name: Setup Submodule
run: git submodule update --init
- name: Pull engine updates

View File

@@ -2,7 +2,7 @@ name: RSI Validator
on:
push:
branches: [ master, staging, stable ]
branches: [ staging, trying ]
merge_group:
pull_request:
paths:
@@ -13,7 +13,7 @@ jobs:
name: Validate RSIs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
- name: Setup Submodule
run: git submodule update --init
- name: Pull engine updates

View File

@@ -1,7 +1,7 @@
name: Map file schema validator
on:
push:
branches: [ master, staging, stable ]
branches: [ master, staging, trying ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
@@ -12,7 +12,7 @@ jobs:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
- name: Setup Submodule
run: git submodule update --init
- name: Pull engine updates

View File

@@ -2,7 +2,7 @@ name: YAML Linter
on:
push:
branches: [ master, staging, stable ]
branches: [ master, staging, trying ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
@@ -13,7 +13,7 @@ jobs:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v3.6.0
- name: Setup submodule
run: |
git submodule update --init --recursive
@@ -24,9 +24,9 @@ jobs:
cd RobustToolbox/
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 8.0.x
- name: Install dependencies
run: dotnet restore
- name: Build

44
.vscode/tasks.json vendored
View File

@@ -32,50 +32,6 @@
"/consoleloggerparameters:'ForceNoAlign;NoSummary'"
],
"problemMatcher": "$msCompile"
},
{
"label": "test",
"command": "dotnet",
"type": "shell",
"args": [
"test",
"--no-build",
"--configuration",
"DebugOpt",
"Content.Tests/Content.Tests.csproj",
"--",
"NUnit.ConsoleOut=0"
],
"group": {
"kind": "test"
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "integration-test",
"command": "dotnet",
"type": "shell",
"args": [
"test",
"--no-build",
"--configuration",
"DebugOpt",
"Content.IntegrationTests/Content.IntegrationTests.csproj",
"--",
"NUnit.ConsoleOut=0",
"NUnit.MapWarningTo=Failed.ConsoleOut=0",
"NUnit.MapWarningTo=Failed"
],
"group": {
"kind": "test"
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -12,12 +12,12 @@ You want to handle the Build, Clean and Rebuild tasks to prevent missing task er
If you want to learn more about these kinds of things, check out Microsoft's official documentation about MSBuild:
https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild
-->
<Project Sdk="Microsoft.NET.Sdk">
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Python>python3</Python>
<Python Condition="'$(OS)'=='Windows_NT' Or '$(OS)'=='Windows'">py -3</Python>
<ProjectGuid>{C899FCA4-7037-4E49-ABC2-44DE72487110}</ProjectGuid>
<TargetFramework>net4.7.2</TargetFramework>
<TargetFrameworkMoniker>.NETFramework, Version=v4.7.2</TargetFrameworkMoniker>
<RestorePackages>false</RestorePackages>
</PropertyGroup>
<PropertyGroup>

View File

@@ -1,19 +1,16 @@
#!/usr/bin/env python3
"""
Installs git hooks, updates them, updates submodules, that kind of thing.
"""
# Installs git hooks, updates them, updates submodules, that kind of thing.
import os
import shutil
import subprocess
import sys
import time
import os
import shutil
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "3"
CURRENT_HOOKS_VERSION = "2"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
@@ -27,10 +24,12 @@ def run_command(command: List[str], capture: bool = False) -> subprocess.Complet
sys.stdout.flush()
completed = None
if capture:
completed = subprocess.run(command, stdout=subprocess.PIPE, text=True)
completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE)
else:
completed = subprocess.run(command)
completed = subprocess.run(command, cwd="..")
if completed.returncode != 0:
print("Error: command exited with code {}!".format(completed.returncode))
@@ -43,7 +42,7 @@ def update_submodules():
Updates all submodules.
"""
if 'GITHUB_ACTIONS' in os.environ:
if ('GITHUB_ACTIONS' in os.environ):
return
if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"):
@@ -76,21 +75,22 @@ def install_hooks():
print("No hooks change detected.")
return
with open("INSTALLED_HOOKS_VERSION", "w") as f:
f.write(CURRENT_HOOKS_VERSION)
print("Hooks need updating.")
hooks_target_dir = Path(run_command(["git", "rev-parse", "--git-path", "hooks"], True).stdout.strip())
hooks_target_dir = Path("..")/".git"/"hooks"
hooks_source_dir = Path("hooks")
# Clear entire tree since we need to kill deleted files too.
for filename in os.listdir(hooks_target_dir):
os.remove(hooks_target_dir / filename)
for filename in os.listdir(str(hooks_target_dir)):
os.remove(str(hooks_target_dir/filename))
for filename in os.listdir(hooks_source_dir):
for filename in os.listdir(str(hooks_source_dir)):
print("Copying hook {}".format(filename))
shutil.copy2(hooks_source_dir / filename, hooks_target_dir / filename)
with open("INSTALLED_HOOKS_VERSION", "w") as f:
f.write(CURRENT_HOOKS_VERSION)
shutil.copy2(str(hooks_source_dir/filename),
str(hooks_target_dir/filename))
def reset_solution():
@@ -104,20 +104,7 @@ def reset_solution():
with SOLUTION_PATH.open("w") as f:
f.write(content)
def check_for_zip_download():
# Check if .git exists,
if run_command(["git", "rev-parse"]).returncode != 0:
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

@@ -1,10 +1,10 @@
#!/bin/bash
gitroot=$(git rev-parse --show-toplevel)
gitroot=`git rev-parse --show-toplevel`
cd "$gitroot/BuildChecker" || exit
cd "$gitroot/BuildChecker"
if [[ $(uname) == MINGW* || $(uname) == CYGWIN* ]]; then
if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
# Windows
py -3 git_helper.py --quiet
else

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# Just call post-checkout since it does the same thing.
gitroot=$(git rev-parse --git-path hooks)
bash "$gitroot/post-checkout"
gitroot=`git rev-parse --show-toplevel`
bash "$gitroot/.git/hooks/post-checkout"

80
CLA.md
View File

@@ -1,80 +0,0 @@
# CrystallEdge Contributor License Agreement
### Version 1.0
##### Thank you for your interest in contributing to CrystallEdge ("We" or "Us").
##### The purpose of this contributor agreement ("Agreement") is for Your protection as a Contributor in addition to the protection of our community.
##### If you wish to contact us regarding licensing matters we can be reached at crystalledge14@gmail.com
## How to use this Contributor Agreement
##### If You are an employee and have created the Contribution as part of your employment, You need to have Your employer approve this Agreement or sign the Entity version of this document as well.
## 1. Definitions
- _**"You"**_ means the individual Copyright owner who Submits a Contribution to Us.
- _**"Contribution(s)"**_ means any work(s) of authorship, including any original modifications or additions to an existing work of authorships, Submitted by You to Us, where You are the author, holder of copyright, or Licensee under an Approved License specified by Us.
- _**"Copyright"**_ means all rights protecting works of authorship, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence.
- **_"Material"_** means the software or documentation made available by Us to third parties. When this Agreement covers more than one software project, the Material means the software or documentation to which the Contributions were Submitted. After You Submit the Contributions, theymay be included in the Material.
- **_"Submit"_** means any act by which Contributions are transferred to Us by You by means of tangible or intangible media, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us, but excluding any transfer that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
- **_"Documentation"_** means any non-software portion of Contributions.
- **_"Approved License"_** means any License specified by CrystallEdge to be Approved or “Compatible” with the Project the Contributions are Submitted to.
## 2. Representations
### 2.1 Representation of legal age
##### You represent that you are older than 16 years of age, and if required by law, have authorization by a legal guardian to enter this agreement.
### 2.2 Assurance of legal rights
##### You represent and assure that You have sufficient rights to your Contribution and are legally entitled to enter this Agreement and grant the licenses specified below or an Approved license if Your Contribution or portions thereof are provided to You under one of the Approved Licenses.
### 2.3 Third Party Contributions
##### If You act on behalf of Your employer or other third party You represent that You are authorized and have the right to Submit the Contribution on behalf of Your employer or the mentioned third party.
### 2.4 Compliance and Non-infringement
##### You represent and warrant that each of your Contributions:
- Is and will remain an original work of authorship;
- to the best of Your knowledge, does not and will not infringe any third partys copyright, trademark, patent, or other intellectual property rights;
- In part or in whole, is licensed under one of the Approved Licenses or is an original work you have the rights to.
- includes the complete and correct details of any license, third-party license, patent, trademark, necessary attributions or other restriction associated with all or any part of Your Contribution in a conspicuous location;
- complies and will continue to comply with all applicable laws, including export control laws and regulations;
### 2.5 Mixed license Contributions
##### Subject to the terms and conditions of this agreement, specifically section 2, if there is a conflict between the grants in section 3, 4 and a Contribution under an Approved License, the terms of the Approved License supersede.
## 2.6 Employee or Representative Submissions
##### If You Submit as a company, You agree that a) Your employees, contractors, and representatives may Submit Contributions on Your behalf; and b) the individual signing this Agreement on Your behalf has the necessary authority including the authority to bind You to the Agreement.
## 3. License grant
### 3.1 Copyright license to Us
##### Subject to the terms and conditions of this Agreement, You hereby grant to Us a worldwide, royalty-free, NON-exclusive, perpetual and irrevocable (except as stated in Sections 2.4 and 8.2) license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, under the Copyright covering the Contributions to use the Contributions by all means, including, but not limited to:
- publish the Contributions,
- modify the Contributions,
- prepare derivative works based upon or containing the Contributions and/or to combine the Contributions with other Materials,
- reproduce the Contributions in original or modified form,
- distribute, to make the Contributions available to the public, display and publicly perform the Contributions in original or modified form.
## 3.2 Moral rights
##### Moral Rights remain unaffected to the extent they are recognized and not waivable by applicable law. Notwithstanding, You may add your name to the attribution mechanism customary used in the Materials you Contribute to, such as the header of the source code files of Your Contributions, and We will respect this attribution when using Your Contributions.
## 4. Patents
### 4.1 Patent license
##### Subject to the terms and conditions of this Agreement You hereby grant to Us and to recipients of Materials distributed by Us a worldwide, royalty-free, non-exclusive, perpetual and irrevocable (except as stated in Section 3.2) patent license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contributions and the Contributions in combination with any Material (and portions of such combination). This license applies to all patents owned or controlled by You, whether already acquired or hereafter acquired, that would be infringed by making, having made, using, selling, offering for sale, importing or otherwise transferring of Your Contribution(s) alone or by combination of Your Contribution(s) with any Material.
### 4.2 Revocation of patent license
##### You reserve the right to revoke the patent license stated in section 3.1 if We make any infringement claim that is targeted at your Contribution(s) and not asserted for a Defensive Purpose. An assertion of claims of the Patents shall be considered for a "Defensive Purpose" if the claims are asserted against an entity that has filed, maintained, threatened, or voluntarily participated in a patent infringement lawsuit against Us or any of Our licensees.
## 5. Disclaimer
#### CONTRIBUTIONS ARE PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US AND BY US TO YOU. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION AND EXTENT TO THE MINIMUM PERIOD AND EXTENT PERMITTED BY LAW.
## 6. Consequential damage waiver
#### TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.
## 7. Approximation of disclaimer and damage waiver
#### IF THE DISCLAIMER AND DAMAGE WAIVER MENTIONED IN SECTION 4. AND SECTION 5. CANNOT BE GIVEN LEGAL EFFECT UNDER APPLICABLE LOCAL LAW, REVIEWING COURTS SHALL APPLY LOCAL LAW THAT MOST CLOSELY APPROXIMATES AN ABSOLUTE WAIVER OF ALL CIVIL OR CONTRACTUAL LIABILITY IN CONNECTION WITH THE CONTRIBUTION.
## 8. Term
##### 8.1 This Agreement shall come into effect upon Your acceptance of the terms and conditions, either by replying to the CLA Bot or by sending a signed copy to crystalledge14@gmail.com with the subject: "\<your name\> CLA"
##### 8.2 In the event of a termination of this Agreement Sections 4, 5, 6, 7 and 8 shall survive such termination and shall remain in full force thereafter. For the avoidance of doubt, Approved (sub)licenses that have already been granted for Contributions at the date of the termination shall remain in full force after the termination of this Agreement.
## 9. Miscellaneous
##### 9.1 This Agreement and all disputes, claims, actions, suits or other proceedings arising out of this agreement or relating in any way to it shall be governed by the laws of Russia excluding its private international law provisions.
##### 9.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings.
##### 9.3 In case of Your death, this agreement shall continue with Your heirs. In case of more than one heir, all heirs must exercise their rights through a commonly authorized person.
##### 9.4 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and that is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
##### 9.5 You agree to notify Us of any facts or circumstances of which you become aware that would make this Agreement inaccurate in any respect.
##### 9.6 Any Substantive modifications to this Agreement will result in a new version being created, to continue Contributing you must agree to the latest version of the Agreement, which supersedes any previous versions.
##### 9.7 CrystallEdge will provide notification of any new version of this agreement being created, if you do not agree to the new version of the Agreement the previous Agreement remains binding.

View File

@@ -1,40 +0,0 @@
# Space Station 14 Code of Conduct
Space Station 14's staff and community is made up volunteers from all over the world, working on every aspect of the project - including development, teaching, and hosting integral tools.
Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to all levels of the project, from commenters to contributors to staff.
This isnt an exhaustive list of things that you cant do. Rather, take it in the spirit in which its intended - a guide to make it easier to enrich all of us and the technical communities in which we participate.
This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here.
If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com).
- **Be friendly and patient.**
- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and contributors, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. We have contributors of all skill levels, some even making their first foray into a new field with this project, so keep that in mind when discussing someone's work.
- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. Its important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Space Station 14 community should be respectful when dealing with other members as well as with people outside the Space Station 14 community. Assume contributions to the project, even those that do not end up being included, are made in good faith.
- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
- Violent threats or language directed against another person.
- Discriminatory jokes and language.
- Posting sexually explicit or violent material.
- Posting (or threatening to post) other people's personally identifying information ("doxing").
- Personal insults, especially those using racist or sexist terms.
- Unwelcome sexual attention.
- Advocating for, or encouraging, any of the above behavior.
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Space Station 14 is no exception. It is important that we resolve disagreements and differing views constructively. Remember that were different. The strength of Space Station 14 comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to make mistakes and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
## On Community Moderation
Deviating from the Code of Conduct on the Github repository may result in moderative actions taken by project Maintainers. This can involve your content being edited or deleted, and may result in a temporary or permanent block from the repository.
This is to ensure Space Station 14 is a healthy community in which contributors feel encouraged and empowered to contribute, and to give you as a member of this community a chance to reflect on how you are interacting with it. While outright offensive and bigoted content will *always* be unacceptable on the repository, Maintainers are at liberty to take moderative actions against more ambiguous content that fail to provide constructive criticism, or that provides constructive criticism in a non-constructive manner. Examples of this include using hyperbole, bringing up PRs/changes unrelated to the discussion at hand, hostile tone, off-topic comments, creating PRs/Issues for the sole purpose of causing discussions, skirting the line of acceptable behavior, etc. Disagreeing with content or each other is fine and appreciated, but only as long as it's done with respect and in a constructive manner.
Maintainers are expected to adhere to the guidelines as listed in the [Github Moderation Guidelines](https://docs.spacestation14.com/en/general-development/github-moderation-guidelines.html), though may deviate should they feel it's in the best interest of the community. If you believe you had an action incorrectly applied against you, you are encouraged to contact staff via [Discord](https://discord.ss14.io/) or [the forums](https://forum.spacestation14.com/), [appeal your Github ban](https://forum.spacestation14.com/c/ban-appeals/appeals-github/38), or make a [staff complaint](https://forum.spacestation14.com/t/staff-complaint-instructions-and-info/31).
## Attribution
This Code of Conduct is an edited version of the [Django Code of Conduct](https://www.djangoproject.com/conduct/), licensed under CC BY 3.0, for the Space Station 14 Github repository.

View File

@@ -9,14 +9,13 @@ using Content.IntegrationTests.Pair;
using Content.Shared.Clothing.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Item;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Benchmarks;
@@ -29,10 +28,11 @@ namespace Content.Benchmarks;
[CategoriesColumn]
public class ComponentQueryBenchmark
{
public const string Map = "Maps/saltern.yml";
public const string Map = "Maps/atlas.yml";
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private MapId _mapId = new(10);
private EntityQuery<ItemComponent> _itemQuery;
private EntityQuery<ClothingComponent> _clothingQuery;
private EntityQuery<MapComponent> _mapQuery;
@@ -54,10 +54,10 @@ public class ComponentQueryBenchmark
_pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
_pair.Server.WaitPost(() =>
{
var map = new ResPath(Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
if (!_entMan.System<MapLoaderSystem>().TryLoadMap(map, out _, out _, opts))
var success = _entMan.System<MapLoaderSystem>().TryLoad(_mapId, Map, out _);
if (!success)
throw new Exception("Map load failed");
_pair.Server.MapMan.DoMapInitialize(_mapId);
}).GetAwaiter().GetResult();
_items = new EntityUid[_entMan.Count<ItemComponent>()];

View File

@@ -1,174 +0,0 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Benchmarks;
/// <summary>
/// Spawns N number of entities with a <see cref="DeltaPressureComponent"/> and
/// simulates them for a number of ticks M.
/// </summary>
[Virtual]
[GcServer(true)]
//[MemoryDiagnoser]
//[ThreadingDiagnoser]
public class DeltaPressureBenchmark
{
/// <summary>
/// Number of entities (windows, really) to spawn with a <see cref="DeltaPressureComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
public int EntityCount;
/// <summary>
/// Number of entities that each parallel processing job will handle.
/// </summary>
// [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN)
[Params(10)]
public int BatchSize;
/// <summary>
/// Number of entities to process per iteration in the DeltaPressure
/// processing loop.
/// </summary>
// [Params(100, 1000, 5000, 10000, 50000)]
[Params(1000)]
public int EntitiesPerIteration;
private readonly EntProtoId _windowProtoId = "Window";
private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible";
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private SharedMapSystem _map = default!;
private IRobustRandom _random = default!;
private IConfigurationManager _cvar = default!;
private ITileDefinitionManager _tileDefMan = default!;
private AtmosphereSystem _atmospereSystem = default!;
private Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>
_testEnt;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
_entMan = server.ResolveDependency<IEntityManager>();
_map = _entMan.System<SharedMapSystem>();
_random = server.ResolveDependency<IRobustRandom>();
_cvar = server.ResolveDependency<IConfigurationManager>();
_tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
_atmospereSystem = _entMan.System<AtmosphereSystem>();
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
_cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration);
_cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize);
var plating = _tileDefMan["Plating"].TileId;
/*
Basically, we want to have a 5-wide grid of tiles.
Edges are walled, and the length of the grid is determined by N + 2.
Windows should only touch the top and bottom walls, and each other.
*/
var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls
const int height = 5;
await server.WaitPost(() =>
{
// Fill required tiles (extend grid) with plating
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
_map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
}
}
// Spawn perimeter walls and windows row in the middle (y = 2)
const int midY = height / 2;
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1;
if (isPerimeter)
{
_entMan.SpawnEntity(_wallProtoId, coords);
continue;
}
// Spawn windows only on the middle row, spanning interior (excluding side walls)
if (y == midY)
{
_entMan.SpawnEntity(_windowProtoId, coords);
}
}
}
});
// Next we run the fixgridatmos command to ensure that we have some air on our grid.
// Wait a little bit as well.
// TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just
// a stamp-on in AtmosphereSystem.
await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1);
var uid = mapdata.Grid.Owner;
_testEnt = new Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(
uid,
_entMan.GetComponent<GridAtmosphereComponent>(uid),
_entMan.GetComponent<GasTileOverlayComponent>(uid),
_entMan.GetComponent<MapGridComponent>(uid),
_entMan.GetComponent<TransformComponent>(uid));
}
[Benchmark]
public async Task PerformFullProcess()
{
await _pair.Server.WaitPost(() =>
{
while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { }
});
}
[Benchmark]
public async Task PerformSingleRunProcess()
{
await _pair.Server.WaitPost(() =>
{
_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure);
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -44,7 +44,7 @@ namespace Content.Benchmarks
for (var i = 0; i < Aabbs1.Length; i++)
{
var aabb = Aabbs1[i];
_b2Tree.CreateProxy(aabb, uint.MaxValue, i);
_b2Tree.CreateProxy(aabb, i);
_tree.Add(i);
}
}

View File

@@ -6,13 +6,12 @@ using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Maps;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Benchmarks;
@@ -21,7 +20,7 @@ public class MapLoadBenchmark
{
private TestPair _pair = default!;
private MapLoaderSystem _mapLoader = default!;
private SharedMapSystem _mapSys = default!;
private IMapManager _mapManager = default!;
[GlobalSetup]
public void Setup()
@@ -37,7 +36,7 @@ public class MapLoadBenchmark
.ToDictionary(x => x.ID, x => x.MapPath.ToString());
_mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
_mapSys = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedMapSystem>();
_mapManager = server.ResolveDependency<IMapManager>();
}
[GlobalCleanup]
@@ -47,25 +46,23 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
public static string[] MapsSource { get; } = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog" };
[ParamsSource(nameof(MapsSource))]
public string Map;
public Dictionary<string, string> Paths;
private MapId _mapId;
[Benchmark]
public async Task LoadMap()
{
var mapPath = new ResPath(Paths[Map]);
var mapPath = Paths[Map];
var server = _pair.Server;
await server.WaitPost(() =>
{
var success = _mapLoader.TryLoadMap(mapPath, out var map, out _);
var success = _mapLoader.TryLoad(new MapId(10), mapPath, out _);
if (!success)
throw new Exception("Map load failed");
_mapId = map.Value.Comp.MapId;
});
}
@@ -73,7 +70,9 @@ public class MapLoadBenchmark
public void IterationCleanup()
{
var server = _pair.Server;
server.WaitPost(() => _mapSys.DeleteMap(_mapId))
.Wait();
server.WaitPost(() =>
{
_mapManager.DeleteMap(new MapId(10));
}).Wait();
}
}

View File

@@ -1,7 +1,15 @@
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
{
@@ -14,15 +22,11 @@ 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");
var baseConfig = new DebugInProcessConfig();
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
#else
var baseConfig = Environment.GetEnvironmentVariable("ROBUST_BENCHMARKS_ENABLE_SQL") != null
? DefaultSQLConfig.Instance
: DefaultConfig.Instance;
#endif
var config = ManualConfig.Create(baseConfig);
config.BuildTimeout = TimeSpan.FromMinutes(5);
var config = Environment.GetEnvironmentVariable("ROBUST_BENCHMARKS_ENABLE_SQL") != null ? DefaultSQLConfig.Instance : null;
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
#endif
}
}
}

View File

@@ -6,16 +6,14 @@ using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Shared.Warps;
using Content.Server.Warps;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Benchmarks;
@@ -36,6 +34,7 @@ public class PvsBenchmark
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private MapId _mapId = new(10);
private ICommonSession[] _players = default!;
private EntityCoordinates[] _spawns = default!;
public int _cycleOffset = 0;
@@ -66,10 +65,10 @@ public class PvsBenchmark
_pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
await _pair.Server.WaitPost(() =>
{
var path = new ResPath(Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
if (!_entMan.System<MapLoaderSystem>().TryLoadMap(path, out _, out _, opts))
var success = _entMan.System<MapLoaderSystem>().TryLoad(_mapId, Map, out _);
if (!success)
throw new Exception("Map load failed");
_pair.Server.MapMan.DoMapInitialize(_mapId);
});
// Get list of ghost warp positions

View File

@@ -1,126 +0,0 @@
#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

@@ -8,7 +8,6 @@ using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Benchmarks;
@@ -19,11 +18,9 @@ namespace Content.Benchmarks;
[Virtual, MemoryDiagnoser]
public class SpawnEquipDeleteBenchmark
{
private static readonly EntProtoId Mob = "MobHuman";
private static readonly ProtoId<StartingGearPrototype> CaptainStartingGear = "CaptainGear";
private TestPair _pair = default!;
private StationSpawningSystem _spawnSys = default!;
private const string Mob = "MobHuman";
private StartingGearPrototype _gear = default!;
private EntityUid _entity;
private EntityCoordinates _coords;
@@ -42,7 +39,7 @@ public class SpawnEquipDeleteBenchmark
var mapData = await _pair.CreateTestMap();
_coords = mapData.GridCoords;
_spawnSys = server.System<StationSpawningSystem>();
_gear = server.ProtoMan.Index(CaptainStartingGear);
_gear = server.ProtoMan.Index<StartingGearPrototype>("CaptainGear");
}
[GlobalCleanup]

View File

@@ -4,20 +4,39 @@ using Robust.Shared.Console;
namespace Content.Client.Access.Commands;
public sealed class ShowAccessReadersCommand : LocalizedEntityCommands
public sealed class ShowAccessReadersCommand : IConsoleCommand
{
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
public string Command => "showaccessreaders";
public override string Command => "showaccessreaders";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
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)
{
var existing = _overlay.RemoveOverlay<AccessOverlay>();
if (!existing)
_overlay.AddOverlay(new AccessOverlay(EntityManager, _cache, _xform));
var collection = IoCManager.Instance;
shell.WriteLine(Loc.GetString($"cmd-showaccessreaders-status", ("status", !existing)));
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");
}
}

View File

@@ -29,7 +29,7 @@ namespace Content.Client.Access.UI
foreach (var access in accessLevels)
{
if (!protoManager.Resolve(access, out var accessLevel))
if (!protoManager.TryIndex(access, out var accessLevel))
{
continue;
}
@@ -88,9 +88,8 @@ namespace Content.Client.Access.UI
button.Disabled = !interfaceEnabled;
if (interfaceEnabled)
{
// Explicit cast because Rider gives a false error otherwise.
button.Pressed = state.TargetAccessReaderIdAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName) ?? false;
button.Disabled = (!state.AllowedModifyAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName)) ?? true;
button.Pressed = state.TargetAccessReaderIdAccessList?.Contains(accessName) ?? false;
button.Disabled = (!state.AllowedModifyAccessList?.Contains(accessName)) ?? true;
}
}
}

View File

@@ -1,26 +0,0 @@
<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

@@ -1,451 +0,0 @@
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
{
private static readonly ProtoId<AccessGroupPrototype> GeneralAccessGroup = "General";
[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.Resolve(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.Resolve(GeneralAccessGroup, out var generalAccessProto))
_groupedAccessLevels.TryAdd(generalAccessProto, new());
// Assign known access levels with their associated groups
foreach (var accessLevel in _accessLevels)
{
if (!_protoManager.Resolve(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,11 +1,8 @@
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 Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
@@ -14,21 +11,13 @@ 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()
@@ -75,13 +64,13 @@ namespace Content.Client.Access.UI
_window?.UpdateState(castState);
}
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, ProtoId<JobPrototype> newJobPrototype)
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, string newJobPrototype)
{
if (newFullName.Length > _maxNameLength)
newFullName = newFullName[.._maxNameLength];
if (newFullName.Length > MaxFullNameLength)
newFullName = newFullName[..MaxFullNameLength];
if (newJobTitle.Length > _maxIdJobLength)
newJobTitle = newJobTitle[.._maxIdJobLength];
if (newJobTitle.Length > MaxJobTitleLength)
newJobTitle = newJobTitle[..MaxJobTitleLength];
SendMessage(new WriteToTargetIdMessage(
newFullName,

View File

@@ -1,13 +1,11 @@
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;
@@ -16,17 +14,12 @@ 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();
@@ -46,11 +39,7 @@ 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;
@@ -58,7 +47,6 @@ namespace Content.Client.Access.UI
FullNameSaveButton.OnPressed += _ => SubmitData();
JobTitleLineEdit.OnTextEntered += _ => SubmitData();
JobTitleLineEdit.IsValid = s => s.Length <= _maxIdJobLength;
JobTitleLineEdit.OnTextChanged += _ =>
{
JobTitleSaveButton.Disabled = JobTitleLineEdit.Text == _lastJobTitle;
@@ -123,7 +111,7 @@ namespace Content.Client.Access.UI
foreach (var group in job.AccessGroups)
{
if (!_prototypeManager.Resolve(group, out AccessGroupPrototype? groupPrototype))
if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}
@@ -186,8 +174,7 @@ 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
// or the IdCardComponent's JobPrototype field.
// If the job index is < 0 that means they don't have a job registered in the station records.
// For example, a new ID from a box would have no job index.
if (jobIndex < 0)
{

View File

@@ -1,6 +1,3 @@
using Content.Shared.Actions.Components;
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
namespace Content.Client.Actions;
/// <summary>
@@ -10,17 +7,3 @@ 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,23 +1,17 @@
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;
@@ -28,10 +22,9 @@ 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;
@@ -43,67 +36,154 @@ namespace Content.Client.Actions
public event Action<List<SlotAssignment>>? AssignSlot;
private readonly List<EntityUid> _removed = new();
private readonly List<Entity<ActionComponent>> _added = new();
public static readonly EntProtoId MappingEntityAction = "BaseMappingEntityAction";
private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActionsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ActionsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
SubscribeLocalEvent<ActionComponent, AfterAutoHandleStateEvent>(OnActionAutoHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ActionTargetAttemptEvent>(OnEntityTargetAttempt);
SubscribeLocalEvent<WorldTargetActionComponent, ActionTargetAttemptEvent>(OnWorldTargetAttempt);
SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
}
private void OnActionAutoHandleState(Entity<ActionComponent> ent, ref AfterAutoHandleStateEvent args)
public override void FrameUpdate(float frameTime)
{
UpdateAction(ent);
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);
}
}
public override void UpdateAction(Entity<ActionComponent> ent)
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
{
// 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)
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.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.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)
return;
ActionsUpdated?.Invoke();
}
private void OnHandleState(Entity<ActionsComponent> ent, ref ComponentHandleState args)
private void HandleComponentState(EntityUid uid, ActionsComponent component, 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 comp.Actions)
foreach (var act in component.Actions)
{
if (!stateEnts.Contains(act) && !IsClientSide(act))
_removed.Add(act);
}
comp.Actions.ExceptWith(_removed);
component.Actions.ExceptWith(_removed);
foreach (var actionId in stateEnts)
{
if (!actionId.IsValid())
continue;
if (!comp.Actions.Add(actionId))
if (!component.Actions.Add(actionId))
continue;
if (GetAction(actionId) is {} action)
_added.Add(action);
TryGetActionData(actionId, out var action);
_added.Add((actionId, action));
}
if (_playerManager.LocalEntity != uid)
@@ -118,46 +198,45 @@ namespace Content.Client.Actions
foreach (var action in _added)
{
OnActionAdded?.Invoke(action);
OnActionAdded?.Invoke(action.Item1);
}
ActionsUpdated?.Invoke();
}
public static int ActionComparer(Entity<ActionComponent> a, Entity<ActionComponent> b)
public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
{
var priorityA = a.Comp?.Priority ?? 0;
var priorityB = b.Comp?.Priority ?? 0;
var priorityA = a.Item2?.Priority ?? 0;
var priorityB = b.Item2?.Priority ?? 0;
if (priorityA != priorityB)
return priorityA - priorityB;
priorityA = a.Comp?.Container?.Id ?? 0;
priorityB = b.Comp?.Container?.Id ?? 0;
priorityA = a.Item2?.Container?.Id ?? 0;
priorityB = b.Item2?.Container?.Id ?? 0;
return priorityA - priorityB;
}
protected override void ActionAdded(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
BaseActionComponent action)
{
if (_playerManager.LocalEntity != performer.Owner)
if (_playerManager.LocalEntity != performer)
return;
OnActionAdded?.Invoke(action);
ActionsUpdated?.Invoke();
OnActionAdded?.Invoke(actionId);
}
protected override void ActionRemoved(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
{
if (_playerManager.LocalEntity != performer.Owner)
if (_playerManager.LocalEntity != performer)
return;
OnActionRemoved?.Invoke(action);
ActionsUpdated?.Invoke();
OnActionRemoved?.Invoke(actionId);
}
public IEnumerable<Entity<ActionComponent>> GetClientActions()
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
{
if (_playerManager.LocalEntity is not { } user)
return Enumerable.Empty<Entity<ActionComponent>>();
return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
return GetActions(user);
}
@@ -194,24 +273,25 @@ namespace Content.Client.Actions
CommandBinds.Unregister<ActionsSystem>();
}
public void TriggerAction(Entity<ActionComponent> action)
public void TriggerAction(EntityUid actionId, BaseActionComponent action)
{
if (_playerManager.LocalEntity is not { } user)
return;
// TODO: unhardcode this somehow
if (!HasComp<InstantActionComponent>(action))
return;
if (action.Comp.ClientExclusive)
if (_playerManager.LocalEntity is not { } user ||
!TryComp(user, out ActionsComponent? actions))
{
PerformAction(user, action);
return;
}
if (action is not InstantActionComponent instantAction)
return;
if (action.ClientExclusive)
{
PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
}
else
{
var request = new RequestPerformActionEvent(GetNetEntity(action));
RaisePredictiveEvent(request);
var request = new RequestPerformActionEvent(GetNetEntity(actionId));
EntityManager.RaisePredictiveEvent(request);
}
}
@@ -234,140 +314,39 @@ namespace Content.Client.Actions
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
return;
var actions = EnsureComp<ActionsComponent>(user);
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 actionId = EntityUid.Invalid;
if (map.TryGet<ValueDataNode>("action", out var actionNode))
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
foreach (var index in nodeAssignments)
{
var id = new EntProtoId(actionNode.Value);
actionId = Spawn(id);
var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
assignments.Add(assignment);
}
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;
}
AddActionDirect((user, actions), actionId);
}
}
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)} requir45es 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;
AssignSlot?.Invoke(assignments);
}
public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);

View File

@@ -21,7 +21,7 @@ namespace Content.Client.Actions.UI
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
@@ -52,6 +52,17 @@ namespace Content.Client.Actions.UI
vbox.AddChild(description);
}
if (charges != null && !string.IsNullOrWhiteSpace(charges.ToString()))
{
var chargesLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipActionCharges }
};
chargesLabel.SetMessage(charges);
vbox.AddChild(chargesLabel);
}
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,

View File

@@ -1,19 +1,11 @@
using System.Collections.Frozen;
using System.Linq;
using System.Numerics;
using Content.Client.Administration.Systems;
using Content.Client.Stylesheets;
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;
using Robust.Shared.Configuration;
using Robust.Shared;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Configuration;
namespace Content.Client.Administration;
@@ -24,73 +16,17 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly SharedRoleSystem _roles;
private readonly IPrototypeManager _prototypeManager;
private readonly Font _font;
private readonly Font _fontBold;
private AdminOverlayAntagFormat _overlayFormat;
private AdminOverlayAntagSymbolStyle _overlaySymbolStyle;
private bool _overlayPlaytime;
private bool _overlayStartingJob;
private float _ghostFadeDistance;
private float _ghostHideDistance;
private int _overlayStackMax;
private float _overlayMergeDistance;
//TODO make this adjustable via GUI?
private static readonly FrozenSet<ProtoId<RoleTypePrototype>> Filter =
new ProtoId<RoleTypePrototype>[] {"SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"}
.ToFrozenSet();
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
public AdminNameOverlay(
AdminSystem system,
IEntityManager entityManager,
IEyeManager eyeManager,
IResourceCache resourceCache,
EntityLookupSystem entityLookup,
IUserInterfaceManager userInterfaceManager,
IConfigurationManager config,
SharedRoleSystem roles,
IPrototypeManager prototypeManager)
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup, IUserInterfaceManager userInterfaceManager)
{
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager;
_roles = roles;
_prototypeManager = prototypeManager;
ZIndex = 200;
// Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack();
_fontBold = resourceCache.NotoStack(variation: "Bold");
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);
config.OnValueChanged(CCVars.AdminOverlayGhostFadeDistance, (f) => { _ghostFadeDistance = f; }, true);
config.OnValueChanged(CCVars.AdminOverlayStackMax, (i) => { _overlayStackMax = i; }, true);
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;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
@@ -98,185 +34,43 @@ internal sealed class AdminNameOverlay : Overlay
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.WorldAABB;
var colorDisconnected = Color.White;
var uiScale = _userInterfaceManager.RootControl.UIScale;
var lineoffset = new Vector2(0f, 14f) * uiScale;
var drawnOverlays = new List<(Vector2,Vector2)>() ; // A saved list of the overlays already drawn
// Get all player positions before drawing overlays, so they can be sorted before iteration
var sortable = new List<(PlayerInfo, Box2, EntityUid, Vector2)>();
foreach (var info in _system.PlayerList)
foreach (var playerInfo in _system.PlayerList)
{
var entity = _entityManager.GetEntity(info.NetEntity);
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
// If entity does not exist or is on a different map, skip
if (entity == null
|| !_entityManager.EntityExists(entity)
|| _entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
// Otherwise the entity can not exist yet
if (entity == null || !_entityManager.EntityExists(entity))
{
continue;
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, skip
// if not on screen, continue
if (!aabb.Intersects(in viewport))
{
continue;
}
// Get on-screen coordinates of player
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center).Rounded();
sortable.Add((info, aabb, entity.Value, screenCoordinates));
var uiScale = _userInterfaceManager.RootControl.UIScale;
var lineoffset = new Vector2(0f, 11f) * uiScale;
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
if (playerInfo.Antag)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", uiScale, Color.OrangeRed);
;
}
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
}
// Draw overlays for visible players, starting from the top of the screen
foreach (var info in sortable.OrderBy(s => s.Item4.Y).ToList())
{
var playerInfo = info.Item1;
var rolePrototype = playerInfo.RoleProto == null
? null
: _prototypeManager.Index(playerInfo.RoleProto.Value);
var roleName = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
var roleColor = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
var roleSymbol = rolePrototype?.Symbol ?? RoleTypePrototype.FallbackSymbol;
var aabb = info.Item2;
var entity = info.Item3;
var screenCoordinatesCenter = info.Item4;
//the center position is kept separately, for simpler position comparison later
var centerOffset = new Vector2(28f, -18f) * uiScale;
var screenCoordinates = screenCoordinatesCenter + centerOffset;
var alpha = 1f;
//TODO make a smarter system where the starting offset can be modified by the predicted position and size of already-drawn overlays/stacks?
var currentOffset = Vector2.Zero;
// Ghosts near the cursor are made transparent/invisible
// TODO would be "cheaper" if playerinfo already contained a ghost bool, this gets called every frame for every onscreen player!
if (_entityManager.HasComponent<GhostComponent>(entity))
{
// We want the map positions here, so we don't have to worry about resolution and such shenanigans
var mobPosition = aabb.Center;
var mousePosition = _eyeManager
.ScreenToMap(_userInterfaceManager.MousePositionScaled.Position * uiScale)
.Position;
var dist = Vector2.Distance(mobPosition, mousePosition);
if (dist < _ghostHideDistance)
continue;
alpha = Math.Clamp((dist - _ghostHideDistance) / (_ghostFadeDistance - _ghostHideDistance), 0f, 1f);
colorDisconnected.A = alpha;
}
// If the new overlay text block is within merge distance of any previous ones
// merge them into a stack so they don't hide each other
var stack = drawnOverlays.FindAll(x =>
Vector2.Distance(_eyeManager.ScreenToMap(x.Item1).Position, aabb.Center) <= _overlayMergeDistance);
if (stack.Count > 0)
{
screenCoordinates = stack.First().Item1 + centerOffset;
// Replacing this overlay's coordinates for the later save with the stack root's coordinates
// so that other overlays don't try to stack to these coordinates
screenCoordinatesCenter = stack.First().Item1;
var i = 1;
foreach (var s in stack)
{
// additional entries after maximum stack size is reached will be drawn over the last entry
if (i <= _overlayStackMax - 1)
currentOffset = lineoffset + s.Item2 ;
i++;
}
}
// Character name
var color = Color.Aquamarine;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
// Username
color = Color.Yellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
// Playtime
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && _overlayPlaytime)
{
color = Color.Orange;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
}
// Job
if (!string.IsNullOrEmpty(playerInfo.StartingJob) && _overlayStartingJob)
{
color = Color.GreenYellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
}
// Determine antag symbol
string? symbol;
switch (_overlaySymbolStyle)
{
case AdminOverlayAntagSymbolStyle.Specific:
symbol = roleSymbol;
break;
case AdminOverlayAntagSymbolStyle.Basic:
symbol = Loc.GetString("player-tab-antag-prefix");
break;
default:
case AdminOverlayAntagSymbolStyle.Off:
symbol = string.Empty;
break;
}
// Determine antag/role type name
string? text;
switch (_overlayFormat)
{
case AdminOverlayAntagFormat.Roletype:
color = roleColor;
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
text = IsFiltered(playerInfo.RoleProto)
? roleName.ToUpper()
: string.Empty;
break;
case AdminOverlayAntagFormat.Subtype:
color = roleColor;
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
text = IsFiltered(playerInfo.RoleProto)
? _roles.GetRoleSubtypeLabel(roleName, 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));
}
}
private static bool IsFiltered(ProtoId<RoleTypePrototype>? roleProtoId)
{
if (roleProtoId == null)
return false;
return Filter.Contains(roleProtoId.Value);
}
}

View File

@@ -0,0 +1,10 @@
using Content.Shared.Administration.Components;
using Robust.Shared.GameStates;
namespace Content.Client.Administration.Components;
[RegisterComponent]
public sealed partial class HeadstandComponent : SharedHeadstandComponent
{
}

View File

@@ -15,7 +15,6 @@ namespace Content.Client.Administration.Managers
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IClientNetManager _netMgr = default!;
[Dependency] private readonly IClientConGroupController _conGroup = default!;
[Dependency] private readonly IClientConsoleHost _host = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
@@ -87,12 +86,12 @@ namespace Content.Client.Administration.Managers
private void UpdateMessageRx(MsgUpdateAdminStatus message)
{
_availableCommands.Clear();
var host = IoCManager.Resolve<IClientConsoleHost>();
// Anything marked as Any we'll just add even if the server doesn't know about it.
foreach (var (command, instance) in _host.AvailableCommands)
foreach (var (command, instance) in host.AvailableCommands)
{
if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null)
continue;
if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) continue;
_availableCommands.Add(command);
}

View File

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

View File

@@ -0,0 +1,7 @@
using Content.Shared.Administration;
namespace Content.Client.Administration.Systems;
public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
{
}

View File

@@ -1,10 +1,8 @@
using Content.Client.Administration.Managers;
using Content.Shared.Roles;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration.Systems
{
@@ -16,9 +14,6 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private AdminNameOverlay _adminNameOverlay = default!;
@@ -27,16 +22,7 @@ namespace Content.Client.Administration.Systems
private void InitializeOverlay()
{
_adminNameOverlay = new AdminNameOverlay(
this,
EntityManager,
_eyeManager,
_resourceCache,
_entityLookup,
_userInterfaceManager,
_configurationManager,
_roles,
_proto);
_adminNameOverlay = new AdminNameOverlay(this, EntityManager, _eyeManager, _resourceCache, _entityLookup, _userInterfaceManager);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}
@@ -52,8 +38,7 @@ 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

@@ -19,11 +19,11 @@ namespace Content.Client.Administration.Systems
OnBwoinkTextMessageRecieved?.Invoke(this, message);
}
public void Send(NetUserId channelId, string text, bool playSound, bool adminOnly)
public void Send(NetUserId channelId, string text, bool playSound)
{
// Reuse the channel ID as the 'true sender'.
// Server will ignore this and if someone makes it not ignore this (which is bad, allows impersonation!!!), that will help.
RaiseNetworkEvent(new BwoinkTextMessage(channelId, channelId, text, playSound: playSound, adminOnly: adminOnly));
RaiseNetworkEvent(new BwoinkTextMessage(channelId, channelId, text, playSound: playSound));
SendInputTextUpdated(channelId, false);
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Administration.Components;
using Content.Client.Administration.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Administration.Systems;

View File

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

View File

@@ -1,20 +0,0 @@
<Control
xmlns="https://spacestation14.io"
xmlns:viewport="clr-namespace:Content.Client.Viewport"
MouseFilter="Stop">
<PanelContainer StyleClasses="BackgroundDark" Name="AdminCameraWindowRoot" Access="Public">
<BoxContainer Orientation="Vertical" Access="Public">
<!-- Camera -->
<Control VerticalExpand="True" Name="CameraViewBox">
<viewport:ScalingViewport Name="CameraView"
MinSize="100 100"
MouseFilter="Ignore" />
</Control>
<!-- Controller buttons -->
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
<Button StyleClasses="OpenRight" Name="FollowButton" HorizontalExpand="True" Access="Public" Text="{Loc 'admin-camera-window-follow'}" />
<Button StyleClasses="OpenLeft" Name="PopControl" HorizontalExpand="True" Access="Public" Text="{Loc 'admin-camera-window-pop-out'}" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</Control>

View File

@@ -1,101 +0,0 @@
using System.Numerics;
using Content.Client.Eye;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Administration.UI.AdminCamera;
[GenerateTypedNameReferences]
public sealed partial class AdminCameraControl : Control
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
public event Action? OnFollow;
public event Action? OnPopoutControl;
private readonly EyeLerpingSystem _eyeLerpingSystem;
private readonly FixedEye _defaultEye = new();
private AdminCameraEuiState? _nextState;
private const float MinimumZoom = 0.1f;
private const float MaximumZoom = 2.0f;
public EntityUid? CurrentCamera;
public float Zoom = 1.0f;
public bool IsPoppedOut;
public AdminCameraControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_eyeLerpingSystem = _entManager.System<EyeLerpingSystem>();
CameraView.Eye = _defaultEye;
FollowButton.OnPressed += _ => OnFollow?.Invoke();
PopControl.OnPressed += _ => OnPopoutControl?.Invoke();
CameraView.OnResized += OnResized;
}
private new void OnResized()
{
var width = Math.Max(CameraView.PixelWidth, (int)Math.Floor(CameraView.MinWidth));
var height = Math.Max(CameraView.PixelHeight, (int)Math.Floor(CameraView.MinHeight));
CameraView.ViewportSize = new Vector2i(width, height);
}
protected override void MouseWheel(GUIMouseWheelEventArgs args)
{
base.MouseWheel(args);
if (CameraView.Eye == null)
return;
Zoom = Math.Clamp(Zoom - args.Delta.Y * 0.15f * Zoom, MinimumZoom, MaximumZoom);
CameraView.Eye.Zoom = new Vector2(Zoom, Zoom);
args.Handle();
}
public void SetState(AdminCameraEuiState state)
{
_nextState = state;
}
// I know that this is awful, but I copied this from the solution editor anyways.
// This is needed because EUIs update before the gamestate is applied, which means it will fail to get the uid from the net entity.
// The suggestion from the comment in the solution editor saying to use a BUI is not ideal either:
// - We would need to bind the UI to an entity, but with how BUIs currently work we cannot open it in the same tick as we spawn that entity on the server.
// - We want the UI opened by the user session, not by their currently attached entity. Otherwise it would close in cases where admins move from one entity to another, for example when ghosting.
protected override void FrameUpdate(FrameEventArgs args)
{
if (_nextState == null || _timing.LastRealTick < _nextState.Tick) // make sure the last gamestate has been applied
return;
if (!_entManager.TryGetEntity(_nextState.Camera, out var cameraUid))
return;
if (CurrentCamera == null)
{
_eyeLerpingSystem.AddEye(cameraUid.Value);
CurrentCamera = cameraUid;
}
else if (CurrentCamera != cameraUid)
{
_eyeLerpingSystem.RemoveEye(CurrentCamera.Value);
_eyeLerpingSystem.AddEye(cameraUid.Value);
CurrentCamera = cameraUid;
}
if (_entManager.TryGetComponent<EyeComponent>(CurrentCamera, out var eye))
CameraView.Eye = eye.Eye ?? _defaultEye;
}
}

View File

@@ -1,117 +0,0 @@
using System.Numerics;
using Content.Client.Eui;
using Content.Shared.Administration;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Administration.UI.AdminCamera;
/// <summary>
/// Admin Eui for opening a viewport window to observe entities.
/// Use the "Open Camera" admin verb or the "camera" command to open.
/// </summary>
[UsedImplicitly]
public sealed partial class AdminCameraEui : BaseEui
{
private readonly AdminCameraWindow _window;
private readonly AdminCameraControl _control;
// If not null the camera is in "popped out" mode and is in an external window.
private OSWindow? _OSWindow;
// The last location the window was located at in game.
// Is used for getting knowing where to "pop in" external windows.
private Vector2 _lastLocation;
public AdminCameraEui()
{
_window = new AdminCameraWindow();
_control = new AdminCameraControl();
_window.Contents.AddChild(_control);
_control.OnFollow += () => SendMessage(new AdminCameraFollowMessage());
_window.OnClose += () =>
{
if (!_control.IsPoppedOut)
SendMessage(new CloseEuiMessage());
};
_control.OnPopoutControl += () =>
{
if (_control.IsPoppedOut)
PopIn();
else
PopOut();
};
}
// Pop the window out into an external OS window
private void PopOut()
{
_lastLocation = _window.Position;
// TODO: When there is a way to have a minimum window size, enforce something!
_OSWindow = new OSWindow
{
SetSize = _window.Size,
Title = _window.Title ?? Loc.GetString("admin-camera-window-title-placeholder"),
};
_OSWindow.Show();
if (_OSWindow.Root == null)
return;
_control.Orphan();
_OSWindow.Root.AddChild(_control);
_OSWindow.Closed += () =>
{
if (_control.IsPoppedOut)
SendMessage(new CloseEuiMessage());
};
_control.IsPoppedOut = true;
_control.PopControl.Text = Loc.GetString("admin-camera-window-pop-in");
_window.Close();
}
// Pop the window back into the in game window.
private void PopIn()
{
_control.Orphan();
_window.Contents.AddChild(_control);
_window.Open(_lastLocation);
_control.IsPoppedOut = false;
_control.PopControl.Text = Loc.GetString("admin-camera-window-pop-out");
_OSWindow?.Close();
_OSWindow = null;
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.Close();
}
public override void HandleState(EuiStateBase baseState)
{
if (baseState is not AdminCameraEuiState state)
return;
_window.SetState(state);
_control.SetState(state);
}
}

View File

@@ -1,6 +0,0 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc admin-camera-window-title-placeholder}"
SetSize="425 550"
MinSize="200 225"
Name="Window">
</DefaultWindow>

View File

@@ -1,23 +0,0 @@
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.AdminCamera;
[GenerateTypedNameReferences]
public sealed partial class AdminCameraWindow : DefaultWindow
{
public AdminCameraWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ContentsContainer.Margin = new Thickness(5, 0, 5, 0);
}
public void SetState(AdminCameraEuiState state)
{
Title = Loc.GetString("admin-camera-window-title", ("name", state.Name));
}
}

View File

@@ -6,7 +6,8 @@
xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs"
xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab">
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"
xmlns:baby="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab">
<TabContainer Name="MasterTabContainer">
<adminTab:AdminTab />
<adminbusTab:AdminbusTab />
@@ -14,6 +15,7 @@
<tabs:RoundTab />
<tabs:ServerTab />
<panic:PanicBunkerTab Name="PanicBunkerControl" Access="Public" />
<baby:BabyJailTab Name="BabyJailControl" Access="Public" />
<playerTab:PlayerTab Name="PlayerTabControl" Access="Public" />
<objectsTab:ObjectsTab Name="ObjectsTabControl" Access="Public" />
</TabContainer>

View File

@@ -21,6 +21,10 @@ public sealed partial class AdminMenuWindow : DefaultWindow
MasterTabContainer.SetTabTitle((int) TabIndex.Round, Loc.GetString("admin-menu-round-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Server, Loc.GetString("admin-menu-server-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.PanicBunker, Loc.GetString("admin-menu-panic-bunker-tab"));
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
MasterTabContainer.SetTabTitle((int) TabIndex.BabyJail, Loc.GetString("admin-menu-baby-jail-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Players, Loc.GetString("admin-menu-players-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Objects, Loc.GetString("admin-menu-objects-tab"));
MasterTabContainer.OnTabChanged += OnTabChanged;
@@ -48,6 +52,7 @@ public sealed partial class AdminMenuWindow : DefaultWindow
Round,
Server,
PanicBunker,
BabyJail,
Players,
Objects,
}

View File

@@ -0,0 +1,59 @@
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,7 +1,7 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc ban-panel-title}" MinSize="410 500">
Title="{Loc ban-panel-title}" MinSize="350 500">
<BoxContainer Orientation="Vertical">
<TabContainer Name="Tabs" VerticalExpand="True">
<!-- Basic info -->

View File

@@ -1,14 +1,12 @@
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using Content.Client.Administration.UI.CustomControls;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -33,21 +31,14 @@ public sealed partial class BanPanel : DefaultWindow
private uint Multiplier { get; set; }
private bool HasBanFlag { get; set; }
private TimeSpan? ButtonResetOn { get; set; }
// This is less efficient than just holding a reference to the root control and enumerating children, but you
// have to know how the controls are nested, which makes the code more complicated.
// Role group name -> the role buttons themselves.
private readonly Dictionary<string, List<Button>> _roleCheckboxes = new();
private readonly List<CheckBox> _roleCheckboxes = new();
private readonly ISawmill _banpanelSawmill;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
private const string ExpandedArrow = "▼";
private const string ContractedArrow = "▶";
private enum TabNumbers
{
@@ -153,90 +144,47 @@ public sealed partial class BanPanel : DefaultWindow
ReasonTextEdit.Placeholder = new Rope.Leaf(Loc.GetString("ban-panel-reason"));
var departmentJobs = _protoMan.EnumeratePrototypes<DepartmentPrototype>()
.OrderBy(x => x.Weight);
foreach (var proto in departmentJobs)
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var proto in prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
var roles = proto.Roles.Select(x => _protoMan.Index(x))
.OrderBy(x => x.ID);
CreateRoleGroup(proto.ID, proto.Color, roles);
CreateRoleGroup(proto.ID, proto.Roles.Select(p => p.Id), proto.Color);
}
var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
.OrderBy(x => x.ID);
CreateRoleGroup("Antagonist", Color.Red, antagRoles);
CreateRoleGroup("Antagonist", prototypeManager.EnumeratePrototypes<AntagPrototype>().Select(p => p.ID), Color.Red);
}
/// <summary>
/// Creates a "Role group" which stores information and logic for one "group" of roll bans.
/// For example, all antags are one group, logi is a group, medical is a group, etc...
/// </summary>
private void CreateRoleGroup<T>(string groupName, Color color, IEnumerable<T> roles) where T : class, IPrototype
private void CreateRoleGroup(string roleName, IEnumerable<string> roleList, Color color)
{
var outerContainer = new BoxContainer
{
Name = $"{groupName}GroupOuterBox",
Name = $"{roleName}GroupOuterBox",
HorizontalExpand = true,
VerticalExpand = true,
Orientation = BoxContainer.LayoutOrientation.Vertical,
Margin = new Thickness(4),
Margin = new Thickness(4)
};
// Stores stuff like ban all and expand buttons.
var roleGroupHeader = new BoxContainer
var departmentCheckbox = new CheckBox
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Name = $"{roleName}GroupCheckbox",
Text = roleName,
Modulate = color,
HorizontalAlignment = HAlignment.Left
};
// Stores the role checkboxes themselves.
var innerContainer = new GridContainer
outerContainer.AddChild(departmentCheckbox);
var innerContainer = new BoxContainer
{
Name = $"{groupName}GroupInnerBox",
Name = $"{roleName}GroupInnerBox",
HorizontalExpand = true,
Columns = 2,
Visible = false,
Margin = new Thickness(15, 5, 0, 5),
Orientation = BoxContainer.LayoutOrientation.Horizontal
};
var roleGroupCheckbox = CreateRoleGroupHeader(groupName, roleGroupHeader, color, innerContainer);
outerContainer.AddChild(roleGroupHeader);
// Add the roles themselves
foreach (var role in roles)
departmentCheckbox.OnToggled += args =>
{
AddRoleCheckbox(groupName, role.ID, innerContainer, roleGroupCheckbox);
}
outerContainer.AddChild(innerContainer);
RolesContainer.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat
foreach (var child in innerContainer.Children)
{
BackgroundColor = color
}
});
RolesContainer.AddChild(outerContainer);
RolesContainer.AddChild(new HSeparator());
}
private Button CreateRoleGroupHeader(string groupName, BoxContainer header, Color color, GridContainer innerContainer)
{
var roleGroupCheckbox = new Button
{
Name = $"{groupName}GroupCheckbox",
Text = "Ban all",
Margin = new Thickness(0, 0, 5, 0),
ToggleMode = true,
};
// When this is toggled, toggle all buttons in this group so they match.
roleGroupCheckbox.OnToggled += args =>
{
foreach (var role in _roleCheckboxes[groupName])
{
role.Pressed = args.Pressed;
if (child is CheckBox c)
{
c.Pressed = args.Pressed;
}
}
if (args.Pressed)
@@ -251,12 +199,15 @@ public sealed partial class BanPanel : DefaultWindow
}
else
{
foreach (var roleButtons in _roleCheckboxes.Values)
foreach (var childContainer in RolesContainer.Children)
{
foreach (var button in roleButtons)
if (childContainer is Container)
{
if (button.Pressed)
return;
foreach (var child in childContainer.Children)
{
if (child is CheckBox { Pressed: true })
return;
}
}
}
@@ -269,73 +220,38 @@ public sealed partial class BanPanel : DefaultWindow
SeverityOption.SelectId((int) newSeverity);
}
};
var hideButton = new Button
outerContainer.AddChild(innerContainer);
foreach (var role in roleList)
{
Text = Loc.GetString("role-bans-expand-roles") + " " + ContractedArrow,
ToggleMode = true,
};
hideButton.OnPressed += args =>
AddRoleCheckbox(role, innerContainer, departmentCheckbox);
}
RolesContainer.AddChild(new PanelContainer
{
innerContainer.Visible = args.Button.Pressed;
((Button)args.Button).Text = args.Button.Pressed
? Loc.GetString("role-bans-contract-roles") + " " + ExpandedArrow
: Loc.GetString("role-bans-expand-roles") + " " + ContractedArrow;
};
header.AddChild(new Label
{
Text = groupName,
Modulate = color,
Margin = new Thickness(0, 0, 5, 0),
PanelOverride = new StyleBoxFlat
{
BackgroundColor = color
}
});
header.AddChild(roleGroupCheckbox);
header.AddChild(hideButton);
return roleGroupCheckbox;
RolesContainer.AddChild(outerContainer);
RolesContainer.AddChild(new HSeparator());
}
/// <summary>
/// Adds a checkbutton specifically for one "role" in a "group"
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
/// </summary>
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
private void AddRoleCheckbox(string role, Control container, CheckBox header)
{
var roleCheckboxContainer = new BoxContainer();
var roleCheckButton = new Button
var roleCheckbox = new CheckBox
{
Name = $"{role}RoleCheckbox",
Text = role,
ToggleMode = true,
Text = role
};
roleCheckButton.OnToggled += args =>
roleCheckbox.OnToggled += args =>
{
// Checks the role group checkbox if all the children are pressed
if (args.Pressed && _roleCheckboxes[group].All(e => e.Pressed))
roleGroupCheckbox.Pressed = args.Pressed;
if (args is { Pressed: true, Button.Parent: { } } && args.Button.Parent.Children.Where(e => e is CheckBox).All(e => ((CheckBox) e).Pressed))
header.Pressed = args.Pressed;
else
roleGroupCheckbox.Pressed = false;
header.Pressed = false;
};
// This is adding the icon before the role name
// TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
// I know the ban manager is doing the same thing, but that should not leak into UI code.
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto))
{
var jobIconTexture = new TextureRect
{
Texture = _entMan.System<SpriteSystem>().Frame0(iconProto.Icon),
TextureScale = new Vector2(2.5f, 2.5f),
Stretch = TextureRect.StretchMode.KeepCentered,
Margin = new Thickness(5, 0, 0, 0),
};
roleCheckboxContainer.AddChild(jobIconTexture);
}
roleCheckboxContainer.AddChild(roleCheckButton);
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []);
_roleCheckboxes[group].Add(roleCheckButton);
container.AddChild(roleCheckbox);
_roleCheckboxes.Add(roleCheckbox);
}
public void UpdateBanFlag(bool newFlag)
@@ -553,13 +469,7 @@ public sealed partial class BanPanel : DefaultWindow
if (_roleCheckboxes.Count == 0)
throw new DebugAssertException("RoleCheckboxes was empty");
foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons))
{
if (button is { Pressed: true, Text: not null })
{
rolesList.Add(button.Text);
}
}
rolesList.AddRange(_roleCheckboxes.Where(c => c is { Pressed: true, Text: { } }).Select(c => c.Text!));
if (rolesList.Count == 0)
{

View File

@@ -1,28 +1,23 @@
<Control
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
<PanelContainer StyleClasses="BackgroundDark">
<SplitContainer Orientation="Vertical" ResizeMode="NotResizable">
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="2" />
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="1" />
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<CheckBox Visible="True" Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<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" />
<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" />
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
</BoxContainer>
</SplitContainer>
<BoxContainer Orientation="Horizontal" SetHeight="30" >
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
<Control HorizontalExpand="True" MinWidth="5" />
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<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" />
<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" />
<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>
</PanelContainer>

View File

@@ -29,15 +29,13 @@ namespace Content.Client.Administration.UI.Bwoink
public AdminAHelpUIHandler AHelpHelper = default!;
private PlayerInfo? _currentPlayer;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public BwoinkControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var newPlayerThreshold = 0;
_cfg.OnValueChanged(CCVars.NewPlayerThreshold, (val) => { newPlayerThreshold = val; }, true);
var uiController = _ui.GetUIController<AHelpUIController>();
if (uiController.UIHelper is not AdminAHelpUIHandler helper)
return;
@@ -47,8 +45,6 @@ namespace Content.Client.Administration.UI.Bwoink
_adminManager.AdminStatusUpdated += UpdateButtons;
UpdateButtons();
AdminOnly.OnToggled += args => PlaySound.Disabled = args.Pressed;
ChannelSelector.OnSelectionChanged += sel =>
{
_currentPlayer = sel;
@@ -61,9 +57,9 @@ namespace Content.Client.Administration.UI.Bwoink
var sb = new StringBuilder();
if (info.Connected)
sb.Append(info.ActiveThisRound ? '⚫' : '◐');
sb.Append('●');
else
sb.Append(info.ActiveThisRound ? '' : '·');
sb.Append(info.ActiveThisRound ? '' : '·');
sb.Append(' ');
if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
@@ -75,12 +71,10 @@ namespace Content.Client.Administration.UI.Bwoink
sb.Append(' ');
}
// Mark antagonists with symbol
if (info.Antag && info.ActiveThisRound)
sb.Append(new Rune(0x1F5E1)); // 🗡
// Mark new players with symbol
if (IsNewPlayer(info))
if (info.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)))
sb.Append(new Rune(0x23F2)); // ⏲
sb.AppendFormat("\"{0}\"", text);
@@ -88,19 +82,6 @@ namespace Content.Client.Administration.UI.Bwoink
return sb.ToString();
};
// <summary>
// Returns true if the player's overall playtime is under the set threshold
// </summary>
bool IsNewPlayer(PlayerInfo info)
{
// Don't show every disconnected player as new, don't show 0-minute players as new if threshold is
if (newPlayerThreshold <= 0 || info.OverallPlaytime is null && !info.Connected)
return false;
return (info.OverallPlaytime is null
|| info.OverallPlaytime < TimeSpan.FromMinutes(newPlayerThreshold));
}
ChannelSelector.Comparison = (a, b) =>
{
var ach = AHelpHelper.EnsurePanel(a.SessionId);
@@ -110,37 +91,31 @@ namespace Content.Client.Administration.UI.Bwoink
if (a.IsPinned != b.IsPinned)
return a.IsPinned ? -1 : 1;
// Then, any chat with unread messages.
// First, sort by unread. Any chat with unread messages appears first.
var aUnread = ach.Unread > 0;
var bUnread = bch.Unread > 0;
if (aUnread != bUnread)
return aUnread ? -1 : 1;
// Then, any chat with recent messages from the current round
// Sort by recent messages during the current round.
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
if (aRecent != bRecent)
return aRecent ? -1 : 1;
// Sort by connection status. Disconnected players will be last.
// Next, sort by connection status. Any disconnected players are grouped towards the end.
if (a.Connected != b.Connected)
return a.Connected ? -1 : 1;
// Sort connected players by whether they have joined the round, then by New Player status, then by Antag status
// Sort connected players by New Player status, then by Antag status
if (a.Connected && b.Connected)
{
var aNewPlayer = IsNewPlayer(a);
var bNewPlayer = IsNewPlayer(b);
var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
// Players who have joined the round will be listed before players in the lobby
if (a.ActiveThisRound != b.ActiveThisRound)
return a.ActiveThisRound ? -1 : 1;
// Within both the joined group and lobby group, new players will be grouped and listed first
if (aNewPlayer != bNewPlayer)
return aNewPlayer ? -1 : 1;
// Within all four previous groups, antagonists will be listed first.
if (a.Antag != b.Antag)
return a.Antag ? -1 : 1;
}
@@ -177,6 +152,11 @@ 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}\"");
@@ -190,6 +170,11 @@ 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

@@ -22,9 +22,12 @@ namespace Content.Client.Administration.UI.Bwoink
return;
}
Title = $"{sel.CharacterName} / {sel.Username} | {Loc.GetString("generic-playtime-title")}: ";
Title = $"{sel.CharacterName} / {sel.Username}";
Title += sel.OverallPlaytime != null ? sel.PlaytimeString : Loc.GetString("generic-unknown-title");
if (sel.OverallPlaytime != null)
{
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
};
OnOpen += () =>

View File

@@ -49,7 +49,6 @@
<Control HorizontalExpand="True"/>
<Label Name="Count" Access="Public"/>
<Control HorizontalExpand="True"/>
<Button Name="ExportLogs" Access="Public" Text="{Loc admin-logs-export}"/>
<Button Name="PopOutButton" Access="Public" Text="{Loc admin-logs-pop-out}"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">

View File

@@ -1,6 +1,4 @@
using System.IO;
using System.Linq;
using Content.Client.Administration.UI.CustomControls;
using System.Linq;
using Content.Client.Eui;
using Content.Shared.Administration.Logs;
using Content.Shared.Eui;
@@ -17,16 +15,6 @@ public sealed class AdminLogsEui : BaseEui
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IFileDialogManager _dialogManager = default!;
[Dependency] private readonly ILogManager _log = default!;
private const char CsvSeparator = ',';
private const string CsvQuote = "\"";
private const string CsvHeader = "Date,ID,PlayerID,Severity,Type,Message";
private ISawmill _sawmill;
private bool _currentlyExportingLogs = false;
public AdminLogsEui()
{
@@ -38,9 +26,6 @@ public sealed class AdminLogsEui : BaseEui
LogsControl.RefreshButton.OnPressed += _ => RequestLogs();
LogsControl.NextButton.OnPressed += _ => NextLogs();
LogsControl.PopOutButton.OnPressed += _ => PopOut();
LogsControl.ExportLogs.OnPressed += _ => ExportLogs();
_sawmill = _log.GetSawmill("admin.logs.ui");
}
private WindowRoot? Root { get; set; }
@@ -89,71 +74,6 @@ public sealed class AdminLogsEui : BaseEui
SendMessage(request);
}
private async void ExportLogs()
{
if (_currentlyExportingLogs)
return;
_currentlyExportingLogs = true;
LogsControl.ExportLogs.Disabled = true;
var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("csv")));
if (file == null)
return;
try
{
// Buffer is set to 4KB for performance reasons. As the average export of 1000 logs is ~200KB
await using var writer = new StreamWriter(file.Value.fileStream, bufferSize: 4096);
await writer.WriteLineAsync(CsvHeader);
foreach (var child in LogsControl.LogsContainer.Children)
{
if (child is not AdminLogLabel logLabel || !child.Visible)
continue;
var log = logLabel.Log;
// Date
// I swear to god if someone adds ,s or "s to the other fields...
await writer.WriteAsync(log.Date.ToString("s", System.Globalization.CultureInfo.InvariantCulture));
await writer.WriteAsync(CsvSeparator);
// ID
await writer.WriteAsync(log.Id.ToString());
await writer.WriteAsync(CsvSeparator);
// PlayerID
var players = log.Players;
for (var i = 0; i < players.Length; i++)
{
await writer.WriteAsync(players[i] + (i == players.Length - 1 ? "" : " "));
}
await writer.WriteAsync(CsvSeparator);
// Severity
await writer.WriteAsync(log.Impact.ToString());
await writer.WriteAsync(CsvSeparator);
// Type
await writer.WriteAsync(log.Type.ToString());
await writer.WriteAsync(CsvSeparator);
// Message
await writer.WriteAsync(CsvQuote);
await writer.WriteAsync(log.Message.Replace(CsvQuote, CsvQuote + CsvQuote));
await writer.WriteAsync(CsvQuote);
await writer.WriteLineAsync();
}
}
catch (Exception exc)
{
_sawmill.Error($"Error when exporting admin log:\n{exc.StackTrace}");
}
finally
{
await file.Value.fileStream.DisposeAsync();
_currentlyExportingLogs = false;
LogsControl.ExportLogs.Disabled = false;
}
}
private void PopOut()
{
if (LogsWindow == null)

View File

@@ -8,21 +8,6 @@
<OptionButton Name="SolutionOption" HorizontalExpand="True"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 4">
<Button Name="VVButton"
Text="{Loc 'admin-solutions-window-vv-button'}"
ToolTip="{Loc 'admin-solutions-window-vv-button-tooltip'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenRight"/>
<Button Name="SolutionButton"
Text="{Loc 'admin-solutions-window-solution-button'}"
ToolTip="{Loc 'admin-solutions-window-solution-button-tooltip'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenLeft"/>
</BoxContainer>
<!-- The total volume / capacity of the solution -->
<BoxContainer Name="VolumeBox" Orientation="Vertical" HorizontalExpand="True" Margin="0 4"/>

View File

@@ -1,4 +1,3 @@
using Content.Client.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
@@ -21,7 +20,6 @@ namespace Content.Client.Administration.UI.ManageSolutions
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IClientAdminManager _admin = default!;
private NetEntity _target = NetEntity.Invalid;
private string? _selectedSolution;
@@ -36,11 +34,6 @@ namespace Content.Client.Administration.UI.ManageSolutions
SolutionOption.OnItemSelected += SolutionSelected;
AddButton.OnPressed += OpenAddReagentWindow;
VVButton.OnPressed += OpenVVWindow;
SolutionButton.OnPressed += OpenSolutionWindow;
VVButton.Disabled = !_admin.CanViewVar();
SolutionButton.Disabled = !_admin.CanViewVar();
}
public override void Close()
@@ -278,32 +271,6 @@ namespace Content.Client.Administration.UI.ManageSolutions
_addReagentWindow.OpenCentered();
}
/// <summary>
/// Open the corresponding solution entity in a ViewVariables window.
/// </summary>
private void OpenVVWindow(BaseButton.ButtonEventArgs obj)
{
if (_solutions == null
|| _selectedSolution == null
|| !_solutions.TryGetValue(_selectedSolution, out var uid)
|| !_entityManager.TryGetNetEntity(uid, out var netEntity))
return;
_consoleHost.ExecuteCommand($"vv {netEntity}");
}
/// <summary>
/// Open the corresponding Solution instance in a ViewVariables window.
/// </summary>
private void OpenSolutionWindow(BaseButton.ButtonEventArgs obj)
{
if (_solutions == null
|| _selectedSolution == null
|| !_solutions.TryGetValue(_selectedSolution, out var uid)
|| !_entityManager.TryGetNetEntity(uid, out var netEntity))
return;
_consoleHost.ExecuteCommand($"vv /entity/{netEntity}/Solution/Solution");
}
/// <summary>
/// When a new solution is selected, set _selectedSolution and update the reagent list.
/// </summary>

View File

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

View File

@@ -130,7 +130,6 @@ namespace Content.Client.Administration.UI
}
var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text;
var suspended = popup.SuspendedCheckbox.Pressed;
if (popup.SourceData is { } src)
{
@@ -140,8 +139,7 @@ namespace Content.Client.Administration.UI
Title = title,
PosFlags = pos,
NegFlags = neg,
RankId = rank,
Suspended = suspended,
RankId = rank
});
}
else
@@ -154,8 +152,7 @@ namespace Content.Client.Administration.UI
Title = title,
PosFlags = pos,
NegFlags = neg,
RankId = rank,
Suspended = suspended,
RankId = rank
});
}
@@ -174,7 +171,7 @@ namespace Content.Client.Administration.UI
{
Id = src,
Flags = flags,
Name = name,
Name = name
});
}
else
@@ -354,7 +351,6 @@ namespace Content.Client.Administration.UI
public readonly OptionButton RankButton;
public readonly Button SaveButton;
public readonly Button? RemoveButton;
public readonly CheckBox SuspendedCheckbox;
public readonly Dictionary<AdminFlags, (Button inherit, Button sub, Button plus)> FlagButtons
= new();
@@ -385,12 +381,6 @@ namespace Content.Client.Administration.UI
RankButton = new OptionButton();
SaveButton = new Button { Text = Loc.GetString("permissions-eui-edit-admin-window-save-button"), HorizontalAlignment = HAlignment.Right };
SuspendedCheckbox = new CheckBox
{
Text = Loc.GetString("permissions-eui-edit-admin-window-suspended"),
Pressed = data?.Suspended ?? false,
};
RankButton.AddItem(Loc.GetString("permissions-eui-edit-admin-window-no-rank-button"), NoRank);
foreach (var (rId, rank) in ui._ranks)
{
@@ -498,8 +488,7 @@ namespace Content.Client.Administration.UI
{
nameControl,
TitleEdit,
RankButton,
SuspendedCheckbox,
RankButton
}
},
permGrid

View File

@@ -19,19 +19,17 @@
<Label Name="SharedConnections"/>
<BoxContainer Align="Center">
<GridContainer Rows="6">
<GridContainer Rows="5">
<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"/>
<Button Name="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/>
<controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/>
<Button Name="CameraButton" Text="{Loc player-panel-camera}" Disabled="True"/>
</GridContainer>
</BoxContainer>
</BoxContainer>

View File

@@ -2,6 +2,7 @@ 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;
@@ -18,10 +19,8 @@ public sealed partial class PlayerPanel : FancyWindow
public event Action<NetUserId?>? OnOpenBans;
public event Action<NetUserId?>? OnAhelp;
public event Action<string?>? OnKick;
public event Action<string?>? OnCamera;
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;
@@ -34,27 +33,25 @@ public sealed partial class PlayerPanel : FancyWindow
public PlayerPanel(IClientAdminManager adminManager)
{
RobustXamlLoader.Load(this);
_adminManager = adminManager;
RobustXamlLoader.Load(this);
_adminManager = adminManager;
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? "");
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
CameraButton.OnPressed += _ => OnCamera?.Invoke(TargetUsername);
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
WhitelistToggle.OnPressed += _ =>
{
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
SetWhitelisted(!_isWhitelisted);
};
FollowButton.OnPressed += _ => OnFollow?.Invoke();
FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
LogsButton.OnPressed += _ => OnLogs?.Invoke();
DeleteButton.OnPressed += _ => OnDelete?.Invoke();
RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? "");
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
WhitelistToggle.OnPressed += _ =>
{
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
SetWhitelisted(!_isWhitelisted);
};
FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
LogsButton.OnPressed += _ => OnLogs?.Invoke();
DeleteButton.OnPressed += _ => OnDelete?.Invoke();
RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
}
public void SetUsername(string player)
@@ -124,7 +121,6 @@ public sealed partial class PlayerPanel : FancyWindow
{
BanButton.Disabled = !_adminManager.CanCommand("banpanel");
KickButton.Disabled = !_adminManager.CanCommand("kick");
CameraButton.Disabled = !_adminManager.CanCommand("camera");
NotesButton.Disabled = !_adminManager.CanCommand("adminnotes");
ShowBansButton.Disabled = !_adminManager.CanCommand("banlist");
WhitelistToggle.Disabled =

View File

@@ -15,7 +15,7 @@ public sealed class PlayerPanelEui : BaseEui
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IClipboardManager _clipboard = default!;
private PlayerPanel PlayerPanel { get; }
private PlayerPanel PlayerPanel { get; }
public PlayerPanelEui()
{
@@ -25,7 +25,6 @@ public sealed class PlayerPanelEui : BaseEui
PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\"");
// Kick command does not support GUIDs
PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\"");
PlayerPanel.OnCamera += username => _console.ExecuteCommand($"camera \"{username}\"");
PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\"");
PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\"");
PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\"");
@@ -38,8 +37,7 @@ public sealed class PlayerPanelEui : BaseEui
PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage());
PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
PlayerPanel.OnDelete += () => SendMessage(new PlayerPanelDeleteMessage());
PlayerPanel.OnFollow += () => SendMessage(new PlayerPanelFollowMessage());
PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
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,7 +38,6 @@ public sealed partial class SpawnExplosionWindow : DefaultWindow
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_mapSystem = _entMan.System<SharedMapSystem>();
_transform = _entMan.System<TransformSystem>();
_eui = eui;
@@ -88,7 +87,7 @@ public sealed partial class SpawnExplosionWindow : DefaultWindow
{
_mapData.Clear();
MapOptions.Clear();
foreach (var map in _mapSystem.GetAllMapIds())
foreach (var map in _mapManager.GetAllMapIds())
{
_mapData.Add(map);
MapOptions.AddItem(map.ToString());

View File

@@ -16,7 +16,6 @@
<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}"/>
</GridContainer>
</BoxContainer>
</Control>

View File

@@ -1,7 +1,6 @@
<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">
@@ -11,9 +10,9 @@
</BoxContainer>
<cc:PlayerListControl Name="PlayerList" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal">
<controls:ConfirmButton Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
<Button Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" Disabled="True"/>
<Button Name="SubmitAHelpButton" Text="{Loc admin-player-actions-ahelp}" Disabled="True"/>
<controls:ConfirmButton Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
<Button Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" Disabled="True"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -14,6 +14,7 @@ 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()
{
@@ -27,6 +28,9 @@ 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;
@@ -39,6 +43,9 @@ 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)}\"");
}
@@ -57,6 +64,9 @@ 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,9 +14,6 @@ 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);
@@ -24,9 +21,9 @@ namespace Content.Client.Administration.UI.Tabs.AdminbusTab
protected override void EnteredTree()
{
var mapSystem = _entityManager.System<SharedMapSystem>();
var mapManager = IoCManager.Resolve<IMapManager>();
foreach (var mapId in mapSystem.GetAllMapIds())
foreach (var mapId in mapManager.GetAllMapIds())
{
MapOptions.AddItem(mapId.ToString(), (int) mapId);
}
@@ -42,19 +39,21 @@ namespace Content.Client.Administration.UI.Tabs.AdminbusTab
private void Reset()
{
var xformSystem = _entityManager.System<SharedTransformSystem>();
var player = _playerManager.LocalEntity;
var entManager = IoCManager.Resolve<IEntityManager>();
var xformSystem = entManager.System<SharedTransformSystem>();
var playerManager = IoCManager.Resolve<IPlayerManager>();
var player = playerManager.LocalEntity;
var currentMap = MapId.Nullspace;
var position = Vector2.Zero;
var rotation = Angle.Zero;
if (_entityManager.TryGetComponent<TransformComponent>(player, out var xform))
if (entManager.TryGetComponent<TransformComponent>(player, out var xform))
{
currentMap = xform.MapID;
position = xformSystem.GetWorldPosition(xform);
if (_entityManager.TryGetComponent<TransformComponent>(xform.GridUid, out var gridXform))
if (entManager.TryGetComponent<TransformComponent>(xform.GridUid, out var gridXform))
{
rotation = xformSystem.GetWorldRotation(gridXform);
}

View File

@@ -0,0 +1,6 @@
<controls:BabyJailStatusWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
Title="{Loc admin-ui-baby-jail-window-title}">
<RichTextLabel Name="MessageLabel" Access="Public" />
</controls:BabyJailStatusWindow>

View File

@@ -0,0 +1,21 @@
using Content.Client.Message;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
/*
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
[GenerateTypedNameReferences]
public sealed partial class BabyJailStatusWindow : FancyWindow
{
public BabyJailStatusWindow()
{
RobustXamlLoader.Load(this);
MessageLabel.SetMarkup(Loc.GetString("admin-ui-baby-jail-is-enabled"));
}
}

View File

@@ -0,0 +1,26 @@
<controls:BabyJailTab
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="4">
<BoxContainer Orientation="Vertical">
<cc:CommandButton Name="EnabledButton" Command="babyjail" ToggleMode="True"
Text="{Loc admin-ui-baby-jail-disabled}"
ToolTip="{Loc admin-ui-baby-jail-tooltip}" />
<cc:CommandButton Name="ShowReasonButton" Command="babyjail_show_reason"
ToggleMode="True" Text="{Loc admin-ui-baby-jail-show-reason}"
ToolTip="{Loc admin-ui-baby-jail-show-reason-tooltip}" />
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-baby-jail-max-account-age}" MinWidth="175" />
<LineEdit Name="MaxAccountAge" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-baby-jail-max-overall-minutes}" MinWidth="175" />
<LineEdit Name="MaxOverallMinutes" MinWidth="50" Margin="0 0 5 0" />
<Label Text="{Loc generic-minutes}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:BabyJailTab>

View File

@@ -0,0 +1,75 @@
using Content.Shared.Administration.Events;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
/*
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
[GenerateTypedNameReferences]
public sealed partial class BabyJailTab : Control
{
[Dependency] private readonly IConsoleHost _console = default!;
private string _maxAccountAge;
private string _maxOverallMinutes;
public BabyJailTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
MaxAccountAge.OnTextEntered += args => SendMaxAccountAge(args.Text);
MaxAccountAge.OnFocusExit += args => SendMaxAccountAge(args.Text);
_maxAccountAge = MaxAccountAge.Text;
MaxOverallMinutes.OnTextEntered += args => SendMaxOverallMinutes(args.Text);
MaxOverallMinutes.OnFocusExit += args => SendMaxOverallMinutes(args.Text);
_maxOverallMinutes = MaxOverallMinutes.Text;
}
private void SendMaxAccountAge(string text)
{
if (string.IsNullOrWhiteSpace(text) ||
text == _maxAccountAge ||
!int.TryParse(text, out var minutes))
{
return;
}
_console.ExecuteCommand($"babyjail_max_account_age {minutes}");
}
private void SendMaxOverallMinutes(string text)
{
if (string.IsNullOrWhiteSpace(text) ||
text == _maxOverallMinutes ||
!int.TryParse(text, out var minutes))
{
return;
}
_console.ExecuteCommand($"babyjail_max_overall_minutes {minutes}");
}
public void UpdateStatus(BabyJailStatus status)
{
EnabledButton.Pressed = status.Enabled;
EnabledButton.Text = Loc.GetString(status.Enabled
? "admin-ui-baby-jail-enabled"
: "admin-ui-baby-jail-disabled"
);
EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null;
ShowReasonButton.Pressed = status.ShowReason;
MaxAccountAge.Text = status.MaxAccountAgeMinutes.ToString();
_maxAccountAge = MaxAccountAge.Text;
MaxOverallMinutes.Text = status.MaxOverallMinutes.ToString();
_maxOverallMinutes = MaxOverallMinutes.Text;
}
}

View File

@@ -57,43 +57,12 @@ public sealed partial class ObjectsTab : Control
private void TeleportTo(NetEntity nent)
{
var selection = _selections[ObjectTypeOptions.SelectedId];
switch (selection)
{
case ObjectsTabSelection.Grids:
{
// directly teleport to the entity
_console.ExecuteCommand($"tpto {nent}");
}
break;
case ObjectsTabSelection.Maps:
{
// teleport to the map, not to the map entity (which is in nullspace)
if (!_entityManager.TryGetEntity(nent, out var map) || !_entityManager.TryGetComponent<MapComponent>(map, out var mapComp))
break;
_console.ExecuteCommand($"tp 0 0 {mapComp.MapId}");
break;
}
case ObjectsTabSelection.Stations:
{
// teleport to the station's largest grid, not to the station entity (which is in nullspace)
if (!_entityManager.TryGetEntity(nent, out var station))
break;
var largestGrid = _entityManager.EntitySysManager.GetEntitySystem<StationSystem>().GetLargestGrid(station.Value);
if (largestGrid == null)
break;
_console.ExecuteCommand($"tpto {largestGrid.Value}");
break;
}
default:
throw new NotImplementedException();
}
_console.ExecuteCommand($"tpto {nent}");
}
private void Delete(NetEntity nent)
{
_console.ExecuteCommand($"delete {nent}");
RefreshObjectList();
}
public void RefreshObjectList()
@@ -107,24 +76,28 @@ public sealed partial class ObjectsTab : Control
switch (selection)
{
case ObjectsTabSelection.Stations:
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem<StationSystem>().GetStationNames());
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem<StationSystem>().Stations);
break;
case ObjectsTabSelection.Grids:
{
var query = _entityManager.AllEntityQueryEnumerator<MapGridComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
{
var query = _entityManager.AllEntityQueryEnumerator<MapGridComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
break;
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
}
break;
}
case ObjectsTabSelection.Maps:
{
var query = _entityManager.AllEntityQueryEnumerator<MapComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
{
var query = _entityManager.AllEntityQueryEnumerator<MapComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
break;
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
}
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(selection), selection, null);
}
@@ -159,8 +132,8 @@ public sealed partial class ObjectsTab : Control
entry.OnTeleport += TeleportTo;
entry.OnDelete += Delete;
button.ToolTip = $"{info.Name}, {info.Entity}";
button.AddChild(entry);
button.StyleClasses.Clear();
}
private bool DataFilterCondition(string filter, ListData listData)

View File

@@ -1,6 +1,5 @@
<PanelContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Name="BackgroundColorPanel">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
@@ -21,7 +20,7 @@
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<controls:ConfirmButton Name="DeleteButton"
<Button Name="DeleteButton"
Text="{Loc object-tab-entity-delete}"
SizeFlagsStretchRatio="3"
HorizontalExpand="True"

View File

@@ -13,6 +13,7 @@ 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)
{
@@ -27,6 +28,13 @@ public sealed partial class ObjectsTabEntry : PanelContainer
DeleteButton.Disabled = !manager.CanCommand("delete");
TeleportButton.OnPressed += _ => OnTeleport?.Invoke(nent);
DeleteButton.OnPressed += _ => OnDelete?.Invoke(nent);
DeleteButton.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(DeleteButton, _confirmations))
{
return;
}
OnDelete?.Invoke(nent);
};
}
}

View File

@@ -2,13 +2,11 @@ using System.Linq;
using Content.Client.Administration.Systems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -18,7 +16,6 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab;
public sealed partial class PlayerTab : Control
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPlayerManager _playerMan = default!;
private const string ArrowUp = "↑";
@@ -32,10 +29,6 @@ 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()
@@ -48,11 +41,6 @@ public sealed partial class PlayerTab : Control
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
_config.OnValueChanged(CCVars.AdminPlayerTabRoleSetting, RoleSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabColorSetting, ColorSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabSymbolSetting, SymbolSettingChanged, true);
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
@@ -118,30 +106,6 @@ public sealed partial class PlayerTab : Control
#region ListContainer
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);
}
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
{
_players = players;
@@ -164,15 +128,9 @@ 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),
_playerTabColorSetting,
_playerTabRoleSetting,
_playerTabSymbolSetting);
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
button.StyleClasses.Clear();
}
/// <summary>
@@ -238,7 +196,7 @@ public sealed partial class PlayerTab : Control
Header.Username => Compare(x.Username, y.Username),
Header.Character => Compare(x.CharacterName, y.CharacterName),
Header.Job => Compare(x.StartingJob, y.StartingJob),
Header.RoleType => y.SortWeight - x.SortWeight,
Header.Antagonist => x.Antag.CompareTo(y.Antag),
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
_ => 1
};

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