Compare commits

..

12 Commits

Author SHA1 Message Date
Morbo
11c5ecf880 Move Icarus to dir & use timing instead of accum 2023-01-08 00:28:16 +03:00
Morbo
9c6a393617 Why i did this 2023-01-08 00:28:15 +03:00
Morbo
eba81123e7 Squashed commit of Icarus
commit e3784bc237
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 11 16:41:35 2022 +0300

    Complete spawn beam command

commit acc2fd5a55
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 11 02:15:30 2022 +0300

    Start creating spawn beam command

commit 179116cc22
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jul 10 01:41:20 2022 +0300

    Update UI button & add timer and cooldown

commit d07d294b91
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:32:46 2022 +0300

    Remove fire verb from terminal

commit f6e950cbf7
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:32:09 2022 +0300

    Update locale keys

commit 4410380abc
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:30:05 2022 +0300

    Fix locale

commit ddd080f5f8
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:29:10 2022 +0300

    Add simple terminal UI for firing

commit 925ec94daf
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 18:03:32 2022 +0300

    Dont call Ignite with visual update at every tick

commit 9e868145ff
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 17:55:31 2022 +0300

    Correct beam spawning directed in center of station

commit 1e9966e946
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 20:57:19 2022 +0300

    Start work on beam spawn & launch direction

commit a3dab14c6c
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 13:48:02 2022 +0300

    Rename stuff & add move separate launch method

commit 586cf26f0f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 05:16:36 2022 +0300

    Add arson radius for beam

commit 9b76fc7d74
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 03:02:12 2022 +0300

    Use AllMask for fixture beam

commit 24b3223403
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 02:55:35 2022 +0300

    Use ambient component for loop sound

commit a477e2f64f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 02:54:57 2022 +0300

    Combine sun ray sprites in one

commit dc2f707f12
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jul 6 04:09:42 2022 +0300

    Add states & preparing phase

commit 888f5714dc
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jul 6 00:35:05 2022 +0300

    Tweak beam component

commit 4793eaa494
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jul 6 00:34:35 2022 +0300

    Copy singularity destroy logic to beam

commit 9f0a8ca631
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:37:49 2022 +0300

    Fix terminal parent

commit bdc027444f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:37:34 2022 +0300

    No rotation for beam

commit 54834dbb07
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:18:21 2022 +0300

    Remove IcarusBeam comments

commit 8941193a0f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:16:03 2022 +0300

    Terminal spawn IcarusBeam

commit 993b4315bb
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:12:58 2022 +0300

    Revert "Concatenate images to one"

    This reverts commit acfe951a0c.

commit c3d4e811db
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:12:49 2022 +0300

    Add loop sound & destroying all

commit acfe951a0c
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jul 3 16:47:21 2022 +0300

    Concatenate images to one

commit c0794feada
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jul 3 16:40:03 2022 +0300

    Basic Icarus beam

commit f6598c255b
Merge: f570a9de6c c3a759f848
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jun 29 16:50:41 2022 +0300

    Merge branch 'master' into icarus

commit f570a9de6c
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 16:52:33 2022 +0300

    Basic fire system

commit 4591833d81
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 07:16:52 2022 +0300

    Add announcements & activation verb

commit a99c5c3488
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 06:39:24 2022 +0300

    Move system to server and add basic fire logic

commit 6df2a1a571
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 04:39:30 2022 +0300

    Use ItemSlots component for simplicity

commit ab44d02630
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 03:35:02 2022 +0300

    Add basic key insert to slot

commit dafe921ac4
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jun 25 17:11:41 2022 +0300

    Add sounds

commit 445a0d544f
Merge: 9864bae192 937ee3da02
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 06:33:45 2022 +0300

    Merge branch 'master' into icarus

commit 9864bae192
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 04:33:24 2022 +0300

    Add basic server icarus components

commit 184bd103b0
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 03:39:24 2022 +0300

    Add to machine meta

commit a88eb38484
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 03:39:13 2022 +0300

    Update terminal sprites

commit d88fe1c1c4
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 03:28:55 2022 +0300

    Add computer goldeneye sprites to machines

commit 81fb2c02a2
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Thu Jun 23 16:04:49 2022 +0300

    Add sprites
2023-01-07 17:27:46 +03:00
Morbo
d6afbb30bb Update todo 2023-01-07 06:15:01 +03:00
Morbo
8434c9cd9f Use custom starting gear 2023-01-07 06:14:10 +03:00
Morbo
335ad04647 Add gaiter mask 2023-01-07 06:13:42 +03:00
Morbo
1521dff5e3 Add tactical suit 2023-01-07 05:55:09 +03:00
Morbo
b6fc63a61d Use custom map 2023-01-07 05:31:57 +03:00
Morbo
bd18265088 Add custom spawnpoint 2023-01-07 05:08:37 +03:00
Morbo
da7170e600 Add todos 2023-01-07 04:58:42 +03:00
Morbo
e9b053c856 Add round end screen info 2023-01-07 04:55:07 +03:00
Morbo
d5afa731cf Assault Ops gamemode barebones 2023-01-07 04:41:31 +03:00
40589 changed files with 2532357 additions and 6945334 deletions

View File

@@ -1,5 +1,4 @@
root = true
[*]
charset = utf-8
@@ -13,7 +12,6 @@ tab_width = 4
#end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120
#### .NET Coding Conventions ####
@@ -77,8 +75,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,10 +125,9 @@ 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
csharp_space_after_cast = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
@@ -201,7 +196,7 @@ csharp_preserve_single_line_blocks = true
#dotnet_naming_style.begins_with_i.word_separator =
#dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_diagnostic.ide0055.severity = warning
dotnet_naming_rule.constants_rule.severity = warning
dotnet_naming_rule.constants_rule.style = upper_camel_case_style
@@ -282,7 +277,7 @@ dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case
dotnet_naming_style.t_upper_camel_case_style.required_prefix = T
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.constants_symbols.applicable_kinds = field
dotnet_naming_symbols.constants_symbols.required_modifiers = const
@@ -321,41 +316,26 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
dotnet_naming_symbols.property_symbols.applicable_accessibilities = *
dotnet_naming_symbols.property_symbols.applicable_kinds = property
dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly
dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static,readonly
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = *
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace, class, struct, enum, delegate
dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace,class,struct,enum,delegate
dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
# ReSharper properties
resharper_braces_for_ifelse = required_for_multiline
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_csharp_wrap_parameters_style = chop_if_long
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}]
[*.{csproj,xml,yml,dll.config,msbuildproj,targets}]
indent_size = 2
[nuget.config]
indent_size = 2
[{*.yaml,*.yml}]
ij_yaml_indent_sequence_value = false

5
.envrc
View File

@@ -1,5 +0,0 @@
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="
fi
use flake

9
.github/CODEOWNERS vendored
View File

@@ -1,11 +1,4 @@
# Last match in file takes precedence.
# Ping for all PRs
* @Morb0 @JerryImMouse @DIMMoon1
# Maps
/Resources/Prototypes/Maps/** @Morb0 @DIMMoon1 @kvant8
/Resources/Maps/** @Morb0 @DIMMoon1 @kvant8
# Sprites
/Resources/Textures/** @Morb0 @DIMMoon1 @SonicHDC
* @Morb0

View File

@@ -1,41 +0,0 @@
name: Report an issue
description: For issues that are not related to any other issue form.
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue. This template is intended for any issues that do not fit other issue templates.
For feedback or help running Space Station 14, please join our [Discord](https://discord.gg/rGvu9hKffJ).
- type: input
attributes:
label: What version did the issue occur in?
description: You can find the version by opening the changelog in-game and looking at the bottom right corner of the changelog window.
placeholder: wizards/aa1337b
validations:
required: false
- type: textarea
attributes:
label: Description
description: |
A clear and concise description of what the issue is.
If the issue is visual in nature, consider posting a screenshot.
validations:
required: true
- type: textarea
attributes:
label: Reproduction
description: List the steps required to reproduce the issue.
placeholder: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: Additional Context
description: Add any other additional context about the issue here.
validations:
required: false

View File

@@ -1,48 +0,0 @@
name: Report an mapping issue
description: For issues regarding mapping.
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue. This template is intended for any issues related to mapping.
For feedback or help running Space Station 14, please join our [Discord](https://discord.gg/rGvu9hKffJ).
- type: input
attributes:
label: What version did the issue occur in?
description: You can find the version by opening the changelog in-game and looking at the bottom right corner of the changelog window.
placeholder: wizards/aa1337b
validations:
required: false
- type: input
attributes:
label: On what station, shuttle, or grid did the issue occur on?
description: The name of the station, shuttle, or grid. If you do not know the name, try to describe it.
placeholder: Bagel
validations:
required: true
- type: textarea
attributes:
label: Description
description: |
A clear and concise description of what the issue is.
If the issue is visual in nature, consider posting a screenshot.
validations:
required: true
- type: textarea
attributes:
label: Reproduction
description: List the steps required to reproduce the issue.
placeholder: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: Additional Context
description: Add any other additional context about the issue here.
validations:
required: false

View File

@@ -1,4 +1,3 @@
blank_issues_enabled: true
contact_links:
- name: Предложение
url: https://discord.station14.ru

View File

@@ -1,35 +1,33 @@
<!-- Рекомендации: https://docs.spacestation14.io/en/getting-started/pr-guideline -->
<!-- ЭТО ШАБЛОН ВАШЕГО PULL REQUEST. Текст между стрелками - это комментарии - они не будут видны в PR. -->
## Описание PR
<!-- Что вы изменили? -->
<!-- Ниже опишите ваш Pull Request. Что он изменяет? На что еще это может повлиять? Постарайтесь описать все внесённые вами изменения! -->
## Почему / Баланс
<!-- Обсудите, как это повлияет на баланс игры или объясните, почему это было изменено. Укажите ссылки на соответствующие обсуждения или issue. -->
**Скриншоты**
<!-- Если приемлемо, добавьте скриншоты для демонстрации вашего PR. Если ваш PR представляет собой визуальное изменение, добавьте
скриншоты, иначе он может быть закрыт. -->
## Технические детали
<!-- Краткое описание изменений в коде для облегчения проверки. -->
**Проверки**
<!-- Выполнение всех следующих действий, если это приемлемо для вида изменений сильно ускорит разбор вашего PR -->
- [ ] PR полностью завершён и мне не нужна помощь чтобы его закончить.
- [ ] Я внимательно просмотрел все свои изменения и багов в них не нашёл.
- [ ] Я запускал локальный сервер со своими изменениями и всё протестировал.
## Медиа
<!-- Прикрепите медиафайлы, если PR вносит изменения в игру (одежда, предметы, механики и т.д.).
Небольшие исправления/рефакторинг освобождаются от этого требования. -->
## Требования
<!-- Подтвердите следующее, поставив X в скобках [X]: -->
- [ ] Я прочитал(а) и следую [Рекомендациям по оформлению Pull Request и Changelog](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
- [ ] Я добавил(а) медиафайлы к этому PR или он не требует демонстрации в игре.
<!-- Вы должны понимать, что несоблюдение вышеуказанного может привести к закрытию вашего PR по усмотрению сопровождающего -->
## Критические изменения
<!-- Перечислите все критические изменения, включая изменения пространств имен, публичных классов/методов/полей, переименования прототипов; и предоставьте инструкции по их исправлению. -->
**Список изменений**
<!-- Добавьте запись в Changelog, чтобы игроки знали о новых функциях или изменениях, которые могут повлиять на игровой процесс.
Убедитесь, что вы прочитали рекомендации и вынесли этот шаблон Changelog из блока комментариев, чтобы он отображался.
Changelog должен иметь символ :cl:, чтобы бот распознал изменения и добавил их в список изменений игры. -->
**Изменения**
<!--
Здесь вы можете написать список изменений, который будет автоматически добавлен в игру, когда ваш PR будет принят
Поддерживается 4 типа значков: add, remove, tweak, fix. Выбрать правильные не должно составить для вас труда.
Вы можете указать своё имя после символа :cl: именно оно будет отображаться в журнале изменений (иначе будет использоваться ваше имя на GitHub)
Например: :cl: Ian
В журнал изменений следует помещать только то, что действительно важно игрокам. Вещи вроде "Рефакторинг системы X" не должны быть в журнале изменений.
В списке изменений тип значка не является часть предложения, поэтому явно указывайте - Добавлен, Удалён, Изменён.
плохо: - add: Новый инструмент для инженеров
хорошо: - add: Добавлен новый инструмент для инженеров
-->
:cl:
- add: Добавлено веселье!
- remove: Удалено веселье!
- tweak: Изменено веселье!
- fix: Исправлено веселье!
-->
- remove: Убрано веселье!

View File

@@ -1,15 +0,0 @@
name: Cache .NET dependencies
description: Cache NuGet packages using Gitea Actions cache server
runs:
using: composite
steps:
- name: Cache NuGet packages
uses: actions/cache@v4
env:
ACTIONS_CACHE_URL: ${{ vars.ACTIONS_CACHE_URL }}
with:
path: |
~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-nuget-

30
.github/labeler.yml vendored
View File

@@ -1,31 +1,15 @@
"Changes: Sprites":
- changed-files:
- any-glob-to-any-file: '**/*.rsi/*.png'
- '**/*.rsi/*.png'
"Changes: Map":
- changed-files:
- any-glob-to-any-file:
- 'Resources/Maps/**/*.yml'
- 'Resources/Prototypes/Maps/**/*.yml'
- 'Resources/Prototypes/Corvax/Maps/**/*.yml'
- 'Resources/Maps/*.yml'
- 'Resources/Prototypes/Maps/*.yml'
"Changes: UI":
- changed-files:
- any-glob-to-any-file: '**/*.xaml*'
"Changes: Shaders":
- changed-files:
- any-glob-to-any-file: '**/*.swsl'
"Changes: Audio":
- changed-files:
- any-glob-to-any-file: '**/*.ogg'
- '**/*.xaml*'
"Changes: Localization":
- changed-files:
- any-glob-to-any-file: '**/*.ftl'
- 'Resources/Locale/**/*.ftl'
"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"
"No C#":
- all: ["!**/*.cs"]

View File

@@ -1,5 +1,4 @@
name: Benchmarks
name: Benchmarks
on:
workflow_dispatch:
schedule:
@@ -11,31 +10,38 @@ jobs:
benchmark:
name: Run Benchmarks
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v2
with:
submodules: 'recursive'
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Install Dependencies
run: dotnet restore
- name: Run Benchmarks
env:
ROBUST_BENCHMARKS_ENABLE_SQL: "1"
ROBUST_BENCHMARKS_SQL_ADDRESS: ${{ secrets.BENCHMARKS_SQL_HOST }}
ROBUST_BENCHMARKS_SQL_PORT: ${{ secrets.BENCHMARKS_SQL_PORT }}
ROBUST_BENCHMARKS_SQL_USER: ${{ secrets.BENCHMARKS_SQL_USER }}
ROBUST_BENCHMARKS_SQL_PASSWORD: ${{ secrets.BENCHMARKS_SQL_PASSWORD }}
ROBUST_BENCHMARKS_SQL_DATABASE: ${{ secrets.BENCHMARKS_SQL_DATABASE }}
GITHUB_SHA: ${{ github.sha }}
- name: Get Engine version
run: |
cd Content.Benchmarks
dotnet run --filter '*' --configuration Release
cd RobustToolbox
git fetch --depth=1
echo "::set-output name=out::$(git rev-parse HEAD)"
id: engine_version
- name: Run script on centcomm
uses: appleboy/ssh-action@master
with:
host: centcomm.spacestation14.io
username: robust-benchmark-runner
key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }}
command_timeout: 100000m
script: |
mkdir benchmark_run_content_${{ github.sha }}
cd benchmark_run_content_${{ github.sha }}
git clone https://github.com/space-wizards/space-station-14.git repo_dir --recursive
cd repo_dir
git checkout ${{ github.sha }}
cd Content.Benchmarks
dotnet restore
export ROBUST_BENCHMARKS_ENABLE_SQL=1
export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}"
export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}"
export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}"
export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD }}"
export ROBUST_BENCHMARKS_SQL_DATABASE="content_benchmarks"
export GITHUB_SHA="${{ github.sha }}"
dotnet run --filter '*' --configuration Release
cd ../../..
rm -rf benchmark_run_content_${{ github.sha }}

View File

@@ -7,10 +7,8 @@ on:
jobs:
docfx:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v2
- name: Setup submodule
run: |
git submodule update --init --recursive
@@ -21,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
with:
dotnet-version: 9.0.x
dotnet-version: 6.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

@@ -1,59 +0,0 @@
name: Build & Test Map Renderer
on:
push:
branches: [ master, staging, stable ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
jobs:
build:
if: github.actor != 'PJBot' && github.event.pull_request.draft == false
strategy:
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: |
git submodule update --init --recursive
- name: Pull engine updates
uses: space-wizards/submodule-dependency@v0.1.5
- name: Update Engine Submodules
run: |
cd RobustToolbox/
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build Content.MapRenderer --configuration Release --no-restore /m
- name: Run Map Renderer
run: dotnet run --project Content.MapRenderer Dev
ci-success:
name: Build & Test Debug
needs:
- build
runs-on: ubuntu-latest
steps:
- name: CI succeeded
run: exit 0

View File

@@ -2,32 +2,40 @@ name: Build & Test Debug
on:
push:
branches: [ master, staging, stable ]
merge_group:
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
- '**.sln'
- '**.git**'
- '**.yml'
# no docs on which one of these is supposed to work, so
# why not just do both
- 'RobustToolbox'
- 'RobustToolbox/**'
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
- '**.sln'
- '**.git**'
- '**.yml'
- 'RobustToolbox'
- 'RobustToolbox/**'
jobs:
build:
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
if: github.actor != 'PJBot'
strategy:
matrix:
os: [ubuntu-latest]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
- name: Delete Wylab override files (duplicates upstream for customization)
run: |
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
uses: actions/checkout@v2
- name: Setup Submodule
run: |
@@ -42,27 +50,21 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
dotnet-version: 6.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build --configuration DebugOpt --no-restore /m
run: dotnet build --configuration Debug --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
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
- name: Run Content.IntegrationTests
shell: pwsh
run: |
DOTNET_gcServer=1 dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
ci-success:
name: Build & Test Debug
needs:
- build
runs-on: ubuntu-latest
steps:
- name: CI succeeded
run: exit 0
$env:DOTNET_gcServer=1
dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0

View File

@@ -0,0 +1,70 @@
name: Build & Test Release
on:
push:
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
- '**.sln'
- '**.git**'
- '**.yml'
# no docs on which one of these is supposed to work, so
# why not just do both
- 'RobustToolbox'
- 'RobustToolbox/**'
pull_request:
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
- '**.sln'
- '**.git**'
- '**.yml'
- 'RobustToolbox'
- 'RobustToolbox/**'
jobs:
build:
if: github.actor != 'PJBot'
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Master
uses: actions/checkout@v2
- name: Setup Submodule
run: |
git submodule update --init --recursive
- name: Pull engine updates
uses: space-wizards/submodule-dependency@v0.1.5
- name: Update Engine Submodules
run: |
cd RobustToolbox/
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Install dependencies
run: dotnet restore
- name: Build Project
run: dotnet build --configuration Release --no-restore /p:WarningsAsErrors=nullable /m
- name: Run Content.Tests
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
- name: Run Content.IntegrationTests
shell: pwsh
run: |
$env:DOTNET_gcServer=1
dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0

View File

@@ -1,15 +0,0 @@
name: CRLF Check
on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
jobs:
build:
name: CRLF Check
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- name: Check for CRLF
run: Tools/check_crlf.py

View File

@@ -1,27 +0,0 @@
name: Close PRs on master
on:
pull_request_target:
types: [ opened, ready_for_review ]
jobs:
run:
runs-on: ubuntu-latest
if: ${{github.head_ref == 'master' || github.head_ref == 'main' || github.head_ref == 'develop'}}
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: "Благодарим вас за вклад в репозиторий Space Station 14. К сожалению, похоже, что вы отправили свой PR из master-ветки. Мы предлагаем вам следовать [нашей документации по использованию git](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html) \n\n Вы можете переместить текущую работу из master-ветки в другую ветку, выполнив команду `git branch <названиеетки>` и сбросив измененив в master-ветке."
# If you prefer to just comment on the pr and not close it, uncomment the bellow and comment the above
# - uses: actions/github-script@v7
# with:
# script: |
# github.rest.issues.createComment({
# issue_number: ${{ github.event.number }},
# owner: context.repo.owner,
# repo: context.repo.repo,
# body: "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. \n\n This pr won't be automatically closed. However, a maintainer may close it for this reason."
# })

19
.github/workflows/conflict-labeler.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Check Merge Conflicts
on:
push:
branches:
- master
pull_request_target:
jobs:
Label:
if: github.actor != 'PJBot'
runs-on: ubuntu-latest
steps:
- name: Check for Merge Conflicts
uses: ike709/actions-label-merge-conflict@9eefdd17e10566023c46d2dc6dc04fcb8ec76142
with:
dirtyLabel: "Merge Conflict"
repoToken: "${{ secrets.GITHUB_TOKEN }}"
commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."

View File

@@ -1,4 +1,4 @@
name: "Labels: Approve"
name: "Labels: Approve"
on:
pull_request_review:
@@ -11,10 +11,8 @@ jobs:
if: github.event.review.state == 'approved'
runs-on: ubuntu-latest
steps:
- name: Remove review labels
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Needs%20Review" || true
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Awaiting%20Changes" || true
- uses: actions-ecosystem/action-remove-labels@v1
with:
labels: |
Status: Needs Review
Status: Awaiting Changes

View File

@@ -1,4 +1,4 @@
name: "Labels: Changes"
name: "Labels: Changes"
on:
pull_request_review:
@@ -11,11 +11,9 @@ jobs:
if: github.event.review.state == 'changes_requested'
runs-on: ubuntu-latest
steps:
- name: Update labels
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
curl -sS -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" \
-d '{"labels":["Status: Awaiting Changes"]}' "$API"
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Needs%20Review" || true
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: "Status: Awaiting Changes"
- uses: actions-ecosystem/action-remove-labels@v1
with:
labels: "Status: Needs Review"

View File

@@ -1,66 +0,0 @@
name: Check Merge Conflicts
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review
jobs:
check-conflicts:
if: github.event.pull_request.draft == false && github.actor != 'IanComradeBot'
runs-on: ubuntu-latest
steps:
- name: Check mergeable status and label
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
run: |
PR_INDEX=${{ github.event.pull_request.number }}
REPO_OWNER=${{ github.repository_owner }}
REPO_NAME=${{ github.event.repository.name }}
API_URL="${{ github.server_url }}/api/v1"
# Get PR mergeable status
PR_DATA=$(curl -s -H "Authorization: token $API_TOKEN" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_INDEX")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable')
echo "PR #$PR_INDEX mergeable status: $MERGEABLE"
LABEL_NAME="S: Merge Conflict"
if [ "$MERGEABLE" = "false" ]; then
echo "PR has merge conflicts, adding label and comment..."
# Add label
curl -s -X POST -H "Authorization: token $API_TOKEN" \
-H "Content-Type: application/json" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels" \
-d "{\"labels\":[\"$LABEL_NAME\"]}"
# Add comment
curl -s -X POST -H "Authorization: token $API_TOKEN" \
-H "Content-Type: application/json" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/comments" \
-d '{"body":"This pull request has conflicts, please resolve those before we can evaluate the pull request."}'
echo "Label and comment added."
else
echo "PR is mergeable, no conflicts detected."
# Check if label exists and remove it
LABELS=$(curl -s -H "Authorization: token $API_TOKEN" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels")
HAS_LABEL=$(echo "$LABELS" | jq -r ".[] | select(.name == \"$LABEL_NAME\") | .id")
if [ -n "$HAS_LABEL" ]; then
echo "Removing stale conflict label..."
# URL-encode the label name (handles spaces, colons, etc.)
LABEL_NAME_ENCODED=$(echo "$LABEL_NAME" | jq -rR @uri)
curl -s -X DELETE -H "Authorization: token $API_TOKEN" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels/$LABEL_NAME_ENCODED"
echo "Conflict label removed."
fi
fi

View File

@@ -1,18 +1,16 @@
name: "Labels: Review"
name: "Labels: Review"
on:
pull_request_target:
types: [review_requested, opened]
types: [review_requested]
jobs:
add_label:
runs-on: ubuntu-latest
steps:
- name: Update labels
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
curl -sS -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" \
-d '{"labels":["S: Needs Review"]}' "$API"
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/S%3A%20Awaiting%20Changes" || true
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: "Status: Needs Review"
- uses: actions-ecosystem/action-remove-labels@v1
with:
labels: "Status: Awaiting Changes"

View File

@@ -1,15 +1,13 @@
name: "Labels: PR"
name: "Labels: PR"
on:
- pull_request_target
jobs:
labeler:
if: github.actor != 'IanComradeBot'
permissions:
contents: read
pull-requests: write
if: github.actor != 'PJBot'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/labeler@v5
- uses: actions/labeler@v3
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,20 +0,0 @@
name: "Labels: Size"
on: pull_request_target
jobs:
size-label:
runs-on: ubuntu-latest
steps:
- name: size-label
uses: "pascalgn/size-label-action@v0.5.5"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
with:
# Custom size configuration
sizes: >
{
"0": "XS",
"10": "S",
"100": "M",
"1000": "L",
"5000": "XL"
}

View File

@@ -1,22 +0,0 @@
name: "Labels: Branch stable"
on:
pull_request_target:
types:
- opened
branches:
- 'stable'
jobs:
add_label:
runs-on: ubuntu-latest
steps:
- name: Add branch label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels":["Branch: Stable"]}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"

View File

@@ -1,22 +0,0 @@
name: "Labels: Branch staging"
on:
pull_request_target:
types:
- opened
branches:
- 'staging'
jobs:
add_label:
runs-on: ubuntu-latest
steps:
- name: Add branch label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels":["Branch: Staging"]}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"

View File

@@ -1,23 +0,0 @@
name: "Labels: Untriaged"
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
jobs:
add_label:
runs-on: ubuntu-latest
steps:
- name: Add untriaged label
if: github.event.issue.labels[0] == null || github.event.pull_request.labels[0] == null
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NUMBER="${{ github.event.pull_request.number || github.event.issue.number }}"
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels":["S: Untriaged"]}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/$NUMBER/labels"

View File

@@ -1,66 +0,0 @@
name: Publish Public
concurrency:
group: publish
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * *'
jobs:
build:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
# - name: Install dependencies
# run: sudo apt-get install -y python3-paramiko python3-lxml
- uses: actions/checkout@v4.2.2
with:
submodules: 'recursive'
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.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 linux-x64 --platform osx-x64 --platform linux-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- name: Install Python dependencies
run: pip install --break-system-packages requests
- name: Publish version
run: Tools/publish_multi_request.py
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN_PUBLIC }}
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
FORK_ID: ${{ vars.FORK_ID_PUBLIC }}
# - name: Publish changelog (Discord)
# 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)
# run: Tools/actions_changelog_rss.py
# env:
# CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}

View File

@@ -1,51 +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
env:
RUNNER_TOOL_CACHE: /toolcache
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: Install Python dependencies
run: pip install --break-system-packages requests
- 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,143 +2,57 @@ name: Publish
concurrency:
group: publish
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- master
schedule:
- cron: '0 1 * * *'
- cron: '0 1 * * *'
jobs:
build:
runs-on: ubuntu-latest
env:
CDN_MANIFEST_URL: https://cdn.wylab.me/fork/wylab/manifest
RUNNER_TOOL_CACHE: /toolcache
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@v2
with:
submodules: 'recursive'
- name: Check if build already published
id: cdn-check
run: |
SHA=$(echo "$GITHUB_SHA" | tr '[:upper:]' '[:lower:]')
if curl -sSf "$CDN_MANIFEST_URL" | jq -e ".builds[\"$SHA\"]" > /dev/null 2>&1; then
echo "Build $SHA already present on CDN; skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "Build $SHA not found on CDN; continuing."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'global.json') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Cache RobustToolbox build output
uses: actions/cache@v4
with:
path: RobustToolbox/bin
key: ${{ runner.os }}-robust-${{ hashFiles('RobustToolbox/**/*.csproj', 'global.json') }}
restore-keys: |
${{ runner.os }}-robust-
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: ${{ env.SSH_KEY != '' }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
printf '%s\n' 'Host git.wylab.me' ' Hostname git.wylab.me' ' Port 22' ' User git' ' IdentityFile ~/.ssh/id_rsa' ' StrictHostKeyChecking no' ' UserKnownHostsFile /dev/null' > ~/.ssh/config
chmod 600 ~/.ssh/config
git clone git@git.wylab.me:wylab/secrets.git Secrets
[ -d Secrets/Resources/Prototypes ] && cp -R Secrets/Resources/Prototypes Resources/Prototypes/WylabSecrets
[ -d Secrets/Resources/ServerPrototypes ] && cp -R Secrets/Resources/ServerPrototypes Resources/Prototypes/WylabSecretsServer
[ -d Secrets/Resources/Locale ] && cp -R Secrets/Resources/Locale Resources/Locale/ru-RU/wylab-secrets
[ -d Secrets/Resources/Textures ] && cp -R Secrets/Resources/Textures Resources/Textures/WylabSecrets
[ -d Secrets/Resources/Audio ] && cp -R Secrets/Resources/Audio Resources/Audio/WylabSecrets
# Wylab-Secrets-End
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
dotnet-version: 6.0.100
- name: Get Engine Tag
run: |
cd RobustToolbox
git fetch --depth=1
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Install dependencies
run: dotnet restore
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Build Packaging
run: dotnet build Content.Packaging --configuration Release --no-restore /m
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- 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
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Publish version
run: Tools/publish_multi_request.py
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
GITHUB_REPOSITORY: wylab/wylab-station-14
FORK_ID: wylab
ROBUST_CDN_URL: https://cdn.wylab.me/
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Trigger Docker image rebuild
if: ${{ success() && steps.cdn-check.outputs.skip != 'true' }}
env:
DISPATCH_TOKEN: ${{ secrets.DOCKER_TRIGGER_TOKEN }}
TARGET_REPO: wylab/WS14-Docker-Linux-Server
PAYLOAD: ${{ github.sha }}
- name: Package all
run: |
if [ -z "${DISPATCH_TOKEN}" ]; then
echo "No DOCKER_TRIGGER_TOKEN configured; skipping dispatch."
exit 0
fi
curl -sSL -X POST \
-H "Authorization: token ${DISPATCH_TOKEN}" \
-H "Content-Type: application/json" \
https://git.wylab.me/api/v1/repos/${TARGET_REPO}/actions/workflows/main.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"commit\":\"${PAYLOAD}\"}}"
Tools/package_server_build.py -p win-x64 linux-x64 osx-x64 linux-arm64
Tools/package_client_build.py
# - 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: Update Build Info
run: Tools/gen_build_info.py
# - name: Publish changelog (RSS)
# continue-on-error: true
# run: Tools/actions_changelog_rss.py
# env:
# CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}
- name: Shuffle files around
run: |
mkdir "release/${{ github.sha }}"
mv release/*.zip "release/${{ github.sha }}"
- name: Upload files to mothership
uses: burnett01/rsync-deployments@5.2
with:
switches: -avzr --ignore-existing
path: "release/${{ github.sha }}"
remote_path: ${{ secrets.BUILDS_PATH }}
remote_host: ${{ secrets.BUILDS_HOST }}
remote_user: ${{ secrets.BUILDS_USERNAME }}
remote_key: ${{ secrets.BUILDS_SSH_KEY }}
- name: Update manifest JSON
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.BUILDS_HOST }}
username: ${{ secrets.BUILDS_USERNAME }}
key: ${{ secrets.BUILDS_SSH_KEY }}
script: node ~/scripts/push_to_manifest.js -fork syndicate -id ${{ github.sha }}

66
.github/workflows/rsi-diff.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Diff RSIs
on:
pull_request_target:
paths:
- '**.rsi/**.png'
jobs:
diff:
name: Diff
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get changed files
id: files
uses: Ana06/get-changed-files@v1.2
with:
format: 'space-delimited'
- name: Diff changed RSIs
id: diff
uses: space-wizards/RSIDiffBot@v1.1
with:
modified: ${{ steps.files.outputs.modified }}
removed: ${{ steps.files.outputs.removed }}
added: ${{ steps.files.outputs.added }}
basename: ${{ github.event.pull_request.base.repo.full_name }}
basesha: ${{ github.event.pull_request.base.sha }}
headname: ${{ github.event.pull_request.head.repo.full_name }}
headsha: ${{ github.event.pull_request.head.sha }}
- name: Potentially find comment
uses: peter-evans/find-comment@v1
id: fc
with:
issue-number: ${{ github.event.number }}
comment-author: 'github-actions[bot]'
body-includes: RSI Diff Bot
- name: Create comment if it doesn't exist
if: steps.fc.outputs.comment-id == ''
uses: peter-evans/create-or-update-comment@v1
with:
issue-number: ${{ github.event.number }}
body: |
${{ steps.diff.outputs.summary-details }}
- name: Update comment if it exists
if: steps.fc.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: replace
body: |
${{ steps.diff.outputs.summary-details }}
- name: Update comment to read that it has been edited
if: steps.fc.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
edit-mode: append
body: |
Edit: diff updated after ${{ github.event.pull_request.head.sha }}

View File

@@ -2,7 +2,7 @@
on:
push:
branches: [ master, staging, stable ]
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
@@ -13,10 +13,8 @@ on:
# why not just do both
- 'RobustToolbox'
- 'RobustToolbox/**'
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
@@ -28,15 +26,12 @@ on:
jobs:
build:
name: Test Packaging
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
if: github.actor != 'PJBot'
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
uses: actions/checkout@v2
- name: Setup Submodule
run: |
@@ -50,33 +45,23 @@ jobs:
cd RobustToolbox/
git submodule update --init --recursive
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: ${{ env.SSH_KEY != '' }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST git.wylab.me" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git -c submodule.Secrets.update=checkout submodule update --init
# Wylab-Secrets-End
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
dotnet-version: 6.0.x
- 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
run: |
Tools/package_server_build.py -p win-x64 linux-x64 osx-x64 linux-arm64
Tools/package_client_build.py
- name: Update Build Info
run: Tools/gen_build_info.py
- name: Shuffle files around
run: |
mkdir "release/${{ github.sha }}"
mv release/*.zip "release/${{ github.sha }}"

View File

@@ -1,54 +0,0 @@
name: Update Contrib and Patreons in credits
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
with:
ref: master
- name: Get this week's Contributors
shell: pwsh
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: Tools/dump_github_contributors.ps1 > Resources/Credits/GitHub.txt
# TODO
#- name: Get this week's Patreons
# 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.
# 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:
commit-message: Update Credits
title: Update Credits
body: This is an automated Pull Request. This PR updates the github contributors in the credits section.
author: PJBot <pieterjan.briers+bot@gmail.com>
branch: automated/credits-${{env.NOW}}

View File

@@ -3,13 +3,12 @@ name: Update Wiki
on:
workflow_dispatch:
push:
branches: [ master ]
branches: [ master, jsondump ]
paths:
- '.github/workflows/update-wiki.yml'
- 'Content.Shared/Chemistry/**.cs'
- 'Content.Server/Chemistry/**.cs'
- 'Content.Server/GuideGenerator/**.cs'
- 'Content.Server/Corvax/GuideGenerator/**.cs'
- 'Resources/Prototypes/Reagents/**.yml'
- 'Resources/Prototypes/Chemistry/**.yml'
- 'Resources/Prototypes/Recipes/Reactions/**.yml'
@@ -19,12 +18,10 @@ jobs:
update-wiki:
name: Build and Publish JSON blobs to wiki
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
uses: actions/checkout@v2
- name: Setup Submodule
run: |
@@ -39,9 +36,9 @@ jobs:
git submodule update --init --recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
dotnet-version: 6.0.100
- name: Install Dependencies
run: dotnet restore
@@ -53,42 +50,23 @@ jobs:
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
continue-on-error: true
- name: Upload chem_prototypes to wiki
- name: Upload chem_prototypes.json to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/chem_prototypes.json
edit_summary: Update chem_prototypes.json via GitHub Actions
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json
api_url: https://wiki.wylab.me/api.php
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Upload react_prototypes to wiki
- name: Upload react_prototypes.json to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/react_prototypes.json
edit_summary: Update react_prototypes.json via GitHub Actions
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json
api_url: https://wiki.wylab.me/api.php
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Upload entity_prototypes to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/entity_prototypes.json
edit_summary: Update entity_prototypes.json via GitHub Actions
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json
api_url: https://wiki.wylab.me/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Upload mealrecipes_prototypes to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/mealrecipes_prototypes.json
edit_summary: Update mealrecipes_prototypes.json via GitHub Actions
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json
api_url: https://wiki.wylab.me/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}

View File

@@ -1,58 +0,0 @@
name: Upstream Sync Auto-Merge
on:
workflow_run:
workflows: ["Build & Test"]
types: [completed]
branches: [upstream-sync]
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: Find and merge upstream-sync PR
uses: actions/github-script@v7
with:
script: |
// Find open PR from upstream-sync branch
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:upstream-sync`,
state: 'open'
});
if (prs.length === 0) {
console.log('No open upstream-sync PR found');
return;
}
const pr = prs[0];
console.log(`Found PR #${pr.number}: ${pr.title}`);
// Merge the PR using rebase
try {
await github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
merge_method: 'rebase'
});
console.log(`Successfully merged PR #${pr.number}`);
} catch (error) {
console.log(`Failed to merge: ${error.message}`);
throw error;
}
// Delete the upstream-sync branch after merge
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/upstream-sync'
});
console.log('Deleted upstream-sync branch');
} catch (error) {
console.log(`Failed to delete branch: ${error.message}`);
}

View File

@@ -1,99 +0,0 @@
name: Upstream Sync
on:
schedule:
- cron: '0 6 * * *' # Daily at 6am UTC
workflow_dispatch: # Manual trigger
jobs:
check-and-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Add upstream remote
run: |
git remote add syndicate https://github.com/space-syndicate/space-station-14.git
git fetch syndicate master
- name: Check for upstream updates
id: check
run: |
BEHIND=$(git rev-list HEAD..syndicate/master --count)
echo "behind=$BEHIND" >> $GITHUB_OUTPUT
if [ "$BEHIND" -gt 0 ]; then
echo "Upstream has $BEHIND new commits"
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "Already up to date"
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
- name: Rebase onto upstream
if: steps.check.outputs.has_updates == 'true'
id: rebase
run: |
# Create sync branch
git checkout -b upstream-sync
# Try rebase
if git rebase syndicate/master; then
echo "rebase_success=true" >> $GITHUB_OUTPUT
git push -f origin upstream-sync
else
git rebase --abort
echo "rebase_success=false" >> $GITHUB_OUTPUT
fi
- name: Create PR for CI verification
if: steps.check.outputs.has_updates == 'true' && steps.rebase.outputs.rebase_success == 'true'
uses: actions/github-script@v7
with:
script: |
const behind = '${{ steps.check.outputs.behind }}';
// Check if PR already exists
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:upstream-sync`,
state: 'open'
});
if (prs.length > 0) {
console.log('PR already exists, skipping creation');
return;
}
// Create PR
await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Upstream sync: ${behind} new commits from syndicate/master`,
head: 'upstream-sync',
base: 'master',
body: `Automatic rebase of wylab commits onto updated syndicate/master.\n\n**${behind} new upstream commits**\n\nThis PR will be auto-merged when CI passes.`
});
- name: Create issue on rebase conflict
if: steps.check.outputs.has_updates == 'true' && steps.rebase.outputs.rebase_success == 'false'
uses: actions/github-script@v7
with:
script: |
const behind = '${{ steps.check.outputs.behind }}';
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Upstream sync failed - ${behind} commits behind`,
body: `Automatic rebase onto syndicate/master failed due to conflicts.\n\nManual intervention required:\n\`\`\`bash\ngit fetch syndicate\ngit rebase syndicate/master\n# resolve conflicts\ngit push --force-with-lease origin master\n\`\`\``,
labels: ['upstream-sync', 'needs-attention']
});

View File

@@ -1,33 +1,14 @@
name: RGA schema validator
on:
push:
branches: [ master, staging, stable ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
name: YAML schema validator
on: [pull_request, push]
jobs:
yaml-schema-validation:
name: YAML RGA schema validator
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
if: github.actor != 'PJBot'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v2
- name: Setup Submodule
run: git submodule update --init
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: ${{ env.SSH_KEY != '' }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST git.wylab.me" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git -c submodule.Secrets.update=checkout submodule update --init
# Wylab-Secrets-End
- name: Pull engine updates
uses: space-wizards/submodule-dependency@v0.1.5
- uses: PaulRitter/yaml-schema-validator@v1

View File

@@ -1,61 +1,23 @@
name: RSI Validator
on:
push:
branches: [ master, staging, stable ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
branches: [ master, staging, stable ]
paths:
- '**.rsi/**'
jobs:
validate_rsis:
name: Validate RSIs
runs-on: ubuntu-latest
steps:
- name: Check for RSI changes
id: check_rsi
uses: dorny/paths-filter@v3
with:
filters: |
rsi:
- '**.rsi/**'
- name: Skip if no RSI changes
if: steps.check_rsi.outputs.rsi != 'true' && github.event_name == 'pull_request'
run: echo "No RSI files changed, skipping validation"
- uses: actions/checkout@v4.2.2
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
- uses: actions/checkout@v2
- name: Setup Submodule
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
run: git submodule update --init
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: (steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request') && env.SSH_KEY != ''
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST git.wylab.me" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git -c submodule.Secrets.update=checkout submodule update --init
# Wylab-Secrets-End
- name: Pull engine updates
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
uses: space-wizards/submodule-dependency@v0.1.5
- name: Install Python dependencies
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
run: |
python3 -m pip install --user --break-system-packages pillow jsonschema
pip3 install --ignore-installed --user pillow jsonschema
- name: Validate RSIs
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
run: |
python3 RobustToolbox/Schemas/validate_rsis.py Resources/

View File

@@ -1,33 +1,13 @@
name: Map file schema validator
on:
push:
branches: [ master, staging, stable ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
name: YAML schema validator
on: [pull_request, push]
jobs:
yaml-schema-validation:
name: YAML map schema validator
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v2
- name: Setup Submodule
run: git submodule update --init
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: ${{ env.SSH_KEY != '' }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST git.wylab.me" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git -c submodule.Secrets.update=checkout submodule update --init
# Wylab-Secrets-End
- name: Pull engine updates
uses: space-wizards/submodule-dependency@v0.1.5
- uses: PaulRitter/yaml-schema-validator@v1

View File

@@ -1,26 +1,13 @@
name: YAML Linter
on:
push:
branches: [ master, staging, stable ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
on: [pull_request, push]
jobs:
build:
name: YAML Linter
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
if: github.actor != 'PJBot'
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- uses: actions/checkout@v4.2.2
- name: Delete Wylab override files (duplicates upstream for customization)
run: |
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
- uses: actions/checkout@v2
- name: Setup submodule
run: |
git submodule update --init --recursive
@@ -31,9 +18,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
with:
dotnet-version: 9.0.x
dotnet-version: 6.0.100
- name: Install dependencies
run: dotnet restore
- name: Build

13
.gitignore vendored
View File

@@ -10,14 +10,6 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Secret
Secrets
Resources/Prototypes/CorvaxSecrets
Resources/Prototypes/CorvaxSecretsServer
Resources/Textures/CorvaxSecrets
Resources/Audio/CorvaxSecrets
Resources/Locale/ru-RU/corvax-secrets
# Build results
[Dd]ebug/
[Dd]ebugPublic/
@@ -170,7 +162,6 @@ PublishScripts/
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
.nuget/
# Microsoft Azure Build Output
csx/
@@ -310,7 +301,3 @@ Resources/MapImages
/Content.Docfx/api/
/Content.Docfx/*site
*.bak
# Direnv stuff
.direnv/

2
.gitmodules vendored
View File

@@ -1,4 +1,4 @@
[submodule "RobustToolbox"]
path = RobustToolbox
url = https://github.com/space-wizards/RobustToolbox.git
branch = master
branch = master

View File

@@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Content Server+Client" type="CompoundRunConfigurationType">
<toRun name="Content.Client" type="DotNetProject" />
<toRun name="Content.Server" type="DotNetProject" />
<method v="2" />
</configuration>
</component>
</component>

9
.vscode/launch.json vendored
View File

@@ -13,15 +13,6 @@
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Client (Compatibility renderer)",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
"args": "--cvar display.compat=true",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Server",
"type": "coreclr",

50
.vscode/tasks.json vendored
View File

@@ -10,7 +10,7 @@
"args": [
"build",
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
"/consoleloggerparameters:'ForceNoAlign;NoSummary'" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
],
"group": {
"kind": "build",
@@ -29,53 +29,9 @@
"build",
"${workspaceFolder}/Content.YAMLLinter/Content.YAMLLinter.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:'ForceNoAlign;NoSummary'"
"/consoleloggerparameters: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>
@@ -32,12 +32,6 @@ https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Tools|AnyCPU' ">
<OutputPath>bin\Tools\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DebugOpt|AnyCPU' ">
<OutputPath>bin\DebugOpt\</OutputPath>
</PropertyGroup>
<Target Name="Build">
<Exec Command="$(Python) git_helper.py" CustomErrorRegularExpression="^Error" />
</Target>

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 = "4"
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 @@
#!/usr/bin/env bash
#!/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 @@
#!/usr/bin/env bash
#!/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"

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 [support@spacestation14.com](mailto:support@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

@@ -1,11 +0,0 @@
# Space Station 14 Contributing Guidelines
Thanks for contributing to Space Station 14.
When contributing, be sure to follow our [codebase conventions](https://docs.spacestation14.com/en/general-development/codebase-info/codebase-organization.html) and [PR guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
Following these guidelines helps us increase review turnaround time, so be sure to review the linked documents in full.
The last major guidelines update was on **December 6th, 2025**.
### Why is this here?
We put this here so that GitHub will notify you when submitting a pull request that the PR guidelines have changed, if you haven't read the latest version.

View File

@@ -131,8 +131,8 @@ namespace Content.Benchmarks
public static Color InterpolateSysVector4In(in Color endPoint1, in Color endPoint2,
float lambda)
{
ref var sva = ref Unsafe.As<Color, SysVector4>(ref Unsafe.AsRef(in endPoint1));
ref var svb = ref Unsafe.As<Color, SysVector4>(ref Unsafe.AsRef(in endPoint2));
ref var sva = ref Unsafe.As<Color, SysVector4>(ref Unsafe.AsRef(endPoint1));
ref var svb = ref Unsafe.As<Color, SysVector4>(ref Unsafe.AsRef(endPoint2));
var res = SysVector4.Lerp(svb, sva, lambda);
@@ -156,8 +156,8 @@ namespace Content.Benchmarks
public static Color InterpolateSimdIn(in Color a, in Color b,
float lambda)
{
var vecA = Unsafe.As<Color, Vector128<float>>(ref Unsafe.AsRef(in a));
var vecB = Unsafe.As<Color, Vector128<float>>(ref Unsafe.AsRef(in b));
var vecA = Unsafe.As<Color, Vector128<float>>(ref Unsafe.AsRef(a));
var vecB = Unsafe.As<Color, Vector128<float>>(ref Unsafe.AsRef(b));
vecB = Fma.MultiplyAdd(Sse.Subtract(vecB, vecA), Vector128.Create(lambda), vecA);

View File

@@ -1,273 +0,0 @@
#nullable enable
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Shared.Clothing.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Item;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Benchmarks;
/// <summary>
/// Benchmarks for comparing the speed of various component fetching/lookup related methods, including directed event
/// subscriptions
/// </summary>
[Virtual]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class ComponentQueryBenchmark
{
public const string Map = "Maps/saltern.yml";
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private EntityQuery<ItemComponent> _itemQuery;
private EntityQuery<ClothingComponent> _clothingQuery;
private EntityQuery<MapComponent> _mapQuery;
private EntityUid[] _items = default!;
[GlobalSetup]
public void Setup()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(QueryBenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
_itemQuery = _entMan.GetEntityQuery<ItemComponent>();
_clothingQuery = _entMan.GetEntityQuery<ClothingComponent>();
_mapQuery = _entMan.GetEntityQuery<MapComponent>();
_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))
throw new Exception("Map load failed");
}).GetAwaiter().GetResult();
_items = new EntityUid[_entMan.Count<ItemComponent>()];
var i = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<ItemComponent>();
while (enumerator.MoveNext(out var uid, out _))
{
_items[i++] = uid;
}
}
[GlobalCleanup]
public async Task Cleanup()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
#region TryComp
/// <summary>
/// Baseline TryComp benchmark. When the benchmark was created, around 40% of the items were clothing.
/// </summary>
[Benchmark(Baseline = true)]
[BenchmarkCategory("TryComp")]
public int TryComp()
{
var hashCode = 0;
foreach (var uid in _items)
{
if (_clothingQuery.TryGetComponent(uid, out var clothing))
hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
}
return hashCode;
}
/// <summary>
/// Variant of <see cref="TryComp"/> that is meant to always fail to get a component.
/// </summary>
[Benchmark]
[BenchmarkCategory("TryComp")]
public int TryCompFail()
{
var hashCode = 0;
foreach (var uid in _items)
{
if (_mapQuery.TryGetComponent(uid, out var map))
hashCode = HashCode.Combine(hashCode, map.GetHashCode());
}
return hashCode;
}
/// <summary>
/// Variant of <see cref="TryComp"/> that is meant to always succeed getting a component.
/// </summary>
[Benchmark]
[BenchmarkCategory("TryComp")]
public int TryCompSucceed()
{
var hashCode = 0;
foreach (var uid in _items)
{
if (_itemQuery.TryGetComponent(uid, out var item))
hashCode = HashCode.Combine(hashCode, item.GetHashCode());
}
return hashCode;
}
/// <summary>
/// Variant of <see cref="TryComp"/> that uses `Resolve()` to try get the component.
/// </summary>
[Benchmark]
[BenchmarkCategory("TryComp")]
public int Resolve()
{
var hashCode = 0;
foreach (var uid in _items)
{
DoResolve(uid, ref hashCode);
}
return hashCode;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DoResolve(EntityUid uid, ref int hash, ClothingComponent? clothing = null)
{
if (_clothingQuery.Resolve(uid, ref clothing, false))
hash = HashCode.Combine(hash, clothing.GetHashCode());
}
#endregion
#region Enumeration
[Benchmark]
[BenchmarkCategory("Item Enumerator")]
public int SingleItemEnumerator()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<ItemComponent>();
while (enumerator.MoveNext(out var item))
{
hashCode = HashCode.Combine(hashCode, item.GetHashCode());
}
return hashCode;
}
[Benchmark]
[BenchmarkCategory("Item Enumerator")]
public int DoubleItemEnumerator()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<ClothingComponent, ItemComponent>();
while (enumerator.MoveNext(out _, out var item))
{
hashCode = HashCode.Combine(hashCode, item.GetHashCode());
}
return hashCode;
}
[Benchmark]
[BenchmarkCategory("Item Enumerator")]
public int TripleItemEnumerator()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<ClothingComponent, ItemComponent, TransformComponent>();
while (enumerator.MoveNext(out _, out _, out var xform))
{
hashCode = HashCode.Combine(hashCode, xform.GetHashCode());
}
return hashCode;
}
[Benchmark]
[BenchmarkCategory("Airlock Enumerator")]
public int SingleAirlockEnumerator()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<AirlockComponent>();
while (enumerator.MoveNext(out var airlock))
{
hashCode = HashCode.Combine(hashCode, airlock.GetHashCode());
}
return hashCode;
}
[Benchmark]
[BenchmarkCategory("Airlock Enumerator")]
public int DoubleAirlockEnumerator()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<AirlockComponent, DoorComponent>();
while (enumerator.MoveNext(out _, out var door))
{
hashCode = HashCode.Combine(hashCode, door.GetHashCode());
}
return hashCode;
}
[Benchmark]
[BenchmarkCategory("Airlock Enumerator")]
public int TripleAirlockEnumerator()
{
var hashCode = 0;
var enumerator = _entMan.AllEntityQueryEnumerator<AirlockComponent, DoorComponent, TransformComponent>();
while (enumerator.MoveNext(out _, out _, out var xform))
{
hashCode = HashCode.Combine(hashCode, xform.GetHashCode());
}
return hashCode;
}
#endregion
[Benchmark(Baseline = true)]
[BenchmarkCategory("Events")]
public int StructEvents()
{
var ev = new QueryBenchEvent();
foreach (var uid in _items)
{
_entMan.EventBus.RaiseLocalEvent(uid, ref ev);
}
return ev.HashCode;
}
}
[ByRefEvent]
public struct QueryBenchEvent
{
public int HashCode;
}
public sealed class QueryBenchSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ClothingComponent, QueryBenchEvent>(OnEvent);
}
private void OnEvent(EntityUid uid, ClothingComponent component, ref QueryBenchEvent args)
{
args.HashCode = HashCode.Combine(args.HashCode, component.GetHashCode());
}
}

View File

@@ -8,10 +8,10 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>12</LangVersion>
<LangVersion>11</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
@@ -25,4 +25,5 @@
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Analyzers.targets" />
</Project>

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

@@ -1,191 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Destructible;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Maps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Benchmarks;
[Virtual]
[GcServer(true)]
[MemoryDiagnoser]
public class DestructibleBenchmark
{
/// <summary>
/// Number of destructible entities per prototype to spawn with a <see cref="DestructibleComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000)]
public int EntityCount;
/// <summary>
/// Amount of blunt damage we do to each entity.
/// </summary>
[Params(10000)]
public FixedPoint2 DamageAmount;
[Params("Blunt")]
public ProtoId<DamageTypePrototype> DamageType;
private static readonly EntProtoId WindowProtoId = "Window";
private static readonly EntProtoId WallProtoId = "WallReinforced";
private static readonly EntProtoId HumanProtoId = "MobHuman";
private static readonly ProtoId<ContentTileDefinition> TileRef = "Plating";
private readonly EntProtoId[] _prototypes = [WindowProtoId, WallProtoId, HumanProtoId];
private readonly List<Entity<DamageableComponent>> _damageables = new();
private readonly List<Entity<DamageableComponent, DestructibleComponent>> _destructbiles = new();
private TestMapData _currentMapData = default!;
private DamageSpecifier _damage;
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private IPrototypeManager _protoMan = default!;
private IRobustRandom _random = default!;
private ITileDefinitionManager _tileDefMan = default!;
private DamageableSystem _damageable = default!;
private DestructibleSystem _destructible = default!;
private SharedMapSystem _map = default!;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
_entMan = server.ResolveDependency<IEntityManager>();
_protoMan = server.ResolveDependency<IPrototypeManager>();
_random = server.ResolveDependency<IRobustRandom>();
_tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
_damageable = _entMan.System<DamageableSystem>();
_destructible = _entMan.System<DestructibleSystem>();
_map = _entMan.System<SharedMapSystem>();
if (!_protoMan.Resolve(DamageType, out var type))
return;
_damage = new DamageSpecifier(type, DamageAmount);
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
}
[IterationSetup]
public void IterationSetup()
{
var plating = _tileDefMan[TileRef].TileId;
var server = _pair.Server;
_currentMapData = _pair.CreateTestMap().GetAwaiter().GetResult();
// We make a rectangular grid of destructible entities, and then damage them all simultaneously to stress test the system.
// Needed for managing the performance of destructive effects and damage application.
server.WaitPost(() =>
{
// Set up a thin line of tiles to place our objects on. They should be anchored for a "realistic" scenario...
for (var x = 0; x < EntityCount; x++)
{
for (var y = 0; y < _prototypes.Length; y++)
{
_map.SetTile(_currentMapData.Grid, _currentMapData.Grid, new Vector2i(x, y), new Tile(plating));
}
}
for (var x = 0; x < EntityCount; x++)
{
var y = 0;
foreach (var protoId in _prototypes)
{
var coords = new EntityCoordinates(_currentMapData.Grid, x + 0.5f, y + 0.5f);
_entMan.SpawnEntity(protoId, coords);
y++;
}
}
var query = _entMan.EntityQueryEnumerator<DamageableComponent, DestructibleComponent>();
_destructbiles.EnsureCapacity(EntityCount);
_damageables.EnsureCapacity(EntityCount);
while (query.MoveNext(out var uid, out var damageable, out var destructible))
{
_damageables.Add((uid, damageable));
_destructbiles.Add((uid, damageable, destructible));
}
})
.GetAwaiter()
.GetResult();
}
[Benchmark]
public async Task PerformDealDamage()
{
await _pair.Server.WaitPost(() =>
{
_damageable.ApplyDamageToAllEntities(_damageables, _damage);
});
}
[Benchmark]
public async Task PerformTestTriggers()
{
await _pair.Server.WaitPost(() =>
{
_destructible.TestAllTriggers(_destructbiles);
});
}
[Benchmark]
public async Task PerformTestBehaviors()
{
await _pair.Server.WaitPost(() =>
{
_destructible.TestAllBehaviors(_destructbiles);
});
}
[IterationCleanup]
public void IterationCleanupAsync()
{
// We need to nuke the entire map and respawn everything as some destructible effects
// spawn entities and whatnot.
_pair.Server.WaitPost(() =>
{
_map.QueueDeleteMap(_currentMapData.MapId);
})
.Wait();
// Deletion of entities is often queued (QueueDel) which must be processed by running ticks
// or else it will grow infinitely and leak memory.
_pair.Server.WaitRunTicks(2)
.GetAwaiter()
.GetResult();
_destructbiles.Clear();
_damageables.Clear();
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -1,14 +1,15 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Shared.DeviceNetwork;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Benchmarks;
@@ -17,22 +18,20 @@ namespace Content.Benchmarks;
[MemoryDiagnoser]
public class DeviceNetworkingBenchmark
{
private TestPair _pair = default!;
private PairTracker _pair = default!;
private DeviceNetworkTestSystem _deviceNetTestSystem = default!;
private DeviceNetworkSystem _deviceNetworkSystem = default!;
private EntityUid _sourceEntity;
private EntityUid _sourceWirelessEntity;
private readonly List<EntityUid> _targetEntities = new();
private readonly List<EntityUid> _targetWirelessEntities = new();
private List<EntityUid> _targetEntities = new();
private List<EntityUid> _targetWirelessEntities = new();
private NetworkPayload _payload = default!;
[TestPrototypes]
private const string Prototypes = @"
- type: entity
name: DummyNetworkDevicePrivate
id: DummyNetworkDevicePrivate
name: DummyNetworkDevice
id: DummyNetworkDevice
components:
- type: DeviceNetwork
transmitFrequency: 100
@@ -59,9 +58,8 @@ public class DeviceNetworkingBenchmark
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(DeviceNetworkingBenchmark).Assembly);
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
_pair = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
var server = _pair.Pair.Server;
await server.WaitPost(() =>
{
@@ -77,30 +75,23 @@ public class DeviceNetworkingBenchmark
["testbool"] = true
};
_sourceEntity = entityManager.SpawnEntity("DummyNetworkDevicePrivate", MapCoordinates.Nullspace);
_sourceEntity = entityManager.SpawnEntity("DummyNetworkDevice", MapCoordinates.Nullspace);
_sourceWirelessEntity = entityManager.SpawnEntity("DummyWirelessNetworkDevice", MapCoordinates.Nullspace);
for (var i = 0; i < EntityCount; i++)
{
_targetEntities.Add(entityManager.SpawnEntity("DummyNetworkDevicePrivate", MapCoordinates.Nullspace));
_targetEntities.Add(entityManager.SpawnEntity("DummyNetworkDevice", MapCoordinates.Nullspace));
_targetWirelessEntities.Add(entityManager.SpawnEntity("DummyWirelessNetworkDevice", MapCoordinates.Nullspace));
}
});
}
[GlobalCleanup]
public async Task Cleanup()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
[Benchmark(Baseline = true, Description = "Entity Events")]
public async Task EventSentBaseline()
{
var server = _pair.Server;
var server = _pair.Pair.Server;
_pair.Server.Post(() =>
_pair.Pair.Server.Post(() =>
{
foreach (var entity in _targetEntities)
{
@@ -115,9 +106,9 @@ public class DeviceNetworkingBenchmark
[Benchmark(Description = "Device Net Broadcast No Connection Checks")]
public async Task DeviceNetworkBroadcastNoConnectionChecks()
{
var server = _pair.Server;
var server = _pair.Pair.Server;
_pair.Server.Post(() =>
_pair.Pair.Server.Post(() =>
{
_deviceNetworkSystem.QueuePacket(_sourceEntity, null, _payload, 100);
});
@@ -129,9 +120,9 @@ public class DeviceNetworkingBenchmark
[Benchmark(Description = "Device Net Broadcast Wireless Connection Checks")]
public async Task DeviceNetworkBroadcastWirelessConnectionChecks()
{
var server = _pair.Server;
var server = _pair.Pair.Server;
_pair.Server.Post(() =>
_pair.Pair.Server.Post(() =>
{
_deviceNetworkSystem.QueuePacket(_sourceWirelessEntity, null, _payload, 100);
});

View File

@@ -9,7 +9,7 @@ namespace Content.Benchmarks
[Virtual]
public class DynamicTreeBenchmark
{
private static readonly Box2[] Aabbs1 =
private static readonly Box2[] _aabbs1 =
{
((Box2) default).Enlarged(1), //2x2 square
((Box2) default).Enlarged(2), //4x4 square
@@ -39,12 +39,12 @@ namespace Content.Benchmarks
public void Setup()
{
_b2Tree = new B2DynamicTree<int>();
_tree = new DynamicTree<int>((in int value) => Aabbs1[value], capacity: 16);
_tree = new DynamicTree<int>((in int value) => _aabbs1[value], capacity: 16);
for (var i = 0; i < Aabbs1.Length; i++)
for (var i = 0; i < _aabbs1.Length; i++)
{
var aabb = Aabbs1[i];
_b2Tree.CreateProxy(aabb, uint.MaxValue, i);
var aabb = _aabbs1[i];
_b2Tree.CreateProxy(aabb, i);
_tree.Add(i);
}
}

View File

@@ -189,7 +189,9 @@ namespace Content.Benchmarks
public TEntity GetEntity(TEntityUid entityUid)
{
if (!TryGetEntity(entityUid, out var entity))
throw new ArgumentException($"Failed to get entity {entityUid} from storage.");
{
throw new ArgumentException();
}
return entity;
}
@@ -198,7 +200,7 @@ namespace Content.Benchmarks
private sealed class GenEntityStorage : EntityStorage<GenEntity, GenEntityUid>
{
private (int generation, GenEntity entity)[] _entities = new (int, GenEntity)[1];
private readonly List<int> _availableSlots = new() { 0 };
private readonly List<int> _availableSlots = new() {0};
public override bool TryGetEntity(GenEntityUid entityUid, out GenEntity entity)
{

View File

@@ -11,7 +11,7 @@ using Robust.Shared.Reflection;
namespace Content.Benchmarks
{
[Virtual]
public partial class EntityManagerGetAllComponents
public class EntityManagerGetAllComponents
{
private IEntityManager _entityManager;
@@ -47,10 +47,8 @@ namespace Content.Benchmarks
var componentFactory = new Mock<IComponentFactory>();
componentFactory.Setup(p => p.GetComponent<DummyComponent>()).Returns(new DummyComponent());
componentFactory.Setup(m => m.GetIndex(typeof(DummyComponent))).Returns(CompIdx.Index<DummyComponent>());
componentFactory.Setup(p => p.GetRegistration(It.IsAny<DummyComponent>())).Returns(dummyReg);
componentFactory.Setup(p => p.GetAllRegistrations()).Returns(new[] { dummyReg });
componentFactory.Setup(p => p.GetAllRefTypes()).Returns(new[] { CompIdx.Index<DummyComponent>() });
componentFactory.Setup(p => p.GetAllRefTypes()).Returns(new[] {CompIdx.Index<DummyComponent>()});
IoCManager.RegisterInstance<IComponentFactory>(componentFactory.Object);
@@ -89,7 +87,7 @@ namespace Content.Benchmarks
return count;
}
private sealed partial class DummyComponent : Component
private sealed class DummyComponent : Component
{
}
}

View File

@@ -1,253 +0,0 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Benchmarks;
/// <summary>
/// Benchmarks the performance of different gas reactions.
/// Tests each reaction type with realistic gas mixtures to measure computational cost.
/// </summary>
[Virtual]
[GcServer(true)]
[MemoryDiagnoser]
public class GasReactionBenchmark
{
private const int Iterations = 1000;
private TestPair _pair = default!;
private AtmosphereSystem _atmosphereSystem = default!;
// Grid and tile for reactions that need a holder
private EntityUid _testGrid = default!;
private TileAtmosphere _testTile = default!;
// Reaction instances
private PlasmaFireReaction _plasmaFireReaction = default!;
private TritiumFireReaction _tritiumFireReaction = default!;
private FrezonProductionReaction _frezonProductionReaction = default!;
private FrezonCoolantReaction _frezonCoolantReaction = default!;
private AmmoniaOxygenReaction _ammoniaOxygenReaction = default!;
private N2ODecompositionReaction _n2oDecompositionReaction = default!;
private WaterVaporReaction _waterVaporReaction = default!;
// Gas mixtures for each reaction type
private GasMixture _plasmaFireMixture = default!;
private GasMixture _tritiumFireMixture = default!;
private GasMixture _frezonProductionMixture = default!;
private GasMixture _frezonCoolantMixture = default!;
private GasMixture _ammoniaOxygenMixture = default!;
private GasMixture _n2oDecompositionMixture = default!;
private GasMixture _waterVaporMixture = default!;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
// Create test map and grid
var mapData = await _pair.CreateTestMap();
_testGrid = mapData.Grid;
await server.WaitPost(() =>
{
var entMan = server.ResolveDependency<IEntityManager>();
_atmosphereSystem = entMan.System<AtmosphereSystem>();
_plasmaFireReaction = new PlasmaFireReaction();
_tritiumFireReaction = new TritiumFireReaction();
_frezonProductionReaction = new FrezonProductionReaction();
_frezonCoolantReaction = new FrezonCoolantReaction();
_ammoniaOxygenReaction = new AmmoniaOxygenReaction();
_n2oDecompositionReaction = new N2ODecompositionReaction();
_waterVaporReaction = new WaterVaporReaction();
SetupGasMixtures();
SetupTile();
});
}
private void SetupGasMixtures()
{
// Plasma Fire: Plasma + Oxygen at high temperature
// Temperature must be > PlasmaMinimumBurnTemperature for reaction to occur
_plasmaFireMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.PlasmaMinimumBurnTemperature + 100f // ~673K
};
_plasmaFireMixture.AdjustMoles(Gas.Plasma, 20f);
_plasmaFireMixture.AdjustMoles(Gas.Oxygen, 100f);
// Tritium Fire: Tritium + Oxygen at high temperature
// Temperature must be > FireMinimumTemperatureToExist for reaction to occur
_tritiumFireMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.FireMinimumTemperatureToExist + 100f // ~473K
};
_tritiumFireMixture.AdjustMoles(Gas.Tritium, 20f);
_tritiumFireMixture.AdjustMoles(Gas.Oxygen, 100f);
// Frezon Production: Oxygen + Tritium + Nitrogen catalyst
// Optimal temperature for efficiency (80% of max efficiency temp)
_frezonProductionMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.FrezonProductionMaxEfficiencyTemperature * 0.8f // ~48K
};
_frezonProductionMixture.AdjustMoles(Gas.Oxygen, 50f);
_frezonProductionMixture.AdjustMoles(Gas.Tritium, 50f);
_frezonProductionMixture.AdjustMoles(Gas.Nitrogen, 10f);
// Frezon Coolant: Frezon + Nitrogen
// Temperature must be > FrezonCoolLowerTemperature (23.15K) for reaction to occur
_frezonCoolantMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C + 50f // ~343K
};
_frezonCoolantMixture.AdjustMoles(Gas.Frezon, 30f);
_frezonCoolantMixture.AdjustMoles(Gas.Nitrogen, 100f);
// Ammonia + Oxygen reaction (concentration-dependent, no temp requirement)
_ammoniaOxygenMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C + 100f // ~393K
};
_ammoniaOxygenMixture.AdjustMoles(Gas.Ammonia, 40f);
_ammoniaOxygenMixture.AdjustMoles(Gas.Oxygen, 40f);
// N2O Decomposition (no temperature requirement, just needs N2O moles)
_n2oDecompositionMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C + 100f // ~393K
};
_n2oDecompositionMixture.AdjustMoles(Gas.NitrousOxide, 100f);
// Water Vapor - needs water vapor to condense
_waterVaporMixture = new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C
};
_waterVaporMixture.AdjustMoles(Gas.WaterVapor, 50f);
}
private void SetupTile()
{
// Create a tile atmosphere to use as holder for all reactions
var testIndices = new Vector2i(0, 0);
_testTile = new TileAtmosphere(_testGrid, testIndices, new GasMixture(Atmospherics.CellVolume)
{
Temperature = Atmospherics.T20C
});
}
private static GasMixture CloneMixture(GasMixture original)
{
return new GasMixture(original);
}
[Benchmark]
public async Task PlasmaFireReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_plasmaFireMixture);
_plasmaFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task TritiumFireReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_tritiumFireMixture);
_tritiumFireReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task FrezonProductionReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_frezonProductionMixture);
_frezonProductionReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task FrezonCoolantReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_frezonCoolantMixture);
_frezonCoolantReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task AmmoniaOxygenReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_ammoniaOxygenMixture);
_ammoniaOxygenReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task N2ODecompositionReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_n2oDecompositionMixture);
_n2oDecompositionReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[Benchmark]
public async Task WaterVaporReaction()
{
await _pair.Server.WaitPost(() =>
{
for (var i = 0; i < Iterations; i++)
{
var mixture = CloneMixture(_waterVaporMixture);
_waterVaporReaction.React(mixture, _testTile, _atmosphereSystem, 1f);
}
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -1,3 +0,0 @@
// Global usings for Content.Benchmarks
global using Robust.UnitTesting.Pool;

View File

@@ -1,79 +1,74 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Shared.Maps;
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;
[Virtual]
public class MapLoadBenchmark
{
private TestPair _pair = default!;
private PairTracker _pair = default!;
private MapLoaderSystem _mapLoader = default!;
private SharedMapSystem _mapSys = default!;
private IMapManager _mapManager = default!;
[GlobalSetup]
public void Setup()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
var server = _pair.Server;
var server = _pair.Pair.Server;
Paths = server.ResolveDependency<IPrototypeManager>()
.EnumeratePrototypes<GameMapPrototype>()
.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]
public async Task Cleanup()
{
await _pair.DisposeAsync();
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 IEnumerable<string> MapsSource { get; set; }
[ParamsSource(nameof(MapsSource))]
public string Map;
[ParamsSource(nameof(MapsSource))] public string Map;
public Dictionary<string, string> Paths;
private MapId _mapId;
public static Dictionary<string, string> Paths;
[Benchmark]
public async Task LoadMap()
{
var mapPath = new ResPath(Paths[Map]);
var server = _pair.Server;
var mapPath = Paths[Map];
var server = _pair.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;
});
}
[IterationCleanup]
public void IterationCleanup()
{
var server = _pair.Server;
server.WaitPost(() => _mapSys.DeleteMap(_mapId))
.Wait();
var server = _pair.Pair.Server;
server.WaitPost(() =>
{
_mapManager.DeleteMap(new MapId(10));
}).Wait();
}
}

View File

@@ -12,9 +12,9 @@ namespace Content.Benchmarks
{
private MemoryStream _writeStream;
private MemoryStream _readStream;
private readonly ushort _x16 = 5;
private readonly uint _x32 = 5;
private readonly ulong _x64 = 5;
private ushort _x16 = 5;
private uint _x32 = 5;
private ulong _x64 = 5;
private ushort _read16;
private uint _read32;
private ulong _read64;
@@ -24,7 +24,7 @@ namespace Content.Benchmarks
{
_writeStream = new MemoryStream(64);
_readStream = new MemoryStream();
_readStream.Write(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8 });
_readStream.Write(new byte[]{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8});
}
[Benchmark]

View File

@@ -48,7 +48,7 @@ namespace Content.Benchmarks
public void BenchReadCore()
{
_inputStream.Position = 0;
ReadPrimitiveCore(_inputStream, out _);
ReadPrimitiveCore(_inputStream, out string _);
}
[Benchmark]
@@ -62,7 +62,7 @@ namespace Content.Benchmarks
public void BenchReadUnsafe()
{
_inputStream.Position = 0;
ReadPrimitiveUnsafe(_inputStream, out _);
ReadPrimitiveUnsafe(_inputStream, out string _);
}
[Benchmark]
@@ -76,356 +76,329 @@ namespace Content.Benchmarks
public void BenchReadSlow()
{
_inputStream.Position = 0;
ReadPrimitiveSlow(_inputStream, out _);
ReadPrimitiveSlow(_inputStream, out string _);
}
public static void WritePrimitiveCore(Stream stream, string value)
{
if (value == null)
{
Primitives.WritePrimitive(stream, (uint) 0);
return;
}
public static void WritePrimitiveCore(Stream stream, string value)
{
if (value == null)
{
Primitives.WritePrimitive(stream, (uint)0);
return;
}
if (value.Length == 0)
{
Primitives.WritePrimitive(stream, (uint) 1);
return;
}
if (value.Length == 0)
{
Primitives.WritePrimitive(stream, (uint)1);
return;
}
Span<byte> buf = stackalloc byte[StringByteBufferLength];
Span<byte> buf = stackalloc byte[StringByteBufferLength];
var totalChars = value.Length;
var totalBytes = Encoding.UTF8.GetByteCount(value);
var totalChars = value.Length;
var totalBytes = Encoding.UTF8.GetByteCount(value);
Primitives.WritePrimitive(stream, (uint) totalBytes + 1);
Primitives.WritePrimitive(stream, (uint) totalChars);
Primitives.WritePrimitive(stream, (uint)totalBytes + 1);
Primitives.WritePrimitive(stream, (uint)totalChars);
var totalRead = 0;
ReadOnlySpan<char> span = value;
while (true)
{
var finalChunk = totalRead + totalChars >= totalChars;
Utf8.FromUtf16(span, buf, out var read, out var wrote, isFinalBlock: finalChunk);
stream.Write(buf[0..wrote]);
totalRead += read;
if (read >= totalChars)
{
break;
}
var totalRead = 0;
ReadOnlySpan<char> span = value;
for (;;)
{
var finalChunk = totalRead + totalChars >= totalChars;
Utf8.FromUtf16(span, buf, out var read, out var wrote, isFinalBlock: finalChunk);
stream.Write(buf.Slice(0, wrote));
totalRead += read;
if (read >= totalChars)
{
break;
}
span = span[read..];
totalChars -= read;
}
}
span = span[read..];
totalChars -= read;
}
}
public static void ReadPrimitiveCore(Stream stream, out string value)
{
Primitives.ReadPrimitive(stream, out uint totalBytes);
private static readonly SpanAction<char, (int, Stream)> _stringSpanRead = StringSpanRead;
if (totalBytes == 0)
{
value = null;
return;
}
public static void ReadPrimitiveCore(Stream stream, out string value)
{
Primitives.ReadPrimitive(stream, out uint totalBytes);
if (totalBytes == 1)
{
value = string.Empty;
return;
}
if (totalBytes == 0)
{
value = null;
return;
}
totalBytes -= 1;
if (totalBytes == 1)
{
value = string.Empty;
return;
}
totalBytes -= 1;
Primitives.ReadPrimitive(stream, out uint totalChars);
value = string.Create((int) totalChars, ((int) totalBytes, stream), StringSpanRead);
}
value = string.Create((int) totalChars, ((int) totalBytes, stream), _stringSpanRead);
}
private static void StringSpanRead(Span<char> span, (int totalBytes, Stream stream) tuple)
{
Span<byte> buf = stackalloc byte[StringByteBufferLength];
private static void StringSpanRead(Span<char> span, (int totalBytes, Stream stream) tuple)
{
Span<byte> buf = stackalloc byte[StringByteBufferLength];
// ReSharper disable VariableHidesOuterVariable
var (totalBytes, stream) = tuple;
// ReSharper restore VariableHidesOuterVariable
// ReSharper disable VariableHidesOuterVariable
var (totalBytes, stream) = tuple;
// ReSharper restore VariableHidesOuterVariable
var totalBytesRead = 0;
var totalCharsRead = 0;
var writeBufStart = 0;
var totalBytesRead = 0;
var totalCharsRead = 0;
var writeBufStart = 0;
while (totalBytesRead < totalBytes)
{
var bytesLeft = totalBytes - totalBytesRead;
var bytesReadLeft = Math.Min(buf.Length, bytesLeft);
var writeSlice = buf[writeBufStart..(bytesReadLeft - writeBufStart)];
var bytesInBuffer = stream.Read(writeSlice);
if (bytesInBuffer == 0) throw new EndOfStreamException();
while (totalBytesRead < totalBytes)
{
var bytesLeft = totalBytes - totalBytesRead;
var bytesReadLeft = Math.Min(buf.Length, bytesLeft);
var writeSlice = buf.Slice(writeBufStart, bytesReadLeft - writeBufStart);
var bytesInBuffer = stream.Read(writeSlice);
if (bytesInBuffer == 0) throw new EndOfStreamException();
var readFromStream = bytesInBuffer + writeBufStart;
var final = readFromStream == bytesLeft;
var status = Utf8.ToUtf16(buf[..readFromStream], span[totalCharsRead..], out var bytesRead, out var charsRead, isFinalBlock: final);
var readFromStream = bytesInBuffer + writeBufStart;
var final = readFromStream == bytesLeft;
var status = Utf8.ToUtf16(buf[..readFromStream], span[totalCharsRead..], out var bytesRead, out var charsRead, isFinalBlock: final);
totalBytesRead += bytesRead;
totalCharsRead += charsRead;
writeBufStart = 0;
totalBytesRead += bytesRead;
totalCharsRead += charsRead;
writeBufStart = 0;
if (status == OperationStatus.DestinationTooSmall)
{
// Malformed data?
throw new InvalidDataException();
}
if (status == OperationStatus.DestinationTooSmall)
{
// Malformed data?
throw new InvalidDataException();
}
if (status == OperationStatus.NeedMoreData)
{
// We got cut short in the middle of a multi-byte UTF-8 sequence.
// So we need to move it to the bottom of the span, then read the next bit *past* that.
// This copy should be fine because we're only ever gonna be copying up to 4 bytes
// from the end of the buffer to the start.
// So no chance of overlap.
buf[bytesRead..].CopyTo(buf);
writeBufStart = bytesReadLeft - bytesRead;
continue;
}
if (status == OperationStatus.NeedMoreData)
{
// We got cut short in the middle of a multi-byte UTF-8 sequence.
// So we need to move it to the bottom of the span, then read the next bit *past* that.
// This copy should be fine because we're only ever gonna be copying up to 4 bytes
// from the end of the buffer to the start.
// So no chance of overlap.
buf[bytesRead..].CopyTo(buf);
writeBufStart = bytesReadLeft - bytesRead;
continue;
}
Debug.Assert(status == OperationStatus.Done);
}
}
Debug.Assert(status == OperationStatus.Done);
}
}
public static void WritePrimitiveSlow(Stream stream, string value)
{
if (value == null)
{
Primitives.WritePrimitive(stream, (uint) 0);
return;
}
else if (value.Length == 0)
{
Primitives.WritePrimitive(stream, (uint) 1);
return;
}
public static void WritePrimitiveSlow(Stream stream, string value)
{
if (value == null)
{
Primitives.WritePrimitive(stream, (uint)0);
return;
}
else if (value.Length == 0)
{
Primitives.WritePrimitive(stream, (uint)1);
return;
}
var encoding = new UTF8Encoding(false, true);
var encoding = new UTF8Encoding(false, true);
var len = encoding.GetByteCount(value);
int len = encoding.GetByteCount(value);
Primitives.WritePrimitive(stream, (uint) len + 1);
Primitives.WritePrimitive(stream, (uint) value.Length);
Primitives.WritePrimitive(stream, (uint)len + 1);
Primitives.WritePrimitive(stream, (uint)value.Length);
var buf = new byte[len];
var buf = new byte[len];
encoding.GetBytes(value, 0, value.Length, buf, 0);
encoding.GetBytes(value, 0, value.Length, buf, 0);
stream.Write(buf, 0, len);
}
stream.Write(buf, 0, len);
}
public static void ReadPrimitiveSlow(Stream stream, out string value)
{
Primitives.ReadPrimitive(stream, out uint len);
public static void ReadPrimitiveSlow(Stream stream, out string value)
{
uint len;
Primitives.ReadPrimitive(stream, out len);
if (len == 0)
{
value = null;
return;
}
else if (len == 1)
{
value = string.Empty;
return;
}
if (len == 0)
{
value = null;
return;
}
else if (len == 1)
{
value = string.Empty;
return;
}
Primitives.ReadPrimitive(stream, out uint _);
uint totalChars;
Primitives.ReadPrimitive(stream, out totalChars);
len -= 1;
len -= 1;
var encoding = new UTF8Encoding(false, true);
var encoding = new UTF8Encoding(false, true);
var buf = new byte[len];
var buf = new byte[len];
var l = 0;
int l = 0;
while (l < len)
{
var r = stream.Read(buf, l, (int) len - l);
if (r == 0)
throw new EndOfStreamException();
l += r;
}
while (l < len)
{
int r = stream.Read(buf, l, (int)len - l);
if (r == 0)
throw new EndOfStreamException();
l += r;
}
value = encoding.GetString(buf);
}
value = encoding.GetString(buf);
}
private sealed class StringHelper
{
public StringHelper()
{
Encoding = new UTF8Encoding(false, true);
}
sealed class StringHelper
{
public StringHelper()
{
this.Encoding = new UTF8Encoding(false, true);
}
private Encoder _encoder;
private Decoder _decoder;
Encoder m_encoder;
Decoder m_decoder;
private byte[] _byteBuffer;
private char[] _charBuffer;
byte[] m_byteBuffer;
char[] m_charBuffer;
public UTF8Encoding Encoding { get; private set; }
public Encoder Encoder
{
get
{
_encoder ??= Encoding.GetEncoder();
return _encoder;
}
}
public Decoder Decoder
{
get
{
_decoder ??= Encoding.GetDecoder();
return _decoder;
}
}
public UTF8Encoding Encoding { get; private set; }
public Encoder Encoder { get { if (m_encoder == null) m_encoder = this.Encoding.GetEncoder(); return m_encoder; } }
public Decoder Decoder { get { if (m_decoder == null) m_decoder = this.Encoding.GetDecoder(); return m_decoder; } }
public byte[] ByteBuffer
{
get
{
_byteBuffer ??= new byte[StringByteBufferLength];
return _byteBuffer;
}
}
public char[] CharBuffer
{
get
{
_charBuffer ??= new char[StringCharBufferLength];
return _charBuffer;
}
}
}
public byte[] ByteBuffer { get { if (m_byteBuffer == null) m_byteBuffer = new byte[StringByteBufferLength]; return m_byteBuffer; } }
public char[] CharBuffer { get { if (m_charBuffer == null) m_charBuffer = new char[StringCharBufferLength]; return m_charBuffer; } }
}
[ThreadStatic]
private static StringHelper _stringHelper;
[ThreadStatic]
static StringHelper s_stringHelper;
public static unsafe void WritePrimitiveUnsafe(Stream stream, string value)
{
if (value == null)
{
Primitives.WritePrimitive(stream, (uint) 0);
return;
}
else if (value.Length == 0)
{
Primitives.WritePrimitive(stream, (uint) 1);
return;
}
public unsafe static void WritePrimitiveUnsafe(Stream stream, string value)
{
if (value == null)
{
Primitives.WritePrimitive(stream, (uint)0);
return;
}
else if (value.Length == 0)
{
Primitives.WritePrimitive(stream, (uint)1);
return;
}
var helper = _stringHelper;
if (helper == null)
_stringHelper = helper = new StringHelper();
var helper = s_stringHelper;
if (helper == null)
s_stringHelper = helper = new StringHelper();
var encoder = helper.Encoder;
var buf = helper.ByteBuffer;
var encoder = helper.Encoder;
var buf = helper.ByteBuffer;
var totalChars = value.Length;
int totalBytes;
int totalChars = value.Length;
int totalBytes;
fixed (char* ptr = value)
totalBytes = encoder.GetByteCount(ptr, totalChars, true);
fixed (char* ptr = value)
totalBytes = encoder.GetByteCount(ptr, totalChars, true);
Primitives.WritePrimitive(stream, (uint) totalBytes + 1);
Primitives.WritePrimitive(stream, (uint) totalChars);
Primitives.WritePrimitive(stream, (uint)totalBytes + 1);
Primitives.WritePrimitive(stream, (uint)totalChars);
var p = 0;
var completed = false;
int p = 0;
bool completed = false;
while (completed == false)
{
int charsConverted;
int bytesConverted;
while (completed == false)
{
int charsConverted;
int bytesConverted;
fixed (char* src = value)
fixed (byte* dst = buf)
{
encoder.Convert(src + p, totalChars - p, dst, buf.Length, true,
out charsConverted, out bytesConverted, out completed);
}
fixed (char* src = value)
fixed (byte* dst = buf)
{
encoder.Convert(src + p, totalChars - p, dst, buf.Length, true,
out charsConverted, out bytesConverted, out completed);
}
stream.Write(buf, 0, bytesConverted);
stream.Write(buf, 0, bytesConverted);
p += charsConverted;
}
}
p += charsConverted;
}
}
public static void ReadPrimitiveUnsafe(Stream stream, out string value)
{
Primitives.ReadPrimitive(stream, out uint totalBytes);
public static void ReadPrimitiveUnsafe(Stream stream, out string value)
{
uint totalBytes;
Primitives.ReadPrimitive(stream, out totalBytes);
if (totalBytes == 0)
{
value = null;
return;
}
else if (totalBytes == 1)
{
value = string.Empty;
return;
}
if (totalBytes == 0)
{
value = null;
return;
}
else if (totalBytes == 1)
{
value = string.Empty;
return;
}
totalBytes -= 1;
totalBytes -= 1;
Primitives.ReadPrimitive(stream, out uint totalChars);
uint totalChars;
Primitives.ReadPrimitive(stream, out totalChars);
var helper = _stringHelper;
if (helper == null)
_stringHelper = helper = new StringHelper();
var helper = s_stringHelper;
if (helper == null)
s_stringHelper = helper = new StringHelper();
var decoder = helper.Decoder;
var buf = helper.ByteBuffer;
char[] chars;
if (totalChars <= StringCharBufferLength)
chars = helper.CharBuffer;
else
chars = new char[totalChars];
var decoder = helper.Decoder;
var buf = helper.ByteBuffer;
char[] chars;
if (totalChars <= StringCharBufferLength)
chars = helper.CharBuffer;
else
chars = new char[totalChars];
var streamBytesLeft = (int) totalBytes;
int streamBytesLeft = (int)totalBytes;
var cp = 0;
int cp = 0;
while (streamBytesLeft > 0)
{
var bytesInBuffer = stream.Read(buf, 0, Math.Min(buf.Length, streamBytesLeft));
if (bytesInBuffer == 0)
throw new EndOfStreamException();
while (streamBytesLeft > 0)
{
int bytesInBuffer = stream.Read(buf, 0, Math.Min(buf.Length, streamBytesLeft));
if (bytesInBuffer == 0)
throw new EndOfStreamException();
streamBytesLeft -= bytesInBuffer;
var flush = streamBytesLeft == 0;
streamBytesLeft -= bytesInBuffer;
bool flush = streamBytesLeft == 0 ? true : false;
var completed = false;
bool completed = false;
var p = 0;
int p = 0;
while (completed == false)
{
decoder.Convert(
buf,
p,
bytesInBuffer - p,
chars,
cp,
(int) totalChars - cp,
flush,
out var bytesConverted,
out var charsConverted,
out completed
);
while (completed == false)
{
int charsConverted;
int bytesConverted;
p += bytesConverted;
cp += charsConverted;
}
}
decoder.Convert(buf, p, bytesInBuffer - p,
chars, cp, (int)totalChars - cp,
flush,
out bytesConverted, out charsConverted, out completed);
value = new string(chars, 0, (int) totalChars);
}
p += bytesConverted;
cp += charsConverted;
}
}
value = new string(chars, 0, (int)totalChars);
}
}
}

View File

@@ -1,7 +1,12 @@
using System;
using BenchmarkDotNet.Running;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using Content.IntegrationTests;
using Content.Server.Maps;
using Robust.Benchmarks.Configs;
using Robust.Shared.Prototypes;
namespace Content.Benchmarks
{
@@ -10,19 +15,24 @@ namespace Content.Benchmarks
public static void Main(string[] args)
{
MainAsync(args).GetAwaiter().GetResult();
}
public static async Task MainAsync(string[] args)
{
var pair = await PoolManager.GetServerClient();
var gameMaps = pair.Pair.Server.ResolveDependency<IPrototypeManager>().EnumeratePrototypes<GameMapPrototype>().ToList();
MapLoadBenchmark.MapsSource = gameMaps.Select(x => x.ID);
#if DEBUG
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

@@ -1,177 +0,0 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Shared.Warps;
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;
// This benchmark probably benefits from some accidental cache locality. I,e. the order in which entities in a pvs
// chunk are sent to players matches the order in which the entities were spawned.
//
// in a real mid-late game round, this is probably no longer the case.
// One way to somewhat offset this is to update the NetEntity assignment to assign random (but still unique) NetEntity uids to entities.
// This makes the benchmark run noticeably slower.
[Virtual]
public class PvsBenchmark
{
public const string Map = "Maps/box.yml";
[Params(1, 8, 80)]
public int PlayerCount { get; set; }
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private ICommonSession[] _players = default!;
private EntityCoordinates[] _spawns = default!;
public int _cycleOffset = 0;
private SharedTransformSystem _sys = default!;
private EntityCoordinates[] _locations = default!;
[GlobalSetup]
public void Setup()
{
#if !DEBUG
ProgramShared.PathOffset = "../../../../";
#endif
PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
_pair.Server.CfgMan.SetCVar(CVars.NetPVS, true);
_pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0);
_pair.Server.CfgMan.SetCVar(CVars.NetPvsAsync, false);
_sys = _entMan.System<SharedTransformSystem>();
SetupAsync().Wait();
}
private async Task SetupAsync()
{
// Spawn the map
_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))
throw new Exception("Map load failed");
});
// Get list of ghost warp positions
_spawns = _entMan.AllComponentsList<WarpPointComponent>()
.OrderBy(x => x.Component.Location)
.Select(x => _entMan.GetComponent<TransformComponent>(x.Uid).Coordinates)
.ToArray();
Array.Resize(ref _players, PlayerCount);
// Spawn "Players"
_players = await _pair.Server.AddDummySessions(PlayerCount);
await _pair.Server.WaitPost(() =>
{
var mind = _pair.Server.System<MindSystem>();
for (var i = 0; i < PlayerCount; i++)
{
var pos = _spawns[i % _spawns.Length];
var uid =_entMan.SpawnEntity("MobHuman", pos);
_pair.Server.ConsoleHost.ExecuteCommand($"setoutfit {_entMan.GetNetEntity(uid)} CaptainGear");
mind.ControlMob(_players[i].UserId, uid);
}
});
// Repeatedly move players around so that they "explore" the map and see lots of entities.
// This will populate their PVS data with out-of-view entities.
var rng = new Random(42);
ShufflePlayers(rng, 100);
_pair.Server.PvsTick(_players);
_pair.Server.PvsTick(_players);
var ents = _players.Select(x => x.AttachedEntity!.Value).ToArray();
_locations = ents.Select(x => _entMan.GetComponent<TransformComponent>(x).Coordinates).ToArray();
}
private void ShufflePlayers(Random rng, int count)
{
while (count > 0)
{
ShufflePlayers(rng);
count--;
}
}
private void ShufflePlayers(Random rng)
{
_pair.Server.PvsTick(_players);
var ents = _players.Select(x => x.AttachedEntity!.Value).ToArray();
var locations = ents.Select(x => _entMan.GetComponent<TransformComponent>(x).Coordinates).ToArray();
// Shuffle locations
var n = locations.Length;
while (n > 1)
{
n -= 1;
var k = rng.Next(n + 1);
(locations[k], locations[n]) = (locations[n], locations[k]);
}
_pair.Server.WaitPost(() =>
{
for (var i = 0; i < PlayerCount; i++)
{
_sys.SetCoordinates(ents[i], locations[i]);
}
}).Wait();
_pair.Server.PvsTick(_players);
}
/// <summary>
/// Basic benchmark for PVS in a static situation where nothing moves or gets dirtied..
/// This effectively provides a lower bound on "real" pvs tick time, as it is missing:
/// - PVS chunks getting dirtied and needing to be rebuilt
/// - Fetching component states for dirty components
/// - Compressing & sending network messages
/// - Sending PVS leave messages
/// </summary>
[Benchmark]
public void StaticTick()
{
_pair.Server.PvsTick(_players);
}
/// <summary>
/// Basic benchmark for PVS in a situation where players are teleporting all over the place. This isn't very
/// realistic, but unlike <see cref="StaticTick"/> this will actually also measure the speed of processing dirty
/// chunks and sending PVS leave messages.
/// </summary>
[Benchmark]
public void CycleTick()
{
_cycleOffset = (_cycleOffset + 1) % _players.Length;
_pair.Server.WaitPost(() =>
{
for (var i = 0; i < PlayerCount; i++)
{
_sys.SetCoordinates(_players[i].AttachedEntity!.Value, _locations[(i + _cycleOffset) % _players.Length]);
}
}).Wait();
_pair.Server.PvsTick(_players);
}
}

View File

@@ -1,148 +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;
var fact = _pair.Server.ResolveDependency<IComponentFactory>();
var bus = (EntityEventBus)entMan.EventBus;
_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);
_sys.NetId = fact.GetRegistration<TransformComponent>().NetID!.Value;
_sys.EvSubs = bus.GetNetCompEventHandlers<BenchSystem.BenchEv>();
})
.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 RaiseNetEvent()
{
return _sys.RaiseNetIdEvent();
}
[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 ushort NetId;
internal EntityEventBus.DirectedEventHandler?[] EvSubs = default!;
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();
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();
RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
return ev.N;
}
public int RaiseNetIdEvent()
{
// Raise a "IComponent" event using a net-id index delegate array (for PVS & client game-state events)
var ev = new BenchEv();
ref var unitEv = ref Unsafe.As<BenchEv, EntityEventBus.Unit>(ref ev);
EvSubs[NetId]?.Invoke(Ent2.Owner, Ent2.Comp, ref unitEv);
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]
[ComponentEvent(Exclusive = false)]
public struct BenchEv
{
public int N;
}
}
}

View File

@@ -1,69 +0,0 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Station.Systems;
using Content.Shared.Roles;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Benchmarks;
/// <summary>
/// This benchmarks spawns several humans, gives them captain equipment and then deletes them.
/// This measures performance for spawning, deletion, containers, and inventory code.
/// </summary>
[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 StartingGearPrototype _gear = default!;
private EntityUid _entity;
private EntityCoordinates _coords;
[Params(1, 4, 16, 64)]
public int N;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapData = await _pair.CreateTestMap();
_coords = mapData.GridCoords;
_spawnSys = server.System<StationSpawningSystem>();
_gear = server.ProtoMan.Index(CaptainStartingGear);
}
[GlobalCleanup]
public async Task Cleanup()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
[Benchmark]
public async Task SpawnDeletePlayer()
{
await _pair.Server.WaitPost(() =>
{
var server = _pair.Server;
for (var i = 0; i < N; i++)
{
_entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
_spawnSys.EquipStartingGear(_entity, _gear);
server.EntMan.DeleteEntity(_entity);
}
});
}
}

View File

@@ -0,0 +1,61 @@
using Content.Client.AME.Components;
using Robust.Client.GameObjects;
using static Content.Shared.AME.SharedAMEControllerComponent;
namespace Content.Client.AME;
public sealed class AMEControllerVisualizerSystem : VisualizerSystem<AMEControllerVisualsComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AMEControllerVisualsComponent, ComponentInit>(OnComponentInit);
}
private void OnComponentInit(EntityUid uid, AMEControllerVisualsComponent component, ComponentInit args)
{
if(TryComp<SpriteComponent>(uid, out var sprite))
{
sprite.LayerMapSet(AMEControllerVisualLayers.Display, sprite.AddLayerState("control_on"));
sprite.LayerSetVisible(AMEControllerVisualLayers.Display, false);
}
}
protected override void OnAppearanceChange(EntityUid uid, AMEControllerVisualsComponent component, ref AppearanceChangeEvent args)
{
base.OnAppearanceChange(uid, component, ref args);
if(args.Sprite != null
&& args.Component.TryGetData<string>(AMEControllerVisuals.DisplayState, out var state))
{
switch(state)
{
case "on":
args.Sprite.LayerSetState(AMEControllerVisualLayers.Display, "control_on");
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, true);
break;
case "critical":
args.Sprite.LayerSetState(AMEControllerVisualLayers.Display, "control_critical");
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, true);
break;
case "fuck":
args.Sprite.LayerSetState(AMEControllerVisualLayers.Display, "control_fuck");
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, true);
break;
case "off":
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, false);
break;
default:
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, false);
break;
}
}
}
}
public enum AMEControllerVisualLayers : byte
{
Display
}

View File

@@ -0,0 +1,70 @@
using Content.Client.AME.Components;
using Robust.Client.GameObjects;
using static Content.Shared.AME.SharedAMEShieldComponent;
namespace Content.Client.AME;
public sealed class AMEShieldingVisualizerSystem : VisualizerSystem<AMEShieldingVisualsComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AMEShieldingVisualsComponent, ComponentInit>(OnComponentInit);
}
private void OnComponentInit(EntityUid uid, AMEShieldingVisualsComponent component, ComponentInit args)
{
if(TryComp<SpriteComponent>(uid, out var sprite))
{
sprite.LayerMapSet(AMEShieldingVisualsLayer.Core, sprite.AddLayerState("core"));
sprite.LayerSetVisible(AMEShieldingVisualsLayer.Core, false);
sprite.LayerMapSet(AMEShieldingVisualsLayer.CoreState, sprite.AddLayerState("core_weak"));
sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, false);
}
}
protected override void OnAppearanceChange(EntityUid uid, AMEShieldingVisualsComponent component, ref AppearanceChangeEvent args)
{
if(args.Sprite == null)
return;
if(args.Component.TryGetData<string>(AMEShieldVisuals.Core, out var core))
{
if (core == "isCore")
{
args.Sprite.LayerSetState(AMEShieldingVisualsLayer.Core, "core");
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.Core, true);
}
else
{
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.Core, false);
}
}
if(args.Component.TryGetData<string>(AMEShieldVisuals.CoreState, out var coreState))
{
switch(coreState)
{
case "weak":
args.Sprite.LayerSetState(AMEShieldingVisualsLayer.CoreState, "core_weak");
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, true);
break;
case "strong":
args.Sprite.LayerSetState(AMEShieldingVisualsLayer.CoreState, "core_strong");
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, true);
break;
case "off":
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, false);
break;
}
}
}
}
public enum AMEShieldingVisualsLayer : byte
{
Core,
CoreState,
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Robust.Shared.GameObjects;
namespace Content.Client.AME.Components;
[RegisterComponent]
public sealed class AMEControllerVisualsComponent : Component
{
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Robust.Shared.GameObjects;
namespace Content.Client.AME.Components;
[RegisterComponent]
public sealed class AMEShieldingVisualsComponent : Component
{
}

View File

@@ -0,0 +1,57 @@
using Content.Shared.Chemistry.Dispenser;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using static Content.Shared.AME.SharedAMEControllerComponent;
namespace Content.Client.AME.UI
{
[UsedImplicitly]
public sealed class AMEControllerBoundUserInterface : BoundUserInterface
{
private AMEWindow? _window;
public AMEControllerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new AMEWindow(this);
_window.OnClose += Close;
_window.OpenCentered();
}
/// <summary>
/// Update the ui each time new state data is sent from the server.
/// </summary>
/// <param name="state">
/// Data of the <see cref="SharedReagentDispenserComponent"/> that this ui represents.
/// Sent from the server.
/// </param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
var castState = (AMEControllerBoundUserInterfaceState) state;
_window?.UpdateState(castState); //Update window state
}
public void ButtonPressed(UiButton button, int dispenseIndex = -1)
{
SendMessage(new UiButtonPressedMessage(button));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Dispose();
}
}
}
}

View File

@@ -0,0 +1,46 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'ame-window-title'}"
MinSize="250 250">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ame-window-engine-status-label'}" />
<Label Text=" " />
<Label Name="InjectionStatus" Text="{Loc 'ame-window-engine-injection-status-not-injecting-label'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="ToggleInjection"
Text="{Loc 'ame-window-toggle-injection-button'}"
StyleClasses="OpenBoth"
Disabled="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ame-window-fuel-status-label'}" />
<Label Text=" " />
<Label Name="FuelAmount" Text="{Loc 'ame-window-fuel-not-inserted-text'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="EjectButton"
Text="{Loc 'ame-window-eject-button'}"
StyleClasses="OpenBoth"
Disabled="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ame-window-injection-amount-label'}" />
<Label Text=" " />
<Label Name="InjectionAmount" Text="0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="IncreaseFuelButton"
Text="{Loc 'ame-window-increase-fuel-button'}"
StyleClasses="OpenRight" />
<Button Name="DecreaseFuelButton"
Text="{Loc 'ame-window-decrease-fuel-button'}"
StyleClasses="OpenLeft" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ame-window-core-count-label'}" />
<Label Text=" " />
<Label Name="CoreCount" Text="0" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,74 @@
using Content.Client.UserInterface;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using static Content.Shared.AME.SharedAMEControllerComponent;
namespace Content.Client.AME.UI
{
[GenerateTypedNameReferences]
public sealed partial class AMEWindow : DefaultWindow
{
public AMEWindow(AMEControllerBoundUserInterface ui)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
EjectButton.OnPressed += _ => ui.ButtonPressed(UiButton.Eject);
ToggleInjection.OnPressed += _ => ui.ButtonPressed(UiButton.ToggleInjection);
IncreaseFuelButton.OnPressed += _ => ui.ButtonPressed(UiButton.IncreaseFuel);
DecreaseFuelButton.OnPressed += _ => ui.ButtonPressed(UiButton.DecreaseFuel);
}
/// <summary>
/// Update the UI state when new state data is received from the server.
/// </summary>
/// <param name="state">State data sent by the server.</param>
public void UpdateState(BoundUserInterfaceState state)
{
var castState = (AMEControllerBoundUserInterfaceState) state;
// Disable all buttons if not powered
if (Contents.Children != null)
{
ButtonHelpers.SetButtonDisabledRecursive(Contents, !castState.HasPower);
EjectButton.Disabled = false;
}
if (!castState.HasFuelJar)
{
EjectButton.Disabled = true;
ToggleInjection.Disabled = true;
FuelAmount.Text = Loc.GetString("ame-window-fuel-not-inserted-text");
}
else
{
EjectButton.Disabled = false;
ToggleInjection.Disabled = false;
FuelAmount.Text = $"{castState.FuelAmount}";
}
if (!castState.IsMaster)
{
ToggleInjection.Disabled = true;
}
if (!castState.Injecting)
{
InjectionStatus.Text = Loc.GetString("ame-window-engine-injection-status-not-injecting-label") + " ";
}
else
{
InjectionStatus.Text = Loc.GetString("ame-window-engine-injection-status-injecting-label") + " ";
}
CoreCount.Text = $"{castState.CoreCount}";
InjectionAmount.Text = $"{castState.InjectionAmount}";
}
}
}

View File

@@ -9,20 +9,18 @@ namespace Content.Client.Access;
public sealed class AccessOverlay : Overlay
{
private const string TextFontPath = "/Fonts/NotoSans/NotoSans-Regular.ttf";
private const int TextFontSize = 12;
private readonly IEntityManager _entityManager;
private readonly SharedTransformSystem _transformSystem;
private readonly EntityLookupSystem _lookup;
private readonly Font _font;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public AccessOverlay(IEntityManager entityManager, IResourceCache resourceCache, SharedTransformSystem transformSystem)
public AccessOverlay(IEntityManager entManager, IResourceCache cache, EntityLookupSystem lookup)
{
_entityManager = entityManager;
_transformSystem = transformSystem;
_font = resourceCache.GetFont(TextFontPath, TextFontSize);
_entityManager = entManager;
_lookup = lookup;
_font = cache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12);
}
protected override void Draw(in OverlayDrawArgs args)
@@ -30,65 +28,52 @@ public sealed class AccessOverlay : Overlay
if (args.ViewportControl == null)
return;
var textBuffer = new StringBuilder();
var query = _entityManager.EntityQueryEnumerator<AccessReaderComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var accessReader, out var transform))
var readerQuery = _entityManager.GetEntityQuery<AccessReaderComponent>();
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
foreach (var ent in _lookup.GetEntitiesIntersecting(args.MapId, args.WorldAABB,
LookupFlags.Static | LookupFlags.Approximate))
{
textBuffer.Clear();
var entityName = _entityManager.ToPrettyString(uid);
textBuffer.AppendLine(entityName.Prototype);
textBuffer.Append("UID: ");
textBuffer.Append(entityName.Uid.Id);
textBuffer.Append(", NUID: ");
textBuffer.Append(entityName.Nuid.Id);
textBuffer.AppendLine();
if (!accessReader.Enabled)
if (!readerQuery.TryGetComponent(ent, out var reader) ||
!xformQuery.TryGetComponent(ent, out var xform))
{
textBuffer.AppendLine("-Disabled");
continue;
}
if (accessReader.AccessLists.Count > 0)
var text = new StringBuilder();
var index = 0;
var a = $"{_entityManager.ToPrettyString(ent)}";
text.Append(a);
foreach (var list in reader.AccessLists)
{
var groupNumber = 0;
foreach (var accessList in accessReader.AccessLists)
a = $"Tag {index}";
text.AppendLine(a);
foreach (var entry in list)
{
groupNumber++;
foreach (var entry in accessList)
{
textBuffer.Append("+Set ");
textBuffer.Append(groupNumber);
textBuffer.Append(": ");
textBuffer.Append(entry.Id);
textBuffer.AppendLine();
}
a = $"- {entry}";
text.AppendLine(a);
}
index++;
}
string textStr;
if (text.Length >= 2)
{
textStr = text.ToString();
textStr = textStr[..^2];
}
else
{
textBuffer.AppendLine("+Unrestricted");
textStr = "";
}
foreach (var key in accessReader.AccessKeys)
{
textBuffer.Append("+Key ");
textBuffer.Append(key.OriginStation);
textBuffer.Append(": ");
textBuffer.Append(key.Id);
textBuffer.AppendLine();
}
var screenPos = args.ViewportControl.WorldToScreen(xform.WorldPosition);
foreach (var tag in accessReader.DenyTags)
{
textBuffer.Append("-Tag ");
textBuffer.AppendLine(tag.Id);
}
var accessInfoText = textBuffer.ToString();
var screenPos = args.ViewportControl.WorldToScreen(_transformSystem.GetWorldPosition(transform));
args.ScreenHandle.DrawString(_font, screenPos, accessInfoText, Color.Gold);
args.ScreenHandle.DrawString(_font, screenPos, textStr, Color.Gold);
}
}
}

View File

@@ -1,11 +0,0 @@
using Content.Shared.Access.Systems;
using JetBrains.Annotations;
namespace Content.Client.Access
{
[UsedImplicitly]
public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
{
}
}

View File

@@ -2,6 +2,4 @@ using Content.Shared.Access.Systems;
namespace Content.Client.Access;
public sealed class AccessSystem : SharedAccessSystem
{
}
public sealed class AccessSystem : SharedAccessSystem {}

View File

@@ -4,20 +4,30 @@ 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 override string Command => "showaccessreaders";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
public string Command => "showaccessreaders";
public string Description => "Shows all access readers in the viewport";
public string Help => $"{Command}";
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 system = entManager.EntitySysManager.GetEntitySystem<EntityLookupSystem>();
overlay.AddOverlay(new AccessOverlay(entManager, cache, system));
shell.WriteLine($"Set access reader debug overlay to true");
}
}

View File

@@ -0,0 +1,7 @@
using Content.Shared.Access.Components;
namespace Content.Client.Access.Components;
[RegisterComponent]
[ComponentReference(typeof(SharedIdCardConsoleComponent))]
public sealed class IdCardConsoleComponent : SharedIdCardConsoleComponent {}

View File

@@ -2,4 +2,6 @@
namespace Content.Client.Access;
public sealed class IdCardSystem : SharedIdCardSystem;
public sealed class IdCardSystem : SharedIdCardSystem
{
}

View File

@@ -1,4 +0,0 @@
<GridContainer xmlns="https://spacestation14.io"
Columns="4"
HorizontalAlignment="Center">
</GridContainer>

View File

@@ -1,59 +0,0 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Content.Shared.Access;
using Content.Shared.Access.Systems;
namespace Content.Client.Access.UI;
[GenerateTypedNameReferences]
public sealed partial class AccessLevelControl : GridContainer
{
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
public readonly Dictionary<ProtoId<AccessLevelPrototype>, Button> ButtonsList = new();
public AccessLevelControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_sawmill = _logManager.GetSawmill("accesslevelcontrol");
}
public void Populate(List<ProtoId<AccessLevelPrototype>> accessLevels, IPrototypeManager prototypeManager)
{
foreach (var access in accessLevels)
{
if (!prototypeManager.TryIndex(access, out var accessLevel))
{
_sawmill.Error($"Unable to find accesslevel for {access}");
continue;
}
var newButton = new Button
{
Text = accessLevel.GetAccessLevelName(),
ToggleMode = true,
};
AddChild(newButton);
ButtonsList.Add(accessLevel.ID, newButton);
}
}
public void UpdateState(
List<ProtoId<AccessLevelPrototype>> pressedList,
List<ProtoId<AccessLevelPrototype>>? enabledList = null)
{
foreach (var (accessName, button) in ButtonsList)
{
button.Pressed = pressedList.Contains(accessName);
button.Disabled = !(enabledList?.Contains(accessName) ?? true);
}
}
}

View File

@@ -1,77 +0,0 @@
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.AccessOverriderComponent;
namespace Content.Client.Access.UI
{
public sealed class AccessOverriderBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SharedAccessOverriderSystem _accessOverriderSystem = default!;
private AccessOverriderWindow? _window;
public AccessOverriderBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_accessOverriderSystem = EntMan.System<SharedAccessOverriderSystem>();
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<AccessOverriderWindow>();
RefreshAccess();
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_window.OnSubmit += SubmitData;
_window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId));
}
public override void OnProtoReload(PrototypesReloadedEventArgs args)
{
base.OnProtoReload(args);
if (!args.WasModified<AccessLevelPrototype>())
return;
RefreshAccess();
if (State != null)
_window?.UpdateState(_prototypeManager, (AccessOverriderBoundUserInterfaceState) State);
}
private void RefreshAccess()
{
List<ProtoId<AccessLevelPrototype>> accessLevels;
if (EntMan.TryGetComponent<AccessOverriderComponent>(Owner, out var accessOverrider))
{
accessLevels = accessOverrider.AccessLevels;
accessLevels.Sort();
}
else
{
accessLevels = new List<ProtoId<AccessLevelPrototype>>();
_accessOverriderSystem.Log.Error($"No AccessOverrider component found for {EntMan.ToPrettyString(Owner)}!");
}
_window?.SetAccessLevels(_prototypeManager, accessLevels);
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
var castState = (AccessOverriderBoundUserInterfaceState) state;
_window?.UpdateState(_prototypeManager, castState);
}
public void SubmitData(List<ProtoId<AccessLevelPrototype>> newAccessList)
{
SendMessage(new WriteToTargetAccessReaderIdMessage(newAccessList));
}
}
}

View File

@@ -1,23 +0,0 @@
<DefaultWindow xmlns="https://spacestation14.io"
MinSize="650 290">
<BoxContainer Orientation="Vertical">
<GridContainer Columns="2">
<GridContainer Name="PrivilegedIdGrid" Columns="3" HorizontalExpand="True">
<Label Text="{Loc 'access-overrider-window-privileged-id'}" />
<Button Name="PrivilegedIdButton" Access="Public"/>
<Label Name="PrivilegedIdLabel" />
</GridContainer>
</GridContainer>
<Label Name="TargetNameLabel" />
<Control MinSize="0 8"/>
<GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
<!-- Access level buttons are added here by the C# code -->
</GridContainer>
<Control MinSize="0 8"/>
<Label Name="MissingPrivilegesLabel" />
<Control MinSize="0 4"/>
<Label Name="MissingPrivilegesText" />
</BoxContainer>
</DefaultWindow>

View File

@@ -1,102 +0,0 @@
using System.Linq;
using Content.Shared.Access;
using Content.Shared.Access.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.AccessOverriderComponent;
namespace Content.Client.Access.UI
{
[GenerateTypedNameReferences]
public sealed partial class AccessOverriderWindow : DefaultWindow
{
private readonly Dictionary<string, Button> _accessButtons = new();
public event Action<List<ProtoId<AccessLevelPrototype>>>? OnSubmit;
public AccessOverriderWindow()
{
RobustXamlLoader.Load(this);
}
public void SetAccessLevels(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels)
{
_accessButtons.Clear();
AccessLevelGrid.RemoveAllChildren();
foreach (var access in accessLevels)
{
if (!protoManager.Resolve(access, out var accessLevel))
{
continue;
}
var newButton = new Button
{
Text = accessLevel.GetAccessLevelName(),
ToggleMode = true,
};
AccessLevelGrid.AddChild(newButton);
_accessButtons.Add(accessLevel.ID, newButton);
newButton.OnPressed += _ =>
{
OnSubmit?.Invoke(
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
_accessButtons.Where(x => x.Value.Pressed).Select(x => new ProtoId<AccessLevelPrototype>(x.Key)).ToList());
};
}
}
public void UpdateState(IPrototypeManager protoManager, AccessOverriderBoundUserInterfaceState state)
{
PrivilegedIdGrid.Visible = state.ShowPrivilegedIdGrid;
PrivilegedIdLabel.Text = state.PrivilegedIdName;
PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
? Loc.GetString("access-overrider-window-eject-button")
: Loc.GetString("access-overrider-window-insert-button");
TargetNameLabel.Text = state.TargetLabel;
TargetNameLabel.FontColorOverride = state.TargetLabelColor;
MissingPrivilegesLabel.Text = "";
MissingPrivilegesLabel.FontColorOverride = Color.Yellow;
MissingPrivilegesText.Text = "";
MissingPrivilegesText.FontColorOverride = Color.Yellow;
if (state.MissingPrivilegesList != null && state.MissingPrivilegesList.Any())
{
var missingPrivileges = new List<string>();
foreach (string tag in state.MissingPrivilegesList)
{
var privilege = Loc.GetString(protoManager.Index<AccessLevelPrototype>(tag)?.Name ?? "generic-unknown");
missingPrivileges.Add(privilege);
}
MissingPrivilegesLabel.Text = state.ShowPrivilegedIdGrid ?
Loc.GetString("access-overrider-window-missing-privileges") :
Loc.GetString("access-overrider-window-missing-privileges-no-id");
MissingPrivilegesText.Text = string.Join(", ", missingPrivileges);
}
var interfaceEnabled = state.IsPrivilegedIdPresent && state.IsPrivilegedIdAuthorized;
foreach (var (accessName, button) in _accessButtons)
{
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;
}
}
}
}
}

View File

@@ -1,8 +1,5 @@
using Content.Shared.Access.Systems;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.Access.UI
{
@@ -13,7 +10,7 @@ namespace Content.Client.Access.UI
{
private AgentIDCardWindow? _window;
public AgentIDCardBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
public AgentIDCardBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -21,11 +18,15 @@ namespace Content.Client.Access.UI
{
base.Open();
_window = this.CreateWindow<AgentIDCardWindow>();
_window = new AgentIDCardWindow();
if (State != null)
UpdateState(State);
_window.OnNameChanged += OnNameChanged;
_window.OnJobChanged += OnJobChanged;
_window.OnJobIconChanged += OnJobIconChanged;
_window.OpenCentered();
_window.OnClose += Close;
_window.OnNameEntered += OnNameChanged;
_window.OnJobEntered += OnJobChanged;
}
private void OnNameChanged(string newName)
@@ -38,11 +39,6 @@ namespace Content.Client.Access.UI
SendMessage(new AgentIDCardJobChangedMessage(newJob));
}
public void OnJobIconChanged(ProtoId<JobIconPrototype> newJobIconId)
{
SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
@@ -55,7 +51,14 @@ namespace Content.Client.Access.UI
_window.SetCurrentName(cast.CurrentName);
_window.SetCurrentJob(cast.CurrentJob);
_window.SetAllowedIcons(cast.CurrentJobIconId);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
_window?.Dispose();
}
}
}

View File

@@ -6,9 +6,5 @@
<LineEdit Name="NameLineEdit" />
<Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
<LineEdit Name="JobLineEdit" />
<Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
<GridContainer Name="IconGrid" Columns="10">
<!-- Job icon buttons are generated in the code -->
</GridContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -1,86 +1,22 @@
using Content.Client.Stylesheets;
using Content.Shared.StatusIcon;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
using System.Linq;
namespace Content.Client.Access.UI
{
[GenerateTypedNameReferences]
public sealed partial class AgentIDCardWindow : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
private readonly SpriteSystem _spriteSystem;
public event Action<string>? OnNameEntered;
private const int JobIconColumnCount = 10;
public event Action<string>? OnNameChanged;
public event Action<string>? OnJobChanged;
public event Action<ProtoId<JobIconPrototype>>? OnJobIconChanged;
public event Action<string>? OnJobEntered;
public AgentIDCardWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text);
NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text);
JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text);
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
}
public void SetAllowedIcons(string currentJobIconId)
{
IconGrid.RemoveAllChildren();
var jobIconButtonGroup = new ButtonGroup();
var i = 0;
var icons = _prototypeManager.EnumeratePrototypes<JobIconPrototype>().Where(icon => icon.AllowSelection).ToList();
icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture));
foreach (var jobIcon in icons)
{
String styleBase = StyleClass.ButtonOpenBoth;
var modulo = i % JobIconColumnCount;
if (modulo == 0)
styleBase = StyleClass.ButtonOpenRight;
else if (modulo == JobIconColumnCount - 1)
styleBase = StyleClass.ButtonOpenLeft;
// Generate buttons
var jobIconButton = new Button
{
Access = AccessLevel.Public,
StyleClasses = { styleBase },
MaxSize = new Vector2(42, 28),
Group = jobIconButtonGroup,
Pressed = currentJobIconId == jobIcon.ID,
ToolTip = jobIcon.LocalizedJobName
};
// Generate buttons textures
var jobIconTexture = new TextureRect
{
Texture = _spriteSystem.Frame0(jobIcon.Icon),
TextureScale = new Vector2(2.5f, 2.5f),
Stretch = TextureRect.StretchMode.KeepCentered,
};
jobIconButton.AddChild(jobIconTexture);
jobIconButton.OnPressed += _ => OnJobIconChanged?.Invoke(jobIcon.ID);
IconGrid.AddChild(jobIconButton);
i++;
}
NameLineEdit.OnTextEntered += e => OnNameEntered?.Invoke(e.Text);
JobLineEdit.OnTextEntered += e => OnJobEntered?.Invoke(e.Text);
}
public void SetCurrentName(string name)

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.RemoveAllChildren();
AccessLevelChecklist.RemoveAllChildren();
// 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(StyleClass.ButtonOpenLeft);
else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1))
accessGroupButton.AddStyleClass(StyleClass.ButtonOpenRight);
else
accessGroupButton.AddStyleClass(StyleClass.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.RemoveAllChildren();
_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,55 +1,41 @@
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Client.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.Client.GameObjects;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
using static Content.Shared.Access.Components.SharedIdCardConsoleComponent;
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!;
[Dependency] private readonly IEntityManager _entityManager = default!;
public IdCardConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
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()
{
base.Open();
List<ProtoId<AccessLevelPrototype>> accessLevels;
List<string> accessLevels;
if (EntMan.TryGetComponent<IdCardConsoleComponent>(Owner, out var idCard))
if (_entityManager.TryGetComponent<IdCardConsoleComponent>(Owner.Owner, out var idCard))
{
accessLevels = idCard.AccessLevels;
accessLevels.Sort();
}
else
{
accessLevels = new List<ProtoId<AccessLevelPrototype>>();
_idCardConsoleSystem.Log.Error($"No IdCardConsole component found for {EntMan.ToPrettyString(Owner)}!");
accessLevels = new List<string>();
Logger.ErrorS(SharedIdCardConsoleSystem.Sawmill, $"No IdCardConsole component found for {_entityManager.ToPrettyString(Owner.Owner)}!");
}
_window = new IdCardConsoleWindow(this, _prototypeManager, accessLevels)
{
Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName
};
_window = new IdCardConsoleWindow(this, _prototypeManager, accessLevels) {Title = _entityManager.GetComponent<MetaDataComponent>(Owner.Owner).EntityName};
_window.CrewManifestButton.OnPressed += _ => SendMessage(new CrewManifestOpenUiMessage());
_window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId));
@@ -62,9 +48,7 @@ namespace Content.Client.Access.UI
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
if (!disposing) return;
_window?.Dispose();
}
@@ -75,13 +59,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<string> 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,7 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
MinSize="650 290">
<BoxContainer Orientation="Vertical">
<!-- Privileged and target IDs, crew manifest button. -->
<GridContainer Columns="2">
<GridContainer Columns="3" HorizontalExpand="True">
<Label Text="{Loc 'id-card-console-window-privileged-id'}" />
@@ -17,7 +16,6 @@
</BoxContainer>
</GridContainer>
<Control MinSize="0 8" />
<!-- Full name and job title editing. -->
<GridContainer Columns="3" HSeparationOverride="4">
<Label Name="FullNameLabel" Text="{Loc 'id-card-console-window-full-name-label'}" />
<LineEdit Name="FullNameLineEdit" HorizontalExpand="True" />
@@ -28,19 +26,14 @@
<Button Name="JobTitleSaveButton" Text="{Loc 'id-card-console-window-save-button'}" Disabled="True" />
</GridContainer>
<Control MinSize="0 8" />
<!-- Job preset selection, grant/revoke all access buttons. -->
<BoxContainer Margin="0 8 0 4">
<BoxContainer>
<Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
<OptionButton Name="JobPresetOptionButton" />
</BoxContainer>
<Control HorizontalExpand="True"/>
<BoxContainer>
<Button Name="SelectAllButton" Text="{Loc 'id-card-console-window-select-all-button'}" />
<Button Name="DeselectAllButton" Text="{Loc 'id-card-console-window-deselect-all-button'}" />
</BoxContainer>
</BoxContainer>
<!-- Individual access buttons -->
<Control Name="AccessLevelControlContainer" />
<GridContainer Columns="2">
<Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
<OptionButton Name="JobPresetOptionButton" />
</GridContainer>
<GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
<!-- Access level buttons are added here by the C# code -->
</GridContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -1,56 +1,42 @@
using System.Collections.Generic;
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.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
using static Content.Shared.Access.Components.SharedIdCardConsoleComponent;
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 Dictionary<string, Button> _accessButtons = new();
private readonly List<string> _jobPrototypeIds = new();
private string? _lastFullName;
private string? _lastJobTitle;
private string? _lastJobProto;
// The job that will be picked if the ID doesn't have a job on the station.
private static ProtoId<JobPrototype> _defaultJob = "Passenger";
public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager,
List<ProtoId<AccessLevelPrototype>> accessLevels)
List<string> accessLevels)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_logMill = _logManager.GetSawmill(SharedIdCardConsoleSystem.Sawmill);
_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,19 +44,17 @@ namespace Content.Client.Access.UI
FullNameSaveButton.OnPressed += _ => SubmitData();
JobTitleLineEdit.OnTextEntered += _ => SubmitData();
JobTitleLineEdit.IsValid = s => s.Length <= _maxIdJobLength;
JobTitleLineEdit.OnTextChanged += _ =>
{
JobTitleSaveButton.Disabled = JobTitleLineEdit.Text == _lastJobTitle;
};
JobTitleSaveButton.OnPressed += _ => SubmitData();
var jobs = _prototypeManager.EnumeratePrototypes<JobPrototype>().ToList();
jobs.Sort((x, y) => string.Compare(x.LocalizedName, y.LocalizedName, StringComparison.CurrentCulture));
var jobs = _prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (!job.OverrideConsoleVisibility.GetValueOrDefault(job.SetPreference))
if (!job.SetPreference)
{
continue;
}
@@ -79,35 +63,43 @@ namespace Content.Client.Access.UI
JobPresetOptionButton.AddItem(Loc.GetString(job.Name), _jobPrototypeIds.Count - 1);
}
SelectAllButton.OnPressed += _ =>
{
SetAllAccess(true);
SubmitData();
};
DeselectAllButton.OnPressed += _ =>
{
SetAllAccess(false);
SubmitData();
};
JobPresetOptionButton.OnItemSelected += SelectJobPreset;
_accessButtons.Populate(accessLevels, prototypeManager);
AccessLevelControlContainer.AddChild(_accessButtons);
foreach (var (id, button) in _accessButtons.ButtonsList)
foreach (var access in accessLevels)
{
button.OnPressed += _ => SubmitData();
if (!prototypeManager.TryIndex<AccessLevelPrototype>(access, out var accessLevel))
{
Logger.ErrorS(SharedIdCardConsoleSystem.Sawmill, $"Unable to find accesslevel for {access}");
continue;
}
var newButton = new Button
{
Text = GetAccessLevelName(accessLevel),
ToggleMode = true,
};
AccessLevelGrid.AddChild(newButton);
_accessButtons.Add(accessLevel.ID, newButton);
newButton.OnPressed += _ => SubmitData();
}
}
/// <param name="enabled">If true, every individual access button will be pressed. If false, each will be depressed.</param>
private void SetAllAccess(bool enabled)
private static string GetAccessLevelName(AccessLevelPrototype prototype)
{
foreach (var button in _accessButtons.ButtonsList.Values)
if (prototype.Name is { } name)
return Loc.GetString(name);
return prototype.ID;
}
private void ClearAllAccess()
{
foreach (var button in _accessButtons.Values)
{
if (!button.Disabled && button.Pressed != enabled)
button.Pressed = enabled;
if (button.Pressed)
{
button.Pressed = false;
}
}
}
@@ -121,12 +113,12 @@ namespace Content.Client.Access.UI
JobTitleLineEdit.Text = Loc.GetString(job.Name);
args.Button.SelectId(args.Id);
SetAllAccess(false);
ClearAllAccess();
// this is a sussy way to do this
foreach (var access in job.Access)
{
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
if (_accessButtons.TryGetValue(access, out var button))
{
button.Pressed = true;
}
@@ -134,14 +126,14 @@ 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;
}
foreach (var access in groupPrototype.Tags)
{
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
if (_accessButtons.TryGetValue(access, out var button))
{
button.Pressed = true;
}
@@ -191,21 +183,20 @@ namespace Content.Client.Access.UI
JobPresetOptionButton.Disabled = !interfaceEnabled;
_accessButtons.UpdateState(state.TargetIdAccessList?.ToList() ??
new List<ProtoId<AccessLevelPrototype>>(),
state.AllowedModifyAccessList?.ToList() ??
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.
// For example, a new ID from a box would have no job index.
if (jobIndex < 0)
foreach (var (accessName, button) in _accessButtons)
{
jobIndex = _jobPrototypeIds.IndexOf(_defaultJob);
button.Disabled = !interfaceEnabled;
if (interfaceEnabled)
{
button.Pressed = state.TargetIdAccessList?.Contains(accessName) ?? false;
}
}
JobPresetOptionButton.SelectId(jobIndex);
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
if (jobIndex >= 0)
{
JobPresetOptionButton.SelectId(jobIndex);
}
_lastFullName = state.TargetIdFullName;
_lastJobTitle = state.TargetIdJobTitle;
@@ -222,7 +213,7 @@ namespace Content.Client.Access.UI
FullNameLineEdit.Text,
JobTitleLineEdit.Text,
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
_accessButtons.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty);
}
}

View File

@@ -1,26 +1,11 @@
using Content.Shared.Actions.Components;
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
using Content.Shared.Actions.ActionTypes;
namespace Content.Client.Actions;
/// <summary>
/// This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slot.
/// This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slow.
/// </summary>
public sealed class FillActionSlotEvent : EntityEventArgs
{
public EntityUid? Action;
public ActionType? 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,20 @@
using System.IO;
using System.Linq;
using Content.Client.Popups;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Mapping;
using Content.Shared.Maps;
using Content.Shared.Actions.ActionTypes;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Audio;
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;
@@ -26,167 +23,190 @@ namespace Content.Client.Actions
[UsedImplicitly]
public sealed class ActionsSystem : SharedActionsSystem
{
public delegate void OnActionReplaced(EntityUid actionId);
public delegate void OnActionReplaced(ActionType existing, ActionType action);
[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 MetaDataSystem _metaData = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
public event Action<EntityUid>? OnActionAdded;
public event Action<EntityUid>? OnActionRemoved;
[Dependency] private readonly PopupSystem _popupSystem = default!;
public event Action<ActionType>? ActionAdded;
public event Action<ActionType>? ActionRemoved;
public event OnActionReplaced? ActionReplaced;
public event Action? ActionsUpdated;
public event Action<ActionsComponent>? LinkActions;
public event Action? UnlinkActions;
public event Action? ClearAssignments;
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";
public ActionsComponent? PlayerActions { get; private set; }
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActionsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ActionsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<ActionComponent, AfterAutoHandleStateEvent>(OnActionAutoHandleState);
SubscribeLocalEvent<EntityTargetActionComponent, ActionTargetAttemptEvent>(OnEntityTargetAttempt);
SubscribeLocalEvent<WorldTargetActionComponent, ActionTargetAttemptEvent>(OnWorldTargetAttempt);
SubscribeLocalEvent<ActionsComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ActionsComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
}
private void OnActionAutoHandleState(Entity<ActionComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateAction(ent);
}
public override void UpdateAction(Entity<ActionComponent> ent)
{
// TODO: Decouple this.
ent.Comp.IconColor = _sharedCharges.GetCurrentCharges(ent.Owner) == 0 ? ent.Comp.DisabledIconColor : ent.Comp.OriginalIconColor;
base.UpdateAction(ent);
if (_playerManager.LocalEntity != ent.Comp.AttachedEntity)
return;
ActionsUpdated?.Invoke();
}
private void 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)
{
if (!stateEnts.Contains(act) && !IsClientSide(act))
_removed.Add(act);
}
comp.Actions.ExceptWith(_removed);
var serverActions = new SortedSet<ActionType>(state.Actions);
var removed = new List<ActionType>();
foreach (var actionId in stateEnts)
foreach (var act in component.Actions.ToList())
{
if (!actionId.IsValid())
if (act.ClientExclusive)
continue;
if (!comp.Actions.Add(actionId))
if (!serverActions.TryGetValue(act, out var serverAct))
{
component.Actions.Remove(act);
if (act.AutoRemove)
removed.Add(act);
continue;
}
if (GetAction(actionId) is {} action)
_added.Add(action);
act.CopyFrom(serverAct);
serverActions.Remove(serverAct);
}
if (_playerManager.LocalEntity != uid)
return;
var added = new List<ActionType>();
foreach (var action in _removed)
// Anything that remains is a new action
foreach (var newAct in serverActions)
{
OnActionRemoved?.Invoke(action);
// We create a new action, not just sorting a reference to the state's action.
var action = (ActionType) newAct.Clone();
component.Actions.Add(action);
added.Add(action);
}
_added.Sort(ActionComparer);
if (_playerManager.LocalPlayer?.ControlledEntity != uid)
return;
foreach (var action in _added)
foreach (var action in removed)
{
OnActionAdded?.Invoke(action);
ActionRemoved?.Invoke(action);
}
foreach (var action in added)
{
ActionAdded?.Invoke(action);
}
ActionsUpdated?.Invoke();
}
public static int ActionComparer(Entity<ActionComponent> a, Entity<ActionComponent> b)
protected override void AddActionInternal(ActionsComponent comp, ActionType action)
{
var priorityA = a.Comp?.Priority ?? 0;
var priorityB = b.Comp?.Priority ?? 0;
if (priorityA != priorityB)
return priorityA - priorityB;
// Sometimes the client receives actions from the server, before predicting that newly added components will add
// their own shared actions. Just in case those systems ever decided to directly access action properties (e.g.,
// action.Toggled), we will remove duplicates:
if (comp.Actions.TryGetValue(action, out var existing))
{
comp.Actions.Remove(existing);
ActionReplaced?.Invoke(existing, action);
}
priorityA = a.Comp?.Container?.Id ?? 0;
priorityB = b.Comp?.Container?.Id ?? 0;
return priorityA - priorityB;
comp.Actions.Add(action);
}
protected override void ActionAdded(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
public override void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
{
if (_playerManager.LocalEntity != performer.Owner)
if (!Resolve(uid, ref comp, false))
return;
OnActionAdded?.Invoke(action);
ActionsUpdated?.Invoke();
base.AddAction(uid, action, provider, comp, dirty);
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
ActionAdded?.Invoke(action);
}
protected override void ActionRemoved(Entity<ActionsComponent> performer, Entity<ActionComponent> action)
public override void RemoveActions(EntityUid uid, IEnumerable<ActionType> actions, ActionsComponent? comp = null, bool dirty = true)
{
if (_playerManager.LocalEntity != performer.Owner)
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
return;
OnActionRemoved?.Invoke(action);
ActionsUpdated?.Invoke();
if (!Resolve(uid, ref comp, false))
return;
var actionList = actions.ToList();
base.RemoveActions(uid, actionList, comp, dirty);
foreach (var act in actionList)
{
if (act.AutoRemove)
ActionRemoved?.Invoke(act);
}
}
public IEnumerable<Entity<ActionComponent>> GetClientActions()
/// <summary>
/// Execute convenience functionality for actions (pop-ups, sound, speech)
/// </summary>
protected override bool PerformBasicActions(EntityUid user, ActionType action, bool predicted)
{
if (_playerManager.LocalEntity is not { } user)
return Enumerable.Empty<Entity<ActionComponent>>();
var performedAction = action.Sound != null
|| !string.IsNullOrWhiteSpace(action.UserPopup)
|| !string.IsNullOrWhiteSpace(action.Popup);
return GetActions(user);
if (!GameTiming.IsFirstTimePredicted)
return performedAction;
if (!string.IsNullOrWhiteSpace(action.UserPopup))
{
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
? Loc.GetString(action.UserPopup)
: Loc.GetString(action.UserPopup + action.PopupToggleSuffix);
_popupSystem.PopupEntity(msg, user);
}
else if (!string.IsNullOrWhiteSpace(action.Popup))
{
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
? Loc.GetString(action.Popup)
: Loc.GetString(action.Popup + action.PopupToggleSuffix);
_popupSystem.PopupEntity(msg, user);
}
if (action.Sound != null)
SoundSystem.Play(action.Sound.GetSound(), Filter.Local(), user, action.AudioParams);
return performedAction;
}
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, LocalPlayerAttachedEvent args)
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
{
LinkAllActions(component);
}
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, LocalPlayerDetachedEvent? args = null)
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent? args = null)
{
UnlinkAllActions();
}
public void UnlinkAllActions()
{
PlayerActions = null;
UnlinkActions?.Invoke();
}
public void LinkAllActions(ActionsComponent? actions = null)
{
if (_playerManager.LocalEntity is not { } user ||
!Resolve(user, ref actions, false))
{
return;
}
var player = _playerManager.LocalPlayer?.ControlledEntity;
if (player == null || !Resolve(player.Value, ref actions))
{
return;
}
LinkActions?.Invoke(actions);
LinkActions?.Invoke(actions);
PlayerActions = actions;
}
public override void Shutdown()
@@ -195,36 +215,64 @@ namespace Content.Client.Actions
CommandBinds.Unregister<ActionsSystem>();
}
public void TriggerAction(Entity<ActionComponent> action)
public void TriggerAction(ActionType? action)
{
if (_playerManager.LocalEntity is not { } user)
if (PlayerActions == null || action == null || _playerManager.LocalPlayer?.ControlledEntity is not { Valid: true } user)
return;
// TODO: unhardcode this somehow
if (!HasComp<InstantActionComponent>(action))
if (action.Provider != null && Deleted(action.Provider))
return;
if (action.Comp.ClientExclusive)
if (action is not InstantAction instantAction)
{
PerformAction(user, action);
return;
}
if (action.ClientExclusive)
{
if (instantAction.Event != null)
instantAction.Event.Performer = user;
PerformAction(user, PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
}
else
{
var request = new RequestPerformActionEvent(GetNetEntity(action));
RaisePredictiveEvent(request);
var request = new RequestPerformActionEvent(instantAction);
EntityManager.RaisePredictiveEvent(request);
}
}
/*public void SaveActionAssignments(string path)
{
// Currently only tested with temporary innate actions (i.e., mapping actions). No guarantee it works with
// other actions. If its meant to be used for full game state saving/loading, the entity that provides
// actions needs to keep the same uid.
var sequence = new SequenceDataNode();
foreach (var (action, assigns) in Assignments.Assignments)
{
var slot = new MappingDataNode();
slot.Add("action", _serializationManager.WriteValue(action));
slot.Add("assignments", _serializationManager.WriteValue(assigns));
sequence.Add(slot);
}
using var writer = _resourceManager.UserData.OpenWriteText(new ResourcePath(path).ToRootedPath());
var stream = new YamlStream { new(sequence.ToSequenceNode()) };
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
}*/
/// <summary>
/// Load actions and their toolbar assignments from a file.
/// </summary>
public void LoadActionAssignments(string path, bool userData)
{
if (_playerManager.LocalEntity is not { } user)
if (PlayerActions == null)
return;
var file = new ResPath(path).ToRootedPath();
var file = new ResourcePath(path).ToRootedPath();
TextReader reader = userData
? _resources.UserData.OpenText(file)
: _resources.ContentFileReadText(file);
@@ -235,161 +283,45 @@ 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("assignments", out var assignmentNode))
if (!map.TryGet("action", out var actionNode))
continue;
var actionId = EntityUid.Invalid;
if (map.TryGet<ValueDataNode>("action", out var actionNode))
var action = _serialization.Read<ActionType>(actionNode, notNullableOverride: true);
if (PlayerActions.Actions.TryGetValue(action, out var existingAction))
{
var id = new EntProtoId(actionNode.Value);
actionId = Spawn(id);
}
else if (map.TryGet<ValueDataNode>("entity", out var entityNode))
{
var id = new EntProtoId(entityNode.Value);
var proto = _proto.Index(id);
actionId = Spawn(MappingEntityAction);
SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id));
SetEvent(actionId, new StartPlacementActionEvent()
{
PlacementOption = "SnapgridCenter",
EntityType = id
});
_metaData.SetEntityName(actionId, proto.Name);
}
else if (map.TryGet<ValueDataNode>("tileId", out var tileNode))
{
var id = new ProtoId<ContentTileDefinition>(tileNode.Value);
var proto = _proto.Index(id);
actionId = Spawn(MappingEntityAction);
if (proto.Sprite is {} sprite)
SetIcon(actionId, new SpriteSpecifier.Texture(sprite));
SetEvent(actionId, new StartPlacementActionEvent()
{
PlacementOption = "AlignTileAny",
TileId = id
});
_metaData.SetEntityName(actionId, Loc.GetString(proto.Name));
existingAction.CopyFrom(action);
action = existingAction;
}
else
{
Log.Error($"Mapping actions from {path} had unknown action data!");
PlayerActions.Actions.Add(action);
}
if (!map.TryGet("assignments", out var assignmentNode))
continue;
}
if (assignmentNode is SequenceDataNode sequenceAssignments)
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
foreach (var index in nodeAssignments)
{
try
{
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(sequenceAssignments, notNullableOverride: true);
foreach (var index in nodeAssignments)
{
assignments.Add(new SlotAssignment(index.Hotbar, index.Slot, actionId));
}
}
catch (Exception ex)
{
Log.Error($"Failed to parse action assignments: {ex}");
}
var assignment = new SlotAssignment(index.Hotbar, index.Slot, action);
assignments.Add(assignment);
}
AddActionDirect((user, actions), actionId);
}
AssignSlot?.Invoke(assignments);
}
private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
{
if (args.Handled)
return;
args.Handled = true;
var (uid, comp) = ent;
var action = args.Action;
var coords = args.Input.Coordinates;
var user = args.User;
if (!ValidateWorldTarget(user, coords, ent))
return;
// optionally send the clicked entity too, if it matches its whitelist etc
// this is the actual entity-world targeting magic
EntityUid? targetEnt = null;
if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
args.Input.EntityUid is { Valid: true } entityUid &&
ValidateEntityTarget(user, entityUid, (uid, entity)))
{
targetEnt = entityUid;
}
if (action.ClientExclusive)
{
// TODO: abstract away from single event or maybe just RaiseLocalEvent?
if (comp.Event is {} ev)
{
ev.Target = coords;
ev.Entity = targetEnt;
}
PerformAction((user, user.Comp), (uid, action));
}
else
RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(targetEnt), GetNetCoordinates(coords)));
args.FoundTarget = true;
}
private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (args.Input.EntityUid is not { Valid: true } entity)
return;
// let world target component handle it
var (uid, comp) = ent;
if (comp.Event is not {} ev)
{
DebugTools.Assert(HasComp<WorldTargetActionComponent>(ent), $"Action {ToPrettyString(ent)} requires WorldTargetActionComponent for entity-world targeting");
return;
}
var action = args.Action;
var user = args.User;
if (!ValidateEntityTarget(user, entity, ent))
return;
if (action.ClientExclusive)
{
ev.Target = entity;
PerformAction((user, user.Comp), (uid, action));
}
else
{
RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(entity)));
}
args.FoundTarget = true;
}
public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
public record struct SlotAssignment(byte Hotbar, byte Slot, ActionType Action);
}
}

View File

@@ -1,5 +1,9 @@
using System;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -23,10 +27,9 @@ namespace Content.Client.Actions.UI
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
{
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSystem;
_gameTiming = IoCManager.Resolve<IGameTiming>();
SetOnlyStyleClass(StyleClass.TooltipPanel);
SetOnlyStyleClass(StyleNano.StyleClassTooltipPanel);
BoxContainer vbox;
AddChild(vbox = new BoxContainer
@@ -37,7 +40,7 @@ namespace Content.Client.Actions.UI
var nameLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleClass.TooltipTitle }
StyleClasses = {StyleNano.StyleClassTooltipActionTitle}
};
nameLabel.SetMessage(name);
vbox.AddChild(nameLabel);
@@ -47,7 +50,7 @@ namespace Content.Client.Actions.UI
var description = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleClass.TooltipDesc }
StyleClasses = {StyleNano.StyleClassTooltipActionDescription}
};
description.SetMessage(desc);
vbox.AddChild(description);
@@ -56,7 +59,7 @@ namespace Content.Client.Actions.UI
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleClass.TooltipDesc },
StyleClasses = {StyleNano.StyleClassTooltipActionCooldown},
Visible = false
});
@@ -65,14 +68,11 @@ namespace Content.Client.Actions.UI
var requiresLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleClass.TooltipDesc }
StyleClasses = {StyleNano.StyleClassTooltipActionRequirements}
};
if (!FormattedMessage.TryFromMarkup("[color=#635c5c]" + requires + "[/color]", out var markup))
return;
requiresLabel.SetMessage(markup);
requiresLabel.SetMessage(FormattedMessage.FromMarkup("[color=#635c5c]" +
requires +
"[/color]"));
vbox.AddChild(requiresLabel);
}
}
@@ -90,11 +90,8 @@ namespace Content.Client.Actions.UI
if (timeLeft > TimeSpan.Zero)
{
var duration = Cooldown.Value.End - Cooldown.Value.Start;
if (!FormattedMessage.TryFromMarkup(Loc.GetString("ui-actionslot-duration", ("duration", (int)duration.TotalSeconds), ("timeLeft", (int)timeLeft.TotalSeconds + 1)), out var markup))
return;
_cooldownLabel.SetMessage(markup);
_cooldownLabel.SetMessage(FormattedMessage.FromMarkup(
$"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 1} sec remaining)[/color]"));
_cooldownLabel.Visible = true;
}
else

View File

@@ -1,282 +1,71 @@
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.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.Administration;
internal sealed class AdminNameOverlay : Overlay
namespace Content.Client.Administration
{
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
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)
internal sealed class AdminNameOverlay : Overlay
{
_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");
private readonly AdminSystem _system;
private readonly IEntityManager _entityManager;
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly Font _font;
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;
}
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
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)
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
{
var entity = _entityManager.GetEntity(info.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)
continue;
var aabb = _entityLookup.GetWorldAABB(entity.Value);
// if not on screen, skip
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));
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
ZIndex = 200;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
// Draw overlays for visible players, starting from the top of the screen
foreach (var info in sortable.OrderBy(s => s.Item4.Y).ToList())
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args)
{
var playerInfo = info.Item1;
var rolePrototype = playerInfo.RoleProto == null
? null
: _prototypeManager.Index(playerInfo.RoleProto.Value);
var viewport = args.WorldAABB;
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))
foreach (var playerInfo in _system.PlayerList)
{
// 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)
// Otherwise the entity can not exist yet
if (!_entityManager.EntityExists(playerInfo.EntityUid))
{
// additional entries after maximum stack size is reached will be drawn over the last entry
if (i <= _overlayStackMax - 1)
currentOffset = lineoffset + s.Item2 ;
i++;
continue;
}
var entity = playerInfo.EntityUid.Value;
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity).MapID != _eyeManager.CurrentMap)
{
continue;
}
var aabb = _entityLookup.GetWorldAABB(entity);
// if not on screen, continue
if (!aabb.Intersects(in viewport))
{
continue;
}
var lineoffset = new Vector2(0f, 11f);
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", Color.OrangeRed);
}
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
}
// 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,33 @@
using System.IO;
using Content.Shared.Administration;
using Robust.Client.UserInterface;
using Robust.Shared.Console;
namespace Content.Client.Administration.Commands;
public sealed class LoadPrototypeCommand : IConsoleCommand
{
public string Command { get; } = "loadprototype";
public string Description { get; } = "Load a prototype file into the server.";
public string Help => Command;
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
LoadPrototype();
}
public static async void LoadPrototype()
{
var dialogManager = IoCManager.Resolve<IFileDialogManager>();
var loadManager = IoCManager.Resolve<IGamePrototypeLoadManager>();
var stream = await dialogManager.OpenFile();
if (stream is null)
return;
// ew oop
var reader = new StreamReader(stream);
var proto = await reader.ReadToEndAsync();
loadManager.SendGamePrototype(proto);
}
}

View File

@@ -0,0 +1,48 @@
using System.Linq;
using Robust.Client.Player;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
namespace Content.Client.Administration.Commands;
/// <summary>
/// Proxy to server-side <c>playglobalsound</c> command. Implements completions.
/// </summary>
public sealed class PlayGlobalSoundCommand : IConsoleCommand
{
public string Command => "playglobalsound";
public string Description => Loc.GetString("play-global-sound-command-description");
public string Help => Loc.GetString("play-global-sound-command-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
shell.RemoteExecuteCommand(argStr);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var hint = Loc.GetString("play-global-sound-command-arg-path");
var res = IoCManager.Resolve<IResourceManager>();
var options = CompletionHelper.ContentFilePath(args[0], res);
return CompletionResult.FromHintOptions(options, hint);
}
if (args.Length == 2)
return CompletionResult.FromHint(Loc.GetString("play-global-sound-command-arg-volume"));
if (args.Length > 2)
{
var plyMgr = IoCManager.Resolve<IPlayerManager>();
var options = plyMgr.Sessions.Select(c => c.Name);
return CompletionResult.FromHintOptions(
options,
Loc.GetString("play-global-sound-command-arg-usern", ("user", args.Length - 2)));
}
return CompletionResult.Empty;
}
}

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