Merge remote-tracking branch 'space-sunrise/master'
# Conflicts: # Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml # Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs # Content.Packaging/ServerPackaging.cs # Content.Server/Content.Server.csproj # Content.Server/Entry/EntryPoint.cs # Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs # Content.Server/Fax/FaxSystem.cs # Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs # Content.Server/_Sunrise/Biocode/Systems/BiocodeDeactivationSystem.cs # Content.Shared/CCVar/CCVars.Atmos.cs # Content.Shared/Repairable/RepairableSystem.cs # Content.Shared/_Sunrise/FleshCult/CauseFleshCultInfection.cs # Resources/Locale/en-US/_prototypes/_sunrise/entities/clothing/belt/belts.ftl # Resources/Locale/en-US/_prototypes/_sunrise/entities/objects/specific/medical/hypospray.ftl # Resources/Locale/en-US/_strings/medical/components/crew-monitoring-component.ftl # Resources/Locale/en-US/_strings/reagents/meta/medicine.ftl # Resources/Locale/ru-RU/_prototypes/_sunrise/entities/clothing/belt/belts.ftl # Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/specific/mech/mechs.ftl # Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/specific/medical/hypospray.ftl # Resources/Locale/ru-RU/_strings/_sunrise/markings/vulpkanin.ftl # Resources/Locale/ru-RU/_strings/_sunrise/mentor-help/mentor-help-ui.ftl # Resources/Locale/ru-RU/_strings/reagents/meta/medicine.ftl # Resources/Maps/Nonstations/nukieplanet.yml # Resources/Maps/_Sunrise/Nonstations/nukieplanet.yml # Resources/Prototypes/Catalog/Fills/Items/belt.yml # Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml # Resources/Prototypes/Damage/containers.yml # Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml # Resources/Prototypes/Entities/Mobs/Player/humanoid.yml # Resources/Prototypes/Entities/Mobs/Species/base.yml # Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml # Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml # Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml # Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml # Resources/Prototypes/Entities/Objects/Tools/cable_coils.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml # Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml # Resources/Prototypes/Reagents/fun.yml # Resources/Prototypes/Reagents/medicine.yml # Resources/Prototypes/Reagents/narcotics.yml # Resources/Prototypes/Reagents/toxins.yml # Resources/Prototypes/Roles/Antags/nukeops.yml # Resources/Prototypes/Roles/Jobs/CentComm/cburn.yml # Resources/Prototypes/Roles/Jobs/CentComm/deathsquad.yml # Resources/Prototypes/Shuttles/shuttle_incoming_event.yml # Resources/Prototypes/Store/currency.yml # Resources/Prototypes/_Starlight/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml # Resources/Prototypes/_Sunrise/Catalog/ert_uplink_catalog.yml # Resources/Prototypes/_Sunrise/Chemistry/medicine.yml # Resources/Prototypes/_Sunrise/Entities/Clothing/Belt/belts.yml # Resources/Prototypes/_Sunrise/Entities/Mobs/Player/eviltwin.yml # Resources/Prototypes/_Sunrise/Entities/Mobs/Player/ghost_roles.yml # Resources/Prototypes/_Sunrise/Entities/Objects/Specific/Mech/mechs.yml # Resources/Prototypes/_Sunrise/Entities/Objects/Specific/Medical/hypospray.yml # Resources/Prototypes/_Sunrise/Entities/Objects/Specific/NTUplink.yml # Resources/Prototypes/_Sunrise/Entities/Objects/Specific/Robotics/borg_brains.yml # Resources/Prototypes/_Sunrise/Entities/Objects/Tools/biocode.yml # Resources/Prototypes/_Sunrise/GameRules/events.yml # Resources/Prototypes/_Sunrise/Reagents/gases.yml # Resources/Prototypes/_Sunrise/Reagents/narcotics.yml # Resources/Prototypes/_Sunrise/Roles/Jobs/Blueshield/blueshield_officer.yml # Resources/Prototypes/_Sunrise/Roles/Jobs/Security/senior_officer.yml # Resources/ServerInfo/Guidebook/Medical/MedicalDoctor.xml # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/meta.json # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonalert.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonalertdirect.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonalertnull.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonclose.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirect.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirectlarge.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirectsmall.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirectxtrlarge.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonfar.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonmedium.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonnull.png # Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinpointer_bloodcult.png # Resources/manifest.yml
This commit is contained in:
commit
7eee898afc
9758 changed files with 2911102 additions and 341549 deletions
|
|
@ -351,7 +351,7 @@ 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,yaml,dll.config,msbuildproj,targets,props,slnx}]
|
||||
indent_size = 2
|
||||
|
||||
[nuget.config]
|
||||
|
|
|
|||
41
.github/ISSUE_TEMPLATE/01_report_issue.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/01_report_issue.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
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
|
||||
|
||||
48
.github/ISSUE_TEMPLATE/02_mapping_issue.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/02_mapping_issue.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/config.yml
vendored
12
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,7 +1,5 @@
|
|||
#contact_links:
|
||||
# - name: Report a Security Vulnerability
|
||||
# url: https://github.com/space-wizards/space-station-14/blob/master/SECURITY.md
|
||||
# about: Please report security vulnerabilities privately so we can fix them before they are publicly disclosed.
|
||||
# - name: Request a Feature
|
||||
# url: https://discord.gg/rGvu9hKffJ
|
||||
# about: Submit feature requests on our Discord server (https://discord.gg/rGvu9hKffJ).
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Request a Feature
|
||||
url: https://discord.gg/rGvu9hKffJ
|
||||
about: Submit feature requests on our Discord server (https://discord.gg/rGvu9hKffJ).
|
||||
|
|
|
|||
20
.github/ISSUE_TEMPLATE/issue_report.md
vendored
20
.github/ISSUE_TEMPLATE/issue_report.md
vendored
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Сообщить о баге
|
||||
about: Сообщить о баге разработчикам для решения проблемы.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: VigersRay
|
||||
|
||||
---
|
||||
|
||||
## Описание
|
||||
<!-- Подробно объясните вашу проблему. Проблемы без должного объяснения могут быть закрыты. -->
|
||||
|
||||
**Воспроизведение**
|
||||
<!-- Включите шаги для воспроизведения, если это возможно. -->
|
||||
|
||||
**Медиа**
|
||||
<!-- Если есть возможность, прикрепите скриншот/видео, показывающие саму проблему и шаги ее воспроизведения. -->
|
||||
|
||||
**Дополнительный контекст**
|
||||
<!-- Добавьте сюда любой другой контекст или скриншоты, связанные с проблемой. -->
|
||||
17
.github/ISSUE_TEMPLATE/issue_request.md
vendored
17
.github/ISSUE_TEMPLATE/issue_request.md
vendored
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
name: Предложить новое
|
||||
about: Запросить у разработчиков добавить что-то новое.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: VigersRay
|
||||
|
||||
---
|
||||
|
||||
**Ваш запрос функции связан с проблемой/ошибкой? Опишите, пожалуйста.**
|
||||
<!-- Четкое и краткое описание проблемы. Например: Меня всегда раздражает, когда [...] -->
|
||||
|
||||
**Опишите желаемое**
|
||||
<!-- Четкое и краткое описание того, что вы хотите, и что оно должно делать. -->
|
||||
|
||||
**Дополнительный контекст**
|
||||
<!-- Добавьте сюда любой другой контекст или скриншоты, связанные с запросом функции. -->
|
||||
11
.github/ISSUE_TEMPLATE/issue_task.md
vendored
11
.github/ISSUE_TEMPLATE/issue_task.md
vendored
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
name: Поставить задачу
|
||||
about: Создать задачу для себя чтобы о ней знали другие, а так же запросить у спрайтеров
|
||||
спрайты.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: VigersRay
|
||||
|
||||
---
|
||||
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,4 +1,5 @@
|
|||
<!-- Пожалуйста прочитайте эту статью перед тем как выложить PR, чтобы избежать лишних правок в процессе осмотра: https://docs.spacestation14.io/en/getting-started/pr-guideline -->
|
||||
<!-- Guidelines: https://docs.spacestation14.io/en/getting-started/pr-guideline -->
|
||||
|
||||
<!-- Текст в стрелочках является комментариями - они не будут видны в вашем PR. -->
|
||||
|
||||
## Кратное описание
|
||||
|
|
|
|||
4
.github/config.yml
vendored
4
.github/config.yml
vendored
|
|
@ -3,7 +3,7 @@ Project-Condor:
|
|||
project: 2
|
||||
inbox: Inbox
|
||||
labels:
|
||||
"W: In Progress": "В Прогрессе"
|
||||
"W: In Progress": "In Progress"
|
||||
"W: Discussion": "Design and Discussion"
|
||||
"W: Backlog": "Backlog"
|
||||
"W: Next": "Следующее"
|
||||
"W: Next": "Next"
|
||||
|
|
|
|||
493
.github/gdd.md
vendored
Normal file
493
.github/gdd.md
vendored
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
## Введение
|
||||
|
||||
**Sunrise Station** — это крупный форк и серверная сборка игры Space Station 14, вдохновлённой культовой Space Station 13. Проект развивается как открытое сообщество, ориентированное на создание глубокой, хаотичной и весёлой ролевой среды в научно-фантастическом сеттинге космической станции. Данный документ предназначен для использования ИИ-агентами, осуществляющими автоматическую проверку пулл-реквестов (PR) и предоставляющими рекомендации по улучшению кода и контента. GDD структурирован так, чтобы обеспечить чёткие критерии оценки изменений, облегчить автоматизацию ревью и поддерживать высокое качество проекта.
|
||||
|
||||
---
|
||||
|
||||
## 1. Цели проекта
|
||||
|
||||
### 1.1 Основные задачи
|
||||
|
||||
- **Создание уникального игрового опыта**: развитие форка SS14 с акцентом на глубокую механику, ролевое взаимодействие и разнообразие сценариев.
|
||||
- **Поддержка и развитие сообщества**: обеспечение открытости кода, прозрачности процессов и вовлечения новых участников.
|
||||
- **Совместимость с upstream**: регулярная синхронизация с основным репозиторием Space Wizards Federation для получения обновлений и исправлений.
|
||||
- **Высокое качество кода и контента**: внедрение строгих стандартов, автоматических тестов и CI/CD для предотвращения деградации качества.
|
||||
- **Масштабируемость и модульность**: архитектура, позволяющая легко добавлять новые механики, карты, ассеты и сценарии.
|
||||
|
||||
### 1.2 Ключевые игровые цели
|
||||
|
||||
- **Постройка и поддержка функционирующей станции** с множеством отделов (инженерный, научный, медицинский, служба безопасности и др.).
|
||||
- **Выполнение сменных задач**: от строительства шаттлов и генераторов до исследования артефактов и обеспечения выживания экипажа.
|
||||
- **Ролевое взаимодействие**: поддержка сложных сценариев, антагонистов, событий и уникальных профессий.
|
||||
|
||||
---
|
||||
|
||||
## 2. Основные игровые механики
|
||||
|
||||
### 2.1 Обзор
|
||||
|
||||
Sunrise Station реализует широкий спектр механик, охватывающих симуляцию физиологии персонажей, взаимодействие с окружением, управление станцией и социальные аспекты. Особое внимание уделяется глубине и взаимосвязанности систем, что обеспечивает богатый игровой опыт.
|
||||
|
||||
### 2.2 Подсистемы физиологии
|
||||
|
||||
#### 2.2.1 Здоровье
|
||||
|
||||
- **Показатель здоровья** отражает степень ранения персонажа. Диапазон от 0 (здоров) до 200+ (смерть).
|
||||
- **Типы урона**: механический (удары, порезы, уколы), термический, электрический, токсический, генетический, удушье, кровотечение, структурный, взрывной, стаминный.
|
||||
- **Кровотечение**: отдельная шкала, зависящая от типа урона. Максимум — 10 единиц, влияет на потерю крови и состояние персонажа.
|
||||
- **Регенерация**: при уроне <20 — восстановление по 0,07 механического и ожогового урона в секунду.
|
||||
- **Модификаторы**: при 60+ урона — замедление, при 100+ — критическое состояние, пассивный урон.
|
||||
|
||||
#### 2.2.2 Выносливость
|
||||
|
||||
- **Базовое значение** — 100 единиц.
|
||||
- **Урон по выносливости**: при 50+ — замедление на 80% (3 сек), при 100+ — падение (6 сек).
|
||||
- **Регенерация**: 3 единицы в секунду, если не получал урон по выносливости 3 сек.
|
||||
- **Влияние веществ**: стимуляторы, дезоксиэфедрин, эфедрин изменяют длительность оглушения и максимальную выносливость.
|
||||
|
||||
#### 2.2.3 Голод и жажда
|
||||
|
||||
- **Голод**: шкала сытости (0–200), влияет на скорость переваривания, замедление при сильном голоде.
|
||||
- **Жажда**: шкала насыщения (0–150), влияет на скорость обработки жидкости и замедление.
|
||||
- **Вещества**: витамины, протеин, другие реагенты оказывают различные эффекты на организм.
|
||||
|
||||
#### 2.2.4 Дыхание
|
||||
|
||||
- **Потребность в газе**: разные расы требуют кислород или азот, отсутствие — урон удушьем.
|
||||
- **Таблица соответствия**: для каждой расы указан тип газа и урон при его отсутствии.
|
||||
|
||||
#### 2.2.5 Защита
|
||||
|
||||
- **Броня и скафандры**: уменьшают урон определённых типов по формуле: `Урон - Урон * защита% = итоговый урон`.
|
||||
- **Взрывной урон**: игнорирует большинство видов защиты, кроме специальной.
|
||||
|
||||
### 2.3 Архитектура станции и карты
|
||||
|
||||
- **Станции определяются YAML-прототипами**, включающими метаданные, компоненты, распределение профессий, параметры биома и освещения.
|
||||
- **Биомы**: задают визуальные и геймплейные эффекты (цвет освещения, параллакс, атмосфера).
|
||||
- **Рабочие места**: для каждого отдела определён диапазон доступных позиций.
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектура и структура кода
|
||||
|
||||
### 3.1 Общая структура репозитория
|
||||
|
||||
| Папка/Файл | Назначение |
|
||||
|---------------------------|----------------------------------------------------|
|
||||
| `.github/` | CI/CD, workflows, шаблоны PR/ISSUE |
|
||||
| `BuildChecker/` | Скрипты и утилиты для сборки |
|
||||
| `BuildFiles/` | Файлы для сборки под разные ОС |
|
||||
| `Content.Client/` | Клиентская часть |
|
||||
| `Content.Server/` | Серверная часть |
|
||||
| `Content.Shared/` | Общие компоненты и системы |
|
||||
| `Content.IntegrationTests/`| Интеграционные тесты |
|
||||
| `Content.Tests/` | Юнит-тесты |
|
||||
| `Content.Benchmarks/` | Бенчмарки производительности |
|
||||
| `Content.MapRenderer/` | Рендеринг карт |
|
||||
| `Content.Packaging/` | Сборка и упаковка ассетов |
|
||||
| `Content.YAMLLinter/` | Линтер YAML-файлов |
|
||||
| `Resources/` | Ассеты, карты, локализация |
|
||||
| `RobustToolbox/` | Игровой движок (подмодуль) |
|
||||
| `Sunrise/` | Специфичные для Sunrise расширения |
|
||||
| `Tools/` | Вспомогательные инструменты |
|
||||
| `README.md`, `README.ru.md`| Документация |
|
||||
| `LICENSE.TXT`, `CLA.txt` | Лицензии и соглашения |
|
||||
|
||||
**Пояснения**:
|
||||
- Вся логика разделена на клиентскую, серверную и общую части (Client/Server/Shared), что облегчает поддержку и расширение.
|
||||
- Тесты и бенчмарки вынесены в отдельные папки для автоматизации проверки качества.
|
||||
- Ассеты и карты структурированы по типу и назначению, YAML-файлы проходят автоматическую валидацию.
|
||||
|
||||
### 3.2 Модули и ключевые компоненты
|
||||
|
||||
- **Content.Server**: логика сервера, обработка событий, симуляция мира.
|
||||
- **Content.Client**: интерфейс пользователя, визуализация, клиентские системы.
|
||||
- **Content.Shared**: общие компоненты ECS, прототипы, сетевые сообщения.
|
||||
- **RobustToolbox**: движок, реализующий ECS, сетевой стек, рендеринг, физику.
|
||||
- **Content.IntegrationTests/Content.Tests**: тестовые сценарии, покрывающие как отдельные компоненты, так и интеграцию систем.
|
||||
- **Content.YAMLLinter**: автоматическая проверка YAML-файлов на корректность и стиль.
|
||||
|
||||
---
|
||||
|
||||
## 4. Используемые технологии и зависимости
|
||||
|
||||
### 4.1 Языки и платформы
|
||||
|
||||
- **C#**: основной язык разработки, поддержка .NET 7+.
|
||||
- **.NET**: кроссплатформенная среда выполнения, сборка и запуск на Windows, Linux, MacOS.
|
||||
- **YAML**: описание карт, прототипов, ассетов.
|
||||
|
||||
### 4.2 Системы сборки и зависимости
|
||||
|
||||
- **NuGet**: управление внешними пакетами, централизовано через `Directory.Packages.props`.
|
||||
- **Nix**: reproducible-сборки, управление окружением для разработчиков (`flake.nix`, `shell.nix`).
|
||||
- **CI/CD**: GitHub Actions, автоматическая сборка, тестирование, обновление кредитов, проверка CRLF и др.
|
||||
- **RobustToolbox**: подмодуль, обновляется автоматически через скрипт `RUN_THIS.py`.
|
||||
- **Скрипты запуска**: `runserver.sh`, `runclient.sh` (и .bat-версии) для локального тестирования и разработки.
|
||||
|
||||
### 4.3 Внешние инструменты
|
||||
|
||||
- **YAMLLinter**: проверка стиля и структуры YAML-файлов.
|
||||
- **DocFX**: генерация документации.
|
||||
- **BenchmarkDotNet**: бенчмарки производительности.
|
||||
- **Git**: контроль версий, поддержка ветвления и PR.
|
||||
|
||||
---
|
||||
|
||||
## 5. Принципы дизайна и кодирования
|
||||
|
||||
### 5.1 Стиль кода
|
||||
|
||||
- **Соблюдение C# Coding Conventions**: отступы, именование, оформление блоков.
|
||||
- **Единый стиль для всех файлов**: enforced через `.editorconfig`, автоматическая проверка на CI.
|
||||
- **Документирование**: XML-комментарии для публичных методов и классов, README для модулей.
|
||||
- **Минимизация дублирования**: выносить общие компоненты в Shared, использовать наследование и композицию.
|
||||
|
||||
### 5.2 Архитектурные принципы
|
||||
|
||||
- **ECS (Entity-Component-System)**: все игровые объекты реализованы как сущности с компонентами, системы обрабатывают логику.
|
||||
- **Модульность**: новые механики и контент добавляются через отдельные модули/прототипы.
|
||||
- **Сетевое разделение**: чёткое разграничение клиентской и серверной логики, минимизация дублирования.
|
||||
- **Конфигурируемость**: параметры вынесены в YAML/JSON, легко изменяются без перекомпиляции.
|
||||
|
||||
### 5.3 Соглашения по именованию
|
||||
|
||||
- **Папки и файлы**: PascalCase для C# файлов, snake_case для ассетов и YAML.
|
||||
- **Классы и интерфейсы**: PascalCase, интерфейсы с префиксом `I`.
|
||||
- **Методы и свойства**: PascalCase.
|
||||
- **Переменные**: camelCase.
|
||||
- **Константы**: ALL_CAPS.
|
||||
|
||||
---
|
||||
|
||||
## 6. Критерии качества для Pull Request (PR)
|
||||
|
||||
### 6.1 Автоматические проверки
|
||||
|
||||
- **Сборка без ошибок**: PR не должен ломать сборку ни на одной поддерживаемой платформе.
|
||||
- **Прохождение всех тестов**: юнит, интеграционные, игровые тесты.
|
||||
- **Проверка форматирования**: соответствие `.editorconfig`, отсутствие лишних пробелов, правильные отступы.
|
||||
- **Валидация YAML**: автоматическая проверка структуры и стиля YAML-файлов.
|
||||
- **Отсутствие конфликтов с upstream**: PR должен быть синхронизирован с последним master/main.
|
||||
- **Проверка лицензий**: новые ассеты и код должны иметь корректные лицензии и соответствовать CLA.
|
||||
|
||||
### 6.2 Ручная и полуавтоматическая проверка
|
||||
|
||||
- **Код-ревью**: анализ логики, читаемости, архитектурных решений.
|
||||
- **Покрытие тестами**: новые функции должны сопровождаться тестами.
|
||||
- **Документация**: наличие описания изменений, комментариев, обновление README при необходимости.
|
||||
- **Обратная совместимость**: изменения не должны ломать существующий функционал без веской причины.
|
||||
|
||||
### 6.3 Критерии приёма изменений
|
||||
|
||||
| Критерий | Обязателен | Примечания |
|
||||
|-------------------------|:----------:|---------------------------------------------|
|
||||
| Сборка проходит | ✔ | На всех поддерживаемых ОС |
|
||||
| Все тесты проходят | ✔ | Включая интеграционные и игровые |
|
||||
| Нет конфликтов | ✔ | С master/main и upstream |
|
||||
| Документация обновлена | ✔ | README, комментарии, changelog |
|
||||
| Лицензии соблюдены | ✔ | Для кода и ассетов |
|
||||
| Нет деградации качества | ✔ | Производительность, безопасность, стиль |
|
||||
| Покрытие тестами | ✔ | Для новых/изменённых функций |
|
||||
| Обратная совместимость | ✔ | Если не указано иное в описании PR |
|
||||
|
||||
---
|
||||
|
||||
## 7. Автоматическая проверка изменений: правила для ИИ
|
||||
|
||||
### 7.1 Общие принципы
|
||||
|
||||
- **Сравнивать изменения с текущим master/main**: анализировать diff, выявлять потенциальные проблемы.
|
||||
- **Проверять соответствие стилю и архитектуре**: использовать встроенные линтеры и анализаторы.
|
||||
- **Оценивать влияние на производительность и безопасность**: выявлять потенциальные bottleneck'и, утечки, уязвимости.
|
||||
- **Проверять наличие и качество тестов**: новые функции должны сопровождаться тестами.
|
||||
- **Анализировать документацию**: изменения должны быть отражены в README, комментариях, changelog.
|
||||
|
||||
### 7.2 Алгоритм автоматической проверки
|
||||
|
||||
1. **Извлечь diff PR**: определить изменённые файлы и области кода.
|
||||
2. **Проверить сборку и тесты**: убедиться в отсутствии ошибок.
|
||||
3. **Запустить линтеры и форматтеры**: проверить стиль кода и YAML.
|
||||
4. **Проверить лицензии и CLA**: новые файлы должны иметь корректные лицензии.
|
||||
5. **Проверить документацию**: наличие и актуальность описаний.
|
||||
6. **Проверить обратную совместимость**: нет ли breaking changes без обоснования.
|
||||
7. **Сравнить с примерами хороших/плохих изменений**: выявить паттерны и антипаттерны.
|
||||
8. **Сформировать рекомендации**: указать на найденные проблемы, предложить улучшения.
|
||||
|
||||
---
|
||||
|
||||
## 8. Примеры хороших и плохих изменений
|
||||
|
||||
### 8.1 Примеры хороших изменений (паттерны)
|
||||
|
||||
| Изменение | Почему хорошо |
|
||||
|------------------------------------------------|----------------------------------------------------|
|
||||
| Добавлен новый компонент с тестами и документацией | Повышает покрытие, облегчает поддержку |
|
||||
| Оптимизация алгоритма с бенчмарком | Улучшает производительность, подтверждено метриками |
|
||||
| Исправление бага с подробным описанием | Улучшает стабильность, облегчает ревью |
|
||||
| Рефакторинг с сохранением обратной совместимости| Повышает читаемость, не ломает существующий код |
|
||||
| Добавление новых YAML-прототипов с валидацией | Расширяет контент, не нарушая структуру |
|
||||
|
||||
**Анализ**:
|
||||
Хорошие изменения сопровождаются тестами, документацией, не ломают существующий функционал, проходят все проверки и улучшают проект по одному из критериев: производительность, стабильность, расширяемость, удобство поддержки.
|
||||
|
||||
### 8.2 Примеры плохих изменений (антипаттерны)
|
||||
|
||||
| Изменение | Почему плохо |
|
||||
|------------------------------------------------|----------------------------------------------------|
|
||||
| Изменение логики без тестов и документации | Усложняет поддержку, повышает риск багов |
|
||||
| Ломает обратную совместимость без обоснования | Приводит к деградации пользовательского опыта |
|
||||
| Добавление ассетов без лицензии | Нарушает юридические требования |
|
||||
| Игнорирование стиля и форматирования | Усложняет ревью, снижает читаемость |
|
||||
| Изменение в core-модулях без обсуждения | Риск конфликтов и деградации архитектуры |
|
||||
|
||||
**Анализ**:
|
||||
Плохие изменения часто сопровождаются отсутствием тестов, документации, нарушают стиль, ломают существующий функционал или не учитывают юридические аспекты. Такие изменения должны быть отклонены или доработаны.
|
||||
|
||||
---
|
||||
|
||||
## 9. Тестирование
|
||||
|
||||
### 9.1 Виды тестов
|
||||
|
||||
- **Юнит-тесты**: проверяют отдельные компоненты и системы на корректность работы.
|
||||
- **Интеграционные тесты**: моделируют взаимодействие между модулями, проверяют сценарии использования.
|
||||
- **Игровые тесты**: автоматизированные сценарии, эмулирующие поведение игроков, загрузку карт, запуск раундов.
|
||||
- **Бенчмарки**: измеряют производительность критичных участков кода.
|
||||
|
||||
### 9.2 Инфраструктура тестирования
|
||||
|
||||
- **PoolManager**: управление парами сервер-клиент для интеграционных тестов.
|
||||
- **PostMapInitTest**: валидация карт, прототипов, YAML-файлов.
|
||||
- **CI/CD**: автоматический запуск тестов при каждом PR.
|
||||
- **Отчёты о покрытии**: анализируются на CI, минимальный порог покрытия обязателен для новых функций.
|
||||
|
||||
### 9.3 Критерии успешного тестирования
|
||||
|
||||
- **100% прохождение тестов**: PR не должен ломать существующие тесты.
|
||||
- **Покрытие новых функций**: все новые/изменённые компоненты должны иметь тесты.
|
||||
- **Валидация YAML и карт**: автоматическая проверка структуры и корректности.
|
||||
|
||||
---
|
||||
|
||||
## 10. Документация и комментарии в коде
|
||||
|
||||
### 10.1 Требования к документации
|
||||
|
||||
- **README.md/README.ru.md**: актуальное описание проекта, инструкции по сборке и запуску.
|
||||
- **XML-комментарии**: для всех публичных методов, классов, интерфейсов.
|
||||
- **Комментарии в коде**: пояснения к сложной логике, TODO/FIXME только при необходимости.
|
||||
- **Документация для новых модулей**: отдельные файлы или разделы в README.
|
||||
|
||||
### 10.2 Автоматизация
|
||||
|
||||
- **DocFX**: генерация HTML-документации из XML-комментариев.
|
||||
- **Проверка на CI**: наличие и актуальность документации для новых функций.
|
||||
|
||||
---
|
||||
|
||||
## 11. Лицензирование и CLA
|
||||
|
||||
### 11.1 Лицензии
|
||||
|
||||
- **MIT**: для кода, заимствованного из Space Wizards Federation.
|
||||
- **CLA (Contributor License Agreement)**: для всех изменений, внесённых в Sunrise Station. Подразумевает передачу прав на использование и модификацию кода проекту, запрещает коммерческое использование без разрешения, требует передачи изменений обратно в проект.
|
||||
- **CC-BY-SA 3.0**: для большинства ассетов (графика, звуки), если не указано иное в метаданных.
|
||||
|
||||
### 11.2 Правила использования кода
|
||||
|
||||
- **Коммерческое использование**: запрещено без отдельного разрешения.
|
||||
- **Публичный хостинг**: только с разрешения владельцев проекта.
|
||||
- **Внесение изменений**: любые модификации должны быть переданы обратно в проект на условиях CLA.
|
||||
- **Лицензии ассетов**: каждый ассет должен иметь явное указание лицензии в метаданных.
|
||||
|
||||
---
|
||||
|
||||
## 12. Процесс работы с пулл-реквестами и ветвлением
|
||||
|
||||
### 12.1 Ветвление
|
||||
|
||||
- **Основная ветка**: `master` (или `main`).
|
||||
- **Фичевые ветки**: для каждой новой функции или исправления создаётся отдельная ветка.
|
||||
- **Синхронизация с upstream**: регулярное обновление ветки от основного репозитория Space Wizards Federation.
|
||||
- **Запрет на PR из master**: все изменения должны идти из отдельных веток.
|
||||
|
||||
### 12.2 Процесс PR
|
||||
|
||||
1. **Создание ветки**: от master/main.
|
||||
2. **Внесение изменений**: с соблюдением всех критериев качества.
|
||||
3. **Добавление тестов и документации**.
|
||||
4. **Проверка локально**: сборка, тесты, линтеры.
|
||||
5. **Открытие PR**: с подробным описанием изменений.
|
||||
6. **Автоматическая проверка**: CI, тесты, линтеры.
|
||||
7. **Код-ревью**: ручная или полуавтоматическая проверка.
|
||||
8. **Мерж или доработка**: после одобрения и устранения замечаний.
|
||||
|
||||
---
|
||||
|
||||
## 13. Безопасность и политика безопасности
|
||||
|
||||
### 13.1 Общие принципы
|
||||
|
||||
- **Ответственное раскрытие уязвимостей**: сообщения о проблемах безопасности принимаются через Discord или email, публичное раскрытие запрещено до разрешения.
|
||||
- **Проверка кода на уязвимости**: автоматические анализаторы, ручной аудит для критичных изменений.
|
||||
- **Ограничение доступа**: права доступа к CI/CD, секретам и инфраструктуре только у доверенных участников.
|
||||
- **Логирование и аудит**: все действия по безопасности должны быть зафиксированы.
|
||||
|
||||
### 13.2 Политика обработки инцидентов
|
||||
|
||||
- **Быстрое реагирование**: при обнаружении уязвимости — немедленное исправление и уведомление участников.
|
||||
- **Документирование инцидентов**: ведение журнала инцидентов и принятых мер.
|
||||
- **Обновление зависимостей**: регулярная проверка и обновление внешних библиотек.
|
||||
|
||||
---
|
||||
|
||||
## 14. Производительность и оптимизация
|
||||
|
||||
### 14.1 Профилирование
|
||||
|
||||
- **Бенчмарки**: регулярное измерение производительности критичных систем (загрузка карт, обработка событий, сетевой стек).
|
||||
- **Анализ bottleneck'ов**: выявление и устранение узких мест.
|
||||
- **Мониторинг логов**: анализ ошибок и предупреждений для выявления деградации.
|
||||
|
||||
### 14.2 Оптимизация
|
||||
|
||||
- **Асинхронность**: использование асинхронных операций для I/O и сетевых задач.
|
||||
- **Кэширование**: для часто используемых данных и ресурсов.
|
||||
- **Минимизация аллокаций**: оптимизация работы с памятью, особенно в горячих путях.
|
||||
- **Логирование**: структурированное, с учётом производительности и объёма данных.
|
||||
|
||||
---
|
||||
|
||||
## 15. Модули и ключевые компоненты
|
||||
|
||||
### 15.1 Server, Client, Shared
|
||||
|
||||
- **Server**: обработка симуляции, событий, логики NPC, взаимодействие с базой данных.
|
||||
- **Client**: визуализация, UI, обработка пользовательского ввода.
|
||||
- **Shared**: компоненты ECS, прототипы, сетевые сообщения, общие утилиты.
|
||||
|
||||
### 15.2 Дополнительные модули
|
||||
|
||||
- **Content.Benchmarks**: тесты производительности.
|
||||
- **Content.MapRenderer**: визуализация карт.
|
||||
- **Content.Packaging**: упаковка ассетов и билдов.
|
||||
- **Content.YAMLLinter**: автоматическая проверка YAML.
|
||||
- **Pow3r, Tools**: вспомогательные утилиты и инструменты.
|
||||
|
||||
---
|
||||
|
||||
## 16. Система сборки и упаковки
|
||||
|
||||
### 16.1 BuildFiles и Packaging
|
||||
|
||||
- **BuildFiles/**: специфичные для платформ файлы сборки (Mac, Windows, Linux).
|
||||
- **Content.Packaging/**: скрипты и конфиги для упаковки ассетов, генерации билдов.
|
||||
- **RUN_THIS.py**: автоматизация инициализации подмодулей и зависимостей.
|
||||
- **Скрипты запуска**: `runserver.sh`, `runclient.sh` для локального тестирования.
|
||||
|
||||
### 16.2 Локальная сборка и запуск
|
||||
|
||||
- **Инструкции**: описаны в README.ru.md, поддерживаются для всех основных ОС.
|
||||
- **Требования**: установленный Git, Python 3.7+, .NET 7+, Nix (опционально).
|
||||
- **Процесс**: клонирование репозитория, запуск скрипта и сборка через dotnet.
|
||||
|
||||
---
|
||||
|
||||
## 17. Критерии приёма изменений: функциональность и совместимость
|
||||
|
||||
### 17.1 Функциональность
|
||||
|
||||
- **Изменения должны быть обоснованы**: каждая новая функция или исправление должно иметь чёткое описание и цель.
|
||||
- **Покрытие тестами**: обязательное для всех новых функций.
|
||||
- **Документация**: обновление README, комментариев, changelog.
|
||||
|
||||
### 17.2 Обратная совместимость
|
||||
|
||||
- **Не ломать существующий функционал**: breaking changes допускаются только с веской причиной и обсуждением.
|
||||
- **Сохранение форматов данных**: YAML, прототипы, карты должны быть совместимы с предыдущими версиями.
|
||||
- **Синхронизация с upstream**: регулярное обновление и разрешение конфликтов.
|
||||
|
||||
---
|
||||
|
||||
## 18. Локализация и ресурсы
|
||||
|
||||
### 18.1 Локализация
|
||||
|
||||
- **README.ru.md**: основной файл документации на русском.
|
||||
- **Локали**: все игровые строки вынесены в отдельные файлы локализации, поддержка нескольких языков.
|
||||
- **Требования к новым строкам**: обязательное добавление в локализационные файлы, поддержка fallback на английский.
|
||||
|
||||
### 18.2 Ассеты и YAML
|
||||
|
||||
- **Стиль оформления**: карты, ассеты, YAML-файлы должны соответствовать единому стилю и структуре.
|
||||
- **Линтеры**: автоматическая проверка на CI.
|
||||
- **Лицензии**: каждый ассет должен иметь явное указание лицензии.
|
||||
|
||||
---
|
||||
|
||||
## 19. Мониторинг и логирование
|
||||
|
||||
### 19.1 Серверные логи
|
||||
|
||||
- **Структурированное логирование**: использование формата JSON или аналогичного для автоматического анализа.
|
||||
- **Уровни логов**: INFO, WARN, ERROR, FATAL, DEBUG.
|
||||
- **Агрегация и централизованное хранение**: все логи собираются в единую систему для анализа и мониторинга.
|
||||
- **Политика хранения**: ротация, удаление старых логов, защита от утечек данных.
|
||||
- **Не логировать чувствительные данные**: пароли, токены, персональные данные должны быть исключены из логов.
|
||||
|
||||
### 19.2 Мониторинг
|
||||
|
||||
- **Метрики производительности**: загрузка процессора, памяти, сетевой трафик, время отклика.
|
||||
- **Алерты**: автоматическое оповещение о сбоях, ошибках, превышении пороговых значений.
|
||||
- **Дашборды**: визуализация состояния сервера и ключевых метрик.
|
||||
|
||||
---
|
||||
|
||||
## 20. Совместимость с upstream Space Wizards Federation
|
||||
|
||||
### 20.1 Синхронизация
|
||||
|
||||
- **Регулярное обновление**: слияние изменений из основного репозитория Space Wizards Federation.
|
||||
- **Разделение кода**: собственные изменения Sunrise Station выделены, чтобы облегчить мержи и разрешение конфликтов.
|
||||
- **Соблюдение лицензий**: заимствованный код — под MIT, собственные изменения — под CLA.
|
||||
- **Обратная совместимость**: поддержка форматов данных и протоколов, используемых в upstream.
|
||||
|
||||
### 20.2 Политика обновлений
|
||||
|
||||
- **Тестирование после мержей**: обязательный прогон всех тестов после синхронизации.
|
||||
- **Документирование конфликтов**: ведение журнала изменений и конфликтов при обновлениях.
|
||||
- **Обратная связь с upstream**: при необходимости — отправка багфиксов и улучшений обратно в основной репозиторий.
|
||||
|
||||
---
|
||||
|
||||
## 21. Приложения: чек-листы для автоматической проверки
|
||||
|
||||
### 21.1 Чек-лист для ИИ-ревью
|
||||
|
||||
- [ ] Сборка проходит на всех платформах
|
||||
- [ ] Все тесты (юнит, интеграционные, игровые) проходят
|
||||
- [ ] Нет конфликтов с master/main и upstream
|
||||
- [ ] Изменения покрыты тестами
|
||||
- [ ] Документация обновлена (README, комментарии)
|
||||
- [ ] YAML-файлы валидны и соответствуют стилю
|
||||
- [ ] Лицензии и CLA соблюдены
|
||||
- [ ] Нет деградации производительности
|
||||
- [ ] Нет утечек или уязвимостей безопасности
|
||||
- [ ] Логирование структурировано, не содержит чувствительных данных
|
||||
- [ ] Обратная совместимость сохранена
|
||||
|
||||
### 21.2 Примеры сообщений для ИИ
|
||||
|
||||
**Положительный пример**:
|
||||
> ✅ Изменения соответствуют стилю, покрыты тестами, документация обновлена. Производительность не ухудшена, обратная совместимость сохранена.
|
||||
|
||||
**Отрицательный пример**:
|
||||
> ❌ Внесённые изменения ломают обратную совместимость (формат карты изменён без миграции). Нет тестов для новых функций. YAML-файлы не проходят валидацию.
|
||||
|
||||
---
|
||||
|
||||
## 22. Заключение
|
||||
|
||||
Данный GDD служит основой для автоматической и ручной проверки изменений в проекте Sunrise Station. Соблюдение описанных стандартов, критериев качества и процессов обеспечивает стабильное развитие, высокое качество кода и контента, а также безопасность и удобство для всех участников сообщества. ИИ-агенты, использующие этот документ, должны строго следовать указанным правилам, обеспечивая объективную и прозрачную оценку каждого пулл-реквеста.
|
||||
20
.github/labeler.yml
vendored
20
.github/labeler.yml
vendored
|
|
@ -1,30 +1,30 @@
|
|||
"Изменения: Спрайты":
|
||||
"Changes: Sprites":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.rsi/*.png'
|
||||
|
||||
"Изменения: Карты":
|
||||
"Changes: Map":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- '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'
|
||||
|
||||
"Изменения: Локализация":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.ftl'
|
||||
|
||||
"Изменения: Аудио":
|
||||
"Changes: Audio":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.ogg'
|
||||
|
||||
"Нет C#":
|
||||
"Changes: No C#":
|
||||
- changed-files:
|
||||
# Equiv to any-glob-to-all as long as this has one matcher. If ALL changed files are not C# files, then apply label.
|
||||
- all-globs-to-all-files: "!**/*.cs"
|
||||
|
||||
"Changes: Localisation":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.ftl'
|
||||
|
|
|
|||
46
.github/workflows/ai-gdd-review.yml
vendored
Normal file
46
.github/workflows/ai-gdd-review.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
name: AI Game Design Review
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
ai_review:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get diff
|
||||
run: |
|
||||
git fetch origin pull/${{ github.event.pull_request.number }}/head:pr_branch
|
||||
git diff ${{ github.event.pull_request.base.sha }} pr_branch > diff.txt
|
||||
|
||||
- name: Save PR metadata
|
||||
run: |
|
||||
echo "${{ github.event.pull_request.title }}" > pr_title.txt
|
||||
echo "${{ github.event.pull_request.body }}" > pr_body.txt
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install google-genai
|
||||
|
||||
- name: Run AI review
|
||||
env:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GEMINI_MODEL: ${{ secrets.GEMINI_MODEL }}
|
||||
run: |
|
||||
python3 Tools/_sunrise/ai_review.py
|
||||
|
||||
- name: Post PR comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
path: ai_review.txt
|
||||
2
.github/workflows/build-docfx.yml
vendored
2
.github/workflows/build-docfx.yml
vendored
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
|
|
|||
2
.github/workflows/build-map-renderer.yml
vendored
2
.github/workflows/build-map-renderer.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
|
|
|||
2
.github/workflows/build-test-debug.yml
vendored
2
.github/workflows/build-test-debug.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
|
|
|||
16
.github/workflows/publish-test.yml
vendored
16
.github/workflows/publish-test.yml
vendored
|
|
@ -24,10 +24,22 @@ jobs:
|
|||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
# Не включать пока приват не будет обновлен к мастеру
|
||||
|
||||
#- name: Checkout SunrisePrivate
|
||||
# uses: actions/checkout@v4.2.2
|
||||
# with:
|
||||
# repository: ${{ secrets.SUNRISE_PRIVATE_REPO }}
|
||||
# path: SunrisePrivate
|
||||
# ssh-key: ${{ secrets.SUNRISE_PRIVATE_REPO_SSH_KEY }}
|
||||
|
||||
#- name: Move Sunrise Private files
|
||||
# run: python3 Tools/_sunrise/move_private.py --clone-dir "SunrisePrivate" --target-dirs "Resources" "Content.Client" "Content.Server" "Content.Shared"
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
|
|
@ -41,7 +53,7 @@ jobs:
|
|||
run: dotnet build Content.Packaging --configuration Release --no-restore /m
|
||||
|
||||
- name: Package server
|
||||
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
|
||||
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
|
||||
|
|
|
|||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
run: dotnet build Content.Packaging --configuration Release --no-restore /m
|
||||
|
||||
- name: Package server
|
||||
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
|
||||
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
|
||||
|
|
|
|||
4
.github/workflows/test-packaging.yml
vendored
4
.github/workflows/test-packaging.yml
vendored
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
|
@ -60,7 +60,7 @@ jobs:
|
|||
run: dotnet build Content.Packaging --configuration Release --no-restore /m
|
||||
|
||||
- name: Package server
|
||||
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
|
||||
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
|
||||
|
|
|
|||
2
.github/workflows/yaml-linter.yml
vendored
2
.github/workflows/yaml-linter.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,3 +1,6 @@
|
|||
# MSbuild binlog files
|
||||
*.binlog
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
|
|
@ -306,4 +309,3 @@ Resources/MapImages
|
|||
|
||||
# Direnv stuff
|
||||
.direnv/
|
||||
.idea/
|
||||
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
|
|
@ -40,7 +40,27 @@
|
|||
"cwd": "${workspaceFolder}/Content.YAMLLinter",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Server - Release",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build - Release",
|
||||
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
|
||||
"args": [""],
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Client - Release",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build - Release",
|
||||
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
|
||||
"args": [],
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
|
|
@ -50,6 +70,14 @@
|
|||
"Client"
|
||||
],
|
||||
"preLaunchTask": "build"
|
||||
},
|
||||
{
|
||||
"name": "Server/Client - Release",
|
||||
"configurations": [
|
||||
"Server - Release",
|
||||
"Client - Release"
|
||||
],
|
||||
"preLaunchTask": "build - Release"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
.vscode/tasks.json
vendored
22
.vscode/tasks.json
vendored
|
|
@ -76,6 +76,26 @@
|
|||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build - Release",
|
||||
"command": "dotnet",
|
||||
"type": "shell",
|
||||
"args": [
|
||||
"build",
|
||||
"--configuration",
|
||||
"Release",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:'ForceNoAlign;NoSummary'",
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
#!/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 subprocess
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
|
||||
SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
|
||||
# If this doesn't match the saved version we overwrite them all.
|
||||
CURRENT_HOOKS_VERSION = "2"
|
||||
CURRENT_HOOKS_VERSION = "4"
|
||||
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
|
||||
|
||||
|
||||
|
|
@ -25,12 +27,10 @@ def run_command(command: List[str], capture: bool = False) -> subprocess.Complet
|
|||
|
||||
sys.stdout.flush()
|
||||
|
||||
completed = None
|
||||
|
||||
if capture:
|
||||
completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE)
|
||||
completed = subprocess.run(command, stdout=subprocess.PIPE, text=True)
|
||||
else:
|
||||
completed = subprocess.run(command, cwd="..")
|
||||
completed = subprocess.run(command)
|
||||
|
||||
if completed.returncode != 0:
|
||||
print("Error: command exited with code {}!".format(completed.returncode))
|
||||
|
|
@ -43,7 +43,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,22 +76,21 @@ 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("..")/".git"/"hooks"
|
||||
hooks_target_dir = Path(run_command(["git", "rev-parse", "--git-path", "hooks"], True).stdout.strip())
|
||||
hooks_source_dir = Path("hooks")
|
||||
|
||||
# Clear entire tree since we need to kill deleted files too.
|
||||
for filename in os.listdir(str(hooks_target_dir)):
|
||||
os.remove(str(hooks_target_dir/filename))
|
||||
for filename in os.listdir(hooks_target_dir):
|
||||
os.remove(hooks_target_dir / filename)
|
||||
|
||||
for filename in os.listdir(str(hooks_source_dir)):
|
||||
for filename in os.listdir(hooks_source_dir):
|
||||
print("Copying hook {}".format(filename))
|
||||
shutil.copy2(str(hooks_source_dir/filename),
|
||||
str(hooks_target_dir/filename))
|
||||
shutil.copy2(hooks_source_dir / filename, hooks_target_dir / filename)
|
||||
|
||||
with open("INSTALLED_HOOKS_VERSION", "w") as f:
|
||||
f.write(CURRENT_HOOKS_VERSION)
|
||||
|
||||
|
||||
def reset_solution():
|
||||
|
|
@ -107,8 +106,7 @@ def reset_solution():
|
|||
|
||||
def check_for_zip_download():
|
||||
# Check if .git exists,
|
||||
cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
if not os.path.isdir(os.path.join(cur_dir, ".git")):
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
gitroot=`git rev-parse --show-toplevel`
|
||||
gitroot=$(git rev-parse --show-toplevel)
|
||||
|
||||
cd "$gitroot/BuildChecker"
|
||||
cd "$gitroot/BuildChecker" || exit
|
||||
|
||||
if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
|
||||
if [[ $(uname) == MINGW* || $(uname) == CYGWIN* ]]; then
|
||||
# Windows
|
||||
py -3 git_helper.py --quiet
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Just call post-checkout since it does the same thing.
|
||||
gitroot=`git rev-parse --show-toplevel`
|
||||
bash "$gitroot/.git/hooks/post-checkout"
|
||||
gitroot=$(git rev-parse --git-path hooks)
|
||||
bash "$gitroot/post-checkout"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ This isn’t an exhaustive list of things that you can’t do. Rather, take it i
|
|||
|
||||
This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here.
|
||||
|
||||
If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com).
|
||||
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.
|
||||
|
|
|
|||
11
CONTRIBUTING.md
Normal file
11
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# 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.
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<OutputPath>..\bin\Content.Benchmarks\</OutputPath>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
|
||||
<!-- pin transitive deps -->
|
||||
<PackageReference Include="System.Management" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
|
||||
|
|
@ -19,10 +22,12 @@
|
|||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
|
||||
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Benchmarks\Robust.Benchmarks.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Server.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Shared.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Benchmarks.props" />
|
||||
<Import Project="..\RobustToolbox\Imports\Testing.props" />
|
||||
</Project>
|
||||
|
|
|
|||
174
Content.Benchmarks/DeltaPressureBenchmark.cs
Normal file
174
Content.Benchmarks/DeltaPressureBenchmark.cs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
191
Content.Benchmarks/DestructibleBenchmark.cs
Normal file
191
Content.Benchmarks/DestructibleBenchmark.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
253
Content.Benchmarks/GasReactionBenchmark.cs
Normal file
253
Content.Benchmarks/GasReactionBenchmark.cs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
3
Content.Benchmarks/GlobalUsings.cs
Normal file
3
Content.Benchmarks/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Global usings for Content.Benchmarks
|
||||
|
||||
global using Robust.UnitTesting.Pool;
|
||||
|
|
@ -5,7 +5,7 @@ using System.Threading.Tasks;
|
|||
using BenchmarkDotNet.Attributes;
|
||||
using Content.IntegrationTests;
|
||||
using Content.IntegrationTests.Pair;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
|
|
@ -47,7 +47,7 @@ public class MapLoadBenchmark
|
|||
PoolManager.Shutdown();
|
||||
}
|
||||
|
||||
public static readonly string[] MapsSource = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
|
||||
public static string[] MapsSource { get; } = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
|
||||
|
||||
[ParamsSource(nameof(MapsSource))]
|
||||
public string Map;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public class RaiseEventBenchmark
|
|||
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(() =>
|
||||
|
|
@ -30,6 +32,8 @@ public class RaiseEventBenchmark
|
|||
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();
|
||||
|
|
@ -60,6 +64,12 @@ public class RaiseEventBenchmark
|
|||
return _sys.RaiseICompEvent();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseNetEvent()
|
||||
{
|
||||
return _sys.RaiseNetIdEvent();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseCSharpEvent()
|
||||
{
|
||||
|
|
@ -74,6 +84,8 @@ public class RaiseEventBenchmark
|
|||
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()
|
||||
{
|
||||
|
|
@ -92,7 +104,7 @@ public class RaiseEventBenchmark
|
|||
public int RaiseCompEvent()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
EntityManager.EventBus.RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
|
||||
RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +112,16 @@ public class RaiseEventBenchmark
|
|||
{
|
||||
// Raise with an IComponent instead of concrete type
|
||||
var ev = new BenchEv();
|
||||
EntityManager.EventBus.RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +139,7 @@ public class RaiseEventBenchmark
|
|||
}
|
||||
|
||||
[ByRefEvent]
|
||||
[ComponentEvent(Exclusive = false)]
|
||||
public struct BenchEv
|
||||
{
|
||||
public int N;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<GridContainer xmlns="https://spacestation14.io"
|
||||
Columns="5"
|
||||
Columns="4"
|
||||
HorizontalAlignment="Center">
|
||||
</GridContainer>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
MinSize="650 290">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<GridContainer Columns="2">
|
||||
<GridContainer Columns="3" HorizontalExpand="True">
|
||||
<GridContainer Name="PrivilegedIdGrid" Columns="3" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'access-overrider-window-privileged-id'}" />
|
||||
<Button Name="PrivilegedIdButton" Access="Public"/>
|
||||
<Label Name="PrivilegedIdLabel" />
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ namespace Content.Client.Access.UI
|
|||
public void SetAccessLevels(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels)
|
||||
{
|
||||
_accessButtons.Clear();
|
||||
AccessLevelGrid.DisposeAllChildren();
|
||||
AccessLevelGrid.RemoveAllChildren();
|
||||
|
||||
foreach (var access in accessLevels)
|
||||
{
|
||||
if (!protoManager.TryIndex(access, out var accessLevel))
|
||||
if (!protoManager.Resolve(access, out var accessLevel))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -53,6 +53,8 @@ namespace Content.Client.Access.UI
|
|||
|
||||
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")
|
||||
|
|
@ -77,7 +79,9 @@ namespace Content.Client.Access.UI
|
|||
missingPrivileges.Add(privilege);
|
||||
}
|
||||
|
||||
MissingPrivilegesLabel.Text = Loc.GetString("access-overrider-window-missing-privileges");
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ namespace Content.Client.Access.UI
|
|||
|
||||
public void SetAllowedIcons(string currentJobIconId)
|
||||
{
|
||||
IconGrid.DisposeAllChildren();
|
||||
IconGrid.RemoveAllChildren();
|
||||
|
||||
var jobIconButtonGroup = new ButtonGroup();
|
||||
var i = 0;
|
||||
|
|
@ -49,12 +49,12 @@ namespace Content.Client.Access.UI
|
|||
icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture));
|
||||
foreach (var jobIcon in icons)
|
||||
{
|
||||
String styleBase = StyleBase.ButtonOpenBoth;
|
||||
String styleBase = StyleClass.ButtonOpenBoth;
|
||||
var modulo = i % JobIconColumnCount;
|
||||
if (modulo == 0)
|
||||
styleBase = StyleBase.ButtonOpenRight;
|
||||
styleBase = StyleClass.ButtonOpenRight;
|
||||
else if (modulo == JobIconColumnCount - 1)
|
||||
styleBase = StyleBase.ButtonOpenLeft;
|
||||
styleBase = StyleClass.ButtonOpenLeft;
|
||||
|
||||
// Generate buttons
|
||||
var jobIconButton = new Button
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
|||
|
||||
foreach (var accessGroup in _accessGroups)
|
||||
{
|
||||
if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
|
||||
if (!_protoManager.Resolve(accessGroup, out var accessGroupProto))
|
||||
continue;
|
||||
|
||||
_groupedAccessLevels.Add(accessGroupProto, new());
|
||||
|
|
@ -65,13 +65,13 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
|||
|
||||
// Ensure that the 'general' access group is added to handle
|
||||
// misc. access levels that aren't associated with any group
|
||||
if (_protoManager.TryIndex(GeneralAccessGroup, out var generalAccessProto))
|
||||
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.TryIndex(accessLevel, out var accessLevelProto))
|
||||
if (!_protoManager.Resolve(accessLevel, out var accessLevelProto))
|
||||
continue;
|
||||
|
||||
var assigned = false;
|
||||
|
|
@ -99,8 +99,8 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
|||
|
||||
private bool TryRebuildAccessGroupControls()
|
||||
{
|
||||
AccessGroupList.DisposeAllChildren();
|
||||
AccessLevelChecklist.DisposeAllChildren();
|
||||
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.
|
||||
|
|
@ -119,11 +119,11 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
|||
if (_groupedAccessLevels.Count > 1)
|
||||
{
|
||||
if (AccessGroupList.ChildCount == 0)
|
||||
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft);
|
||||
accessGroupButton.AddStyleClass(StyleClass.ButtonOpenLeft);
|
||||
else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1))
|
||||
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight);
|
||||
accessGroupButton.AddStyleClass(StyleClass.ButtonOpenRight);
|
||||
else
|
||||
accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth);
|
||||
accessGroupButton.AddStyleClass(StyleClass.ButtonOpenBoth);
|
||||
}
|
||||
|
||||
accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup);
|
||||
|
|
@ -165,7 +165,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
|
|||
/// </summary>
|
||||
public void RebuildAccessLevelsControls()
|
||||
{
|
||||
AccessLevelChecklist.DisposeAllChildren();
|
||||
AccessLevelChecklist.RemoveAllChildren();
|
||||
_accessLevelEntries.Clear();
|
||||
|
||||
// No access level prototypes were assigned to any of the access level groups
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Content.Shared.Access.Systems;
|
|||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.CrewManifest;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using static Content.Shared.Access.Components.IdCardConsoleComponent;
|
||||
|
|
@ -74,7 +75,7 @@ namespace Content.Client.Access.UI
|
|||
_window?.UpdateState(castState);
|
||||
}
|
||||
|
||||
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, string newJobPrototype)
|
||||
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, ProtoId<JobPrototype> newJobPrototype)
|
||||
{
|
||||
if (newFullName.Length > _maxNameLength)
|
||||
newFullName = newFullName[.._maxNameLength];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<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'}" />
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
</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" />
|
||||
|
|
@ -26,10 +28,19 @@
|
|||
<Button Name="JobTitleSaveButton" Text="{Loc 'id-card-console-window-save-button'}" Disabled="True" />
|
||||
</GridContainer>
|
||||
<Control MinSize="0 8" />
|
||||
<GridContainer Columns="2">
|
||||
<Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
|
||||
<OptionButton Name="JobPresetOptionButton" />
|
||||
</GridContainer>
|
||||
<!-- 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" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,18 @@ 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);
|
||||
|
|
@ -89,14 +101,13 @@ namespace Content.Client.Access.UI
|
|||
}
|
||||
}
|
||||
|
||||
private void ClearAllAccess()
|
||||
/// <param name="enabled">If true, every individual access button will be pressed. If false, each will be depressed.</param>
|
||||
private void SetAllAccess(bool enabled)
|
||||
{
|
||||
foreach (var button in _accessButtons.ButtonsList.Values)
|
||||
{
|
||||
if (button.Pressed)
|
||||
{
|
||||
button.Pressed = false;
|
||||
}
|
||||
if (!button.Disabled && button.Pressed != enabled)
|
||||
button.Pressed = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +121,7 @@ namespace Content.Client.Access.UI
|
|||
JobTitleLineEdit.Text = Loc.GetString(job.Name);
|
||||
args.Button.SelectId(args.Id);
|
||||
|
||||
ClearAllAccess();
|
||||
SetAllAccess(false);
|
||||
|
||||
// this is a sussy way to do this
|
||||
foreach (var access in job.Access)
|
||||
|
|
@ -123,7 +134,7 @@ namespace Content.Client.Access.UI
|
|||
|
||||
foreach (var group in job.AccessGroups)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
|
||||
if (!_prototypeManager.Resolve(group, out AccessGroupPrototype? groupPrototype))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ namespace Content.Client.Actions
|
|||
[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;
|
||||
|
|
@ -286,8 +287,27 @@ namespace Content.Client.Actions
|
|||
continue;
|
||||
}
|
||||
|
||||
if (assignmentNode is SequenceDataNode sequenceAssignments)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
AddActionDirect((user, actions), actionId);
|
||||
}
|
||||
|
||||
AssignSlot?.Invoke(assignments);
|
||||
}
|
||||
|
||||
private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
|
||||
|
|
@ -309,10 +329,10 @@ namespace Content.Client.Actions
|
|||
// this is the actual entity-world targeting magic
|
||||
EntityUid? targetEnt = null;
|
||||
if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
|
||||
args.Input.EntityUid != null &&
|
||||
ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
|
||||
args.Input.EntityUid is { Valid: true } entityUid &&
|
||||
ValidateEntityTarget(user, entityUid, (uid, entity)))
|
||||
{
|
||||
targetEnt = args.Input.EntityUid;
|
||||
targetEnt = entityUid;
|
||||
}
|
||||
|
||||
if (action.ClientExclusive)
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ namespace Content.Client.Actions.UI
|
|||
|
||||
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
|
||||
{
|
||||
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSystem;
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
|
||||
SetOnlyStyleClass(StyleNano.StyleClassTooltipPanel);
|
||||
SetOnlyStyleClass(StyleClass.TooltipPanel);
|
||||
|
||||
BoxContainer vbox;
|
||||
AddChild(vbox = new BoxContainer
|
||||
|
|
@ -36,7 +37,7 @@ namespace Content.Client.Actions.UI
|
|||
var nameLabel = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionTitle}
|
||||
StyleClasses = { StyleClass.TooltipTitle }
|
||||
};
|
||||
nameLabel.SetMessage(name);
|
||||
vbox.AddChild(nameLabel);
|
||||
|
|
@ -46,7 +47,7 @@ namespace Content.Client.Actions.UI
|
|||
var description = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionDescription}
|
||||
StyleClasses = { StyleClass.TooltipDesc }
|
||||
};
|
||||
description.SetMessage(desc);
|
||||
vbox.AddChild(description);
|
||||
|
|
@ -55,7 +56,7 @@ namespace Content.Client.Actions.UI
|
|||
vbox.AddChild(_cooldownLabel = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionCooldown},
|
||||
StyleClasses = { StyleClass.TooltipDesc },
|
||||
Visible = false
|
||||
});
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ namespace Content.Client.Actions.UI
|
|||
var requiresLabel = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionRequirements}
|
||||
StyleClasses = { StyleClass.TooltipDesc }
|
||||
};
|
||||
|
||||
if (!FormattedMessage.TryFromMarkup("[color=#635c5c]" + requires + "[/color]", out var markup))
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
using Content.Shared.Administration.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Administration.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class KillSignComponent : SharedKillSignComponent;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
using Content.Shared.Administration;
|
||||
|
||||
namespace Content.Client.Administration.Systems;
|
||||
|
||||
public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
|
||||
{
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Client.Administration.Systems;
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ public sealed class AdminWhoSystem : EntitySystem
|
|||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
|
||||
SubscribeNetworkEvent<AdminWhoResponseEvent>(OnAdminWhoResponse);
|
||||
}
|
||||
|
||||
|
|
@ -28,4 +29,4 @@ public sealed class AdminWhoSystem : EntitySystem
|
|||
{
|
||||
OnAdminWhoUpdate?.Invoke(args.Admins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
Content.Client/Administration/Systems/BufferingSystem.cs
Normal file
7
Content.Client/Administration/Systems/BufferingSystem.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
using Content.Shared.Administration.Systems;
|
||||
|
||||
namespace Content.Client.Administration.Systems;
|
||||
|
||||
public sealed class BufferingSystem : SharedBufferingSystem
|
||||
{
|
||||
}
|
||||
|
|
@ -1,46 +1,77 @@
|
|||
using System.Numerics;
|
||||
using Content.Client.Administration.Components;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.Administration.Systems;
|
||||
|
||||
public sealed class KillSignSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<KillSignComponent, ComponentStartup>(KillSignAdded);
|
||||
SubscribeLocalEvent<KillSignComponent, ComponentShutdown>(KillSignRemoved);
|
||||
SubscribeLocalEvent<KillSignComponent, AfterAutoHandleStateEvent>(AfterAutoHandleState);
|
||||
}
|
||||
|
||||
private void KillSignRemoved(EntityUid uid, KillSignComponent component, ComponentShutdown args)
|
||||
private void KillSignRemoved(Entity<KillSignComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
if (!_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var layer, false))
|
||||
return;
|
||||
|
||||
_sprite.RemoveLayer((uid, sprite), layer);
|
||||
RemoveKillsign(ent);
|
||||
}
|
||||
|
||||
private void KillSignAdded(EntityUid uid, KillSignComponent component, ComponentStartup args)
|
||||
private void KillSignAdded(Entity<KillSignComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
AddKillsign(ent);
|
||||
}
|
||||
|
||||
private void AfterAutoHandleState(Entity<KillSignComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
// After receiving a new state for the component, we remove the old killsign and build a new one.
|
||||
// This is so changes to the sprite can be displayed live and allowing them to be edited via ViewVariables.
|
||||
// This could just update an existing sprite, but this is both easier and runs rarely anyway.
|
||||
RemoveKillsign(ent);
|
||||
AddKillsign(ent);
|
||||
}
|
||||
|
||||
private void AddKillsign(Entity<KillSignComponent> ent)
|
||||
{
|
||||
// If we hide from owner and we ARE the owner, don't add a killsign.
|
||||
// This could use session specific networking to FULLY hide it, but I am too lazy right now.
|
||||
if (ent.Comp.HideFromOwner && _player.LocalEntity == ent)
|
||||
return;
|
||||
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var _, false))
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
return;
|
||||
|
||||
var adj = _sprite.GetLocalBounds((uid, sprite)).Height / 2 + ((1.0f / 32) * 6.0f);
|
||||
if (_sprite.LayerMapTryGet((ent, sprite), KillSignKey.Key, out var _, false))
|
||||
return;
|
||||
|
||||
var layer = _sprite.AddLayer((uid, sprite), new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "sign"));
|
||||
_sprite.LayerMapSet((uid, sprite), KillSignKey.Key, layer);
|
||||
if (ent.Comp.Sprite == null)
|
||||
return;
|
||||
|
||||
_sprite.LayerSetOffset((uid, sprite), layer, new Vector2(0.0f, adj));
|
||||
sprite.LayerSetShader(layer, "unshaded");
|
||||
var adj = _sprite.GetLocalBounds((ent, sprite)).Height / 2 + ((1.0f / 32) * 6.0f);
|
||||
|
||||
var layer = _sprite.AddLayer((ent, sprite), ent.Comp.Sprite);
|
||||
_sprite.LayerMapSet((ent, sprite), KillSignKey.Key, layer);
|
||||
_sprite.LayerSetScale((ent, sprite), layer, ent.Comp.Scale);
|
||||
_sprite.LayerSetOffset((ent, sprite), layer, ent.Comp.DoOffset ? new Vector2(0.0f, adj) : new Vector2(0.0f, 0.0f));
|
||||
|
||||
if (ent.Comp.ForceUnshaded)
|
||||
sprite.LayerSetShader(layer, "unshaded");
|
||||
}
|
||||
|
||||
private void RemoveKillsign(Entity<KillSignComponent> ent)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
return;
|
||||
|
||||
if (!_sprite.LayerMapTryGet((ent, sprite), KillSignKey.Key, out var layer, false))
|
||||
return;
|
||||
|
||||
_sprite.RemoveLayer((ent, sprite), layer);
|
||||
}
|
||||
|
||||
private enum KillSignKey
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc admin-camera-window-title-placeholder}"
|
||||
SetSize="425 550"
|
||||
MinSize="200 225"
|
||||
Name="Window">
|
||||
MinSize="200 225">
|
||||
</DefaultWindow>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
</PanelContainer.PanelOverride>
|
||||
|
||||
<Control HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="600">
|
||||
<PanelContainer StyleClasses="AngleRect" />
|
||||
<PanelContainer StyleClasses="BackgroundPanel" />
|
||||
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<RichTextLabel Name="Description" />
|
||||
|
|
|
|||
|
|
@ -12,17 +12,19 @@ namespace Content.Client.Administration.UI.AdminRemarks;
|
|||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AdminMessagePopupWindow : Control
|
||||
{
|
||||
[Dependency] private readonly IStylesheetManager _styleMan = default!;
|
||||
|
||||
private float _timer = float.MaxValue;
|
||||
|
||||
public event Action? OnDismissPressed;
|
||||
|
||||
public event Action? OnAcceptPressed;
|
||||
|
||||
public AdminMessagePopupWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
|
||||
Stylesheet = _styleMan.SheetSystem;
|
||||
|
||||
AcceptButton.OnPressed += OnAcceptButtonPressed;
|
||||
DismissButton.OnPressed += OnDismissButtonPressed;
|
||||
|
|
@ -49,7 +51,8 @@ public sealed partial class AdminMessagePopupWindow : Control
|
|||
MessageContainer.AddChild(new AdminMessagePopupMessage(message));
|
||||
}
|
||||
|
||||
Description.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("admin-notes-message-desc", ("count", state.Messages.Length))));
|
||||
Description.SetMessage(
|
||||
FormattedMessage.FromMarkup(Loc.GetString("admin-notes-message-desc", ("count", state.Messages.Length))));
|
||||
}
|
||||
|
||||
private void OnDismissButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ namespace Content.Client.Administration.UI.BanPanel;
|
|||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BanPanel : DefaultWindow
|
||||
{
|
||||
public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
|
||||
public event Action<Ban>? BanSubmitted;
|
||||
public event Action<string>? PlayerChanged;
|
||||
private string? PlayerUsername { get; set; }
|
||||
private (IPAddress, int)? IpAddress { get; set; }
|
||||
|
|
@ -37,8 +37,8 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
// This is less efficient than just holding a reference to the root control and enumerating children, but you
|
||||
// have to know how the controls are nested, which makes the code more complicated.
|
||||
// Role group name -> the role buttons themselves.
|
||||
private readonly Dictionary<string, List<Button>> _roleCheckboxes = new();
|
||||
private readonly ISawmill _banpanelSawmill;
|
||||
private readonly Dictionary<string, List<(Button, IPrototype)>> _roleCheckboxes = new();
|
||||
private readonly ISawmill _banPanelSawmill;
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
|
@ -82,7 +82,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
|
||||
_banPanelSawmill = _logManager.GetSawmill("admin.banpanel");
|
||||
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
|
||||
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
|
||||
PlayerCheckbox.OnPressed += _ =>
|
||||
|
|
@ -113,7 +113,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
TypeOption.SelectId(args.Id);
|
||||
OnTypeChanged();
|
||||
};
|
||||
LastConnCheckbox.OnPressed += args =>
|
||||
LastConnCheckbox.OnPressed += _ =>
|
||||
{
|
||||
IpLine.ModulateSelfOverride = null;
|
||||
HwidLine.ModulateSelfOverride = null;
|
||||
|
|
@ -169,7 +169,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
|
||||
var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
|
||||
.OrderBy(x => x.ID);
|
||||
CreateRoleGroup("Antagonist", Color.Red, antagRoles);
|
||||
CreateRoleGroup(AntagPrototype.GroupName, AntagPrototype.GroupColor, antagRoles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -231,7 +231,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
var roleGroupCheckbox = new Button
|
||||
{
|
||||
Name = $"{groupName}GroupCheckbox",
|
||||
Text = "Ban all",
|
||||
Text = Loc.GetString("role-bans-ban-group"),
|
||||
Margin = new Thickness(0, 0, 5, 0),
|
||||
ToggleMode = true,
|
||||
};
|
||||
|
|
@ -241,14 +241,14 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
{
|
||||
foreach (var role in _roleCheckboxes[groupName])
|
||||
{
|
||||
role.Pressed = args.Pressed;
|
||||
role.Item1.Pressed = args.Pressed;
|
||||
}
|
||||
|
||||
if (args.Pressed)
|
||||
{
|
||||
if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Departmental role ban severity could not be parsed from config!");
|
||||
return;
|
||||
}
|
||||
|
|
@ -260,14 +260,14 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
{
|
||||
foreach (var button in roleButtons)
|
||||
{
|
||||
if (button.Pressed)
|
||||
if (button.Item1.Pressed)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Role ban severity could not be parsed from config!");
|
||||
return;
|
||||
}
|
||||
|
|
@ -299,30 +299,44 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a checkbutton specifically for one "role" in a "group"
|
||||
/// Adds a toggle button specifically for one "role" in a "group"
|
||||
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
|
||||
/// </summary>
|
||||
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
|
||||
{
|
||||
var roleCheckboxContainer = new BoxContainer();
|
||||
var roleCheckButton = new Button
|
||||
var roleToggleButton = new Button
|
||||
{
|
||||
Name = $"{role}RoleCheckbox",
|
||||
Name = role,
|
||||
Text = role,
|
||||
ToggleMode = true,
|
||||
};
|
||||
roleCheckButton.OnToggled += args =>
|
||||
roleToggleButton.OnToggled += args =>
|
||||
{
|
||||
// Checks the role group checkbox if all the children are pressed
|
||||
if (args.Pressed && _roleCheckboxes[group].All(e => e.Pressed))
|
||||
if (args.Pressed && _roleCheckboxes[group].All(e => e.Item1.Pressed))
|
||||
roleGroupCheckbox.Pressed = args.Pressed;
|
||||
else
|
||||
roleGroupCheckbox.Pressed = false;
|
||||
};
|
||||
|
||||
IPrototype rolePrototype;
|
||||
|
||||
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype))
|
||||
rolePrototype = jobPrototype;
|
||||
else if (_protoMan.TryIndex<AntagPrototype>(role, out var antagPrototype))
|
||||
rolePrototype = antagPrototype;
|
||||
else
|
||||
{
|
||||
_banPanelSawmill.Error($"Adding a role checkbox for role {role}: role is not a JobPrototype or AntagPrototype.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This is adding the icon before the role name
|
||||
// Yeah, this is sus, but having to split the functions up and stuff is worse imo.
|
||||
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
|
||||
// TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
|
||||
// // I know the ban manager is doing the same thing, but that should not leak into UI code.
|
||||
if (jobPrototype is not null && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
|
||||
{
|
||||
var jobIconTexture = new TextureRect
|
||||
{
|
||||
|
|
@ -334,12 +348,12 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
roleCheckboxContainer.AddChild(jobIconTexture);
|
||||
}
|
||||
|
||||
roleCheckboxContainer.AddChild(roleCheckButton);
|
||||
roleCheckboxContainer.AddChild(roleToggleButton);
|
||||
|
||||
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
|
||||
|
||||
_roleCheckboxes.TryAdd(group, []);
|
||||
_roleCheckboxes[group].Add(roleCheckButton);
|
||||
_roleCheckboxes[group].Add((roleToggleButton, rolePrototype));
|
||||
}
|
||||
|
||||
public void UpdateBanFlag(bool newFlag)
|
||||
|
|
@ -382,7 +396,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
TimeLine.Text = args.Text;
|
||||
if (!double.TryParse(args.Text, out var result))
|
||||
{
|
||||
ExpiresLabel.Text = "err";
|
||||
ExpiresLabel.Text = Loc.GetString("ban-panel-expiry-error");
|
||||
ErrorLevel |= ErrorLevelEnum.Minutes;
|
||||
TimeLine.ModulateSelfOverride = Color.Red;
|
||||
UpdateSubmitEnabled();
|
||||
|
|
@ -492,7 +506,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
newSeverity = serverSeverity;
|
||||
else
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Server ban severity could not be parsed from config!");
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +519,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
}
|
||||
else
|
||||
{
|
||||
_banpanelSawmill
|
||||
_banPanelSawmill
|
||||
.Warning("Role ban severity could not be parsed from config!");
|
||||
}
|
||||
break;
|
||||
|
|
@ -550,34 +564,51 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
|
||||
private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
string[]? roles = null;
|
||||
ProtoId<JobPrototype>[]? jobs = null;
|
||||
ProtoId<AntagPrototype>[]? antags = null;
|
||||
|
||||
if (TypeOption.SelectedId == (int) Types.Role)
|
||||
{
|
||||
var rolesList = new List<string>();
|
||||
var jobList = new List<ProtoId<JobPrototype>>();
|
||||
var antagList = new List<ProtoId<AntagPrototype>>();
|
||||
|
||||
if (_roleCheckboxes.Count == 0)
|
||||
throw new DebugAssertException("RoleCheckboxes was empty");
|
||||
|
||||
foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons))
|
||||
{
|
||||
if (button is { Pressed: true, Text: not null })
|
||||
if (button.Item1 is { Pressed: true, Name: not null })
|
||||
{
|
||||
rolesList.Add(button.Text);
|
||||
switch (button.Item2)
|
||||
{
|
||||
case JobPrototype:
|
||||
jobList.Add(button.Item2.ID);
|
||||
|
||||
break;
|
||||
case AntagPrototype:
|
||||
antagList.Add(button.Item2.ID);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rolesList.Count == 0)
|
||||
if (jobList.Count + antagList.Count == 0)
|
||||
{
|
||||
Tabs.CurrentTab = (int) TabNumbers.Roles;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
roles = rolesList.ToArray();
|
||||
jobs = jobList.ToArray();
|
||||
antags = antagList.ToArray();
|
||||
}
|
||||
|
||||
if (TypeOption.SelectedId == (int) Types.None)
|
||||
{
|
||||
TypeOption.ModulateSelfOverride = Color.Red;
|
||||
Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -589,6 +620,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
ReasonTextEdit.GrabKeyboardFocus();
|
||||
ReasonTextEdit.ModulateSelfOverride = Color.Red;
|
||||
ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -597,6 +629,7 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
|
||||
SubmitButton.ModulateSelfOverride = Color.Red;
|
||||
SubmitButton.Text = Loc.GetString("ban-panel-confirm");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -605,7 +638,22 @@ public sealed partial class BanPanel : DefaultWindow
|
|||
var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
|
||||
var severity = (NoteSeverity) SeverityOption.SelectedId;
|
||||
var erase = EraseCheckbox.Pressed;
|
||||
BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles, erase);
|
||||
|
||||
var ban = new Ban(
|
||||
player,
|
||||
IpAddress,
|
||||
useLastIp,
|
||||
Hwid,
|
||||
useLastHwid,
|
||||
(uint)(TimeEntered * Multiplier),
|
||||
reason,
|
||||
severity,
|
||||
jobs,
|
||||
antags,
|
||||
erase
|
||||
);
|
||||
|
||||
BanSubmitted?.Invoke(ban);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ public sealed class BanPanelEui : BaseEui
|
|||
{
|
||||
BanPanel = new BanPanel();
|
||||
BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
|
||||
BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase)
|
||||
=> SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase));
|
||||
BanPanel.BanSubmitted += ban => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(ban));
|
||||
BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
{
|
||||
_adminWhoSystem = system;
|
||||
_adminWhoSystem.OnAdminWhoUpdate += OnAdminListReceived;
|
||||
|
||||
// Request the list when the window is initialized
|
||||
RefreshAdminList();
|
||||
}
|
||||
|
||||
public void Uninitialize()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<Button Visible="True" Name="AdminWho" Access="Public" Text="{Loc 'admin-who-button'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
|
||||
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
|
||||
<Button Visible="False" Access="Public" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
|
||||
<controls:ConfirmButton Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
|
||||
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
|
||||
<controls:ConfirmButton Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
using Content.Shared.Administration.Logs;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Administration.UI.CustomControls;
|
||||
|
||||
public sealed class AdminLogLabel : RichTextLabel
|
||||
{
|
||||
public AdminLogLabel(ref SharedAdminLog log, HSeparator separator)
|
||||
{
|
||||
Log = log;
|
||||
Separator = separator;
|
||||
|
||||
SetMessage($"{log.Date:HH:mm:ss}: {log.Message}");
|
||||
OnVisibilityChanged += VisibilityChanged;
|
||||
}
|
||||
|
||||
public new SharedAdminLog Log { get; }
|
||||
|
||||
public HSeparator Separator { get; }
|
||||
|
||||
private void VisibilityChanged(Control control)
|
||||
{
|
||||
Separator.Visible = Visible;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
OnVisibilityChanged -= VisibilityChanged;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Administration.UI.CustomControls;
|
||||
|
||||
|
|
@ -95,26 +96,13 @@ public sealed partial class PlayerListControl : BoxContainer
|
|||
private void FilterList()
|
||||
{
|
||||
_sortedPlayerList.Clear();
|
||||
|
||||
Regex filterRegex;
|
||||
// There is no neat way to handle invalid regex being submitted other than
|
||||
// catching and ignoring the exception which gets thrown when it's invalid.
|
||||
try
|
||||
{
|
||||
filterRegex = new Regex(FilterLineEdit.Text, RegexOptions.IgnoreCase);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var info in _playerList)
|
||||
{
|
||||
var displayName = $"{info.CharacterName} ({info.Username})";
|
||||
if (info.IdentityName != info.CharacterName)
|
||||
displayName += $" [{info.IdentityName}]";
|
||||
if (!string.IsNullOrEmpty(FilterLineEdit.Text)
|
||||
&& !filterRegex.IsMatch(displayName))
|
||||
&& !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant()))
|
||||
continue;
|
||||
_sortedPlayerList.Add(info);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Administration.UI.CustomControls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PlayerListEntry : BoxContainer
|
||||
{
|
||||
private readonly ResPath _pinnedResPath = new("/Textures/Interface/Bwoink/pinned.png");
|
||||
private readonly ResPath _unPinnedResPath = new("/Textures/Interface/Bwoink/un_pinned.png");
|
||||
|
||||
public PlayerListEntry()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
|
@ -42,15 +47,6 @@ public sealed partial class PlayerListEntry : BoxContainer
|
|||
|
||||
private void UpdatePinButtonTexture(bool isPinned)
|
||||
{
|
||||
if (isPinned)
|
||||
{
|
||||
PlayerEntryPinButton?.RemoveStyleClass(StyleNano.StyleClassPinButtonUnpinned);
|
||||
PlayerEntryPinButton?.AddStyleClass(StyleNano.StyleClassPinButtonPinned);
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerEntryPinButton?.RemoveStyleClass(StyleNano.StyleClassPinButtonPinned);
|
||||
PlayerEntryPinButton?.AddStyleClass(StyleNano.StyleClassPinButtonUnpinned);
|
||||
}
|
||||
PlayerEntryPinButton.TexturePath = isPinned ? _pinnedResPath.CanonPath : _unPinnedResPath.CanonPath;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:aui="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:ui="clr-namespace:Content.Client.Options.UI">
|
||||
xmlns:aui="clr-namespace:Content.Client.Administration.UI.CustomControls">
|
||||
<PanelContainer StyleClasses="BackgroundDark">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
|
@ -53,13 +52,6 @@
|
|||
<Button Name="ExportLogs" Access="Public" Text="{Loc admin-logs-export}"/>
|
||||
<Button Name="PopOutButton" Access="Public" Text="{Loc admin-logs-pop-out}"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer HorizontalExpand="True">
|
||||
<Button Name="RenderRichTextButton" Access="Public" Text="{Loc admin-logs-render-rich-text}"
|
||||
StyleClasses="OpenRight" ToggleMode="True"/>
|
||||
<Button Name="RemoveMarkupButton" Access="Public" Text="{Loc admin-logs-remove-markup}"
|
||||
StyleClasses="OpenLeft" ToggleMode="True"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<LineEdit Name="LogSearch" Access="Public" StyleClasses="actionSearchBox"
|
||||
HorizontalExpand="true" PlaceHolder="{Loc admin-logs-search-logs-placeholder}"/>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using System.Runtime.InteropServices;
|
|||
using System.Text.RegularExpressions;
|
||||
using Content.Client._Sunrise.Administration.UI.CustomControls;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.Administration.UI.Logs.Entries;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Client.AutoGenerated;
|
||||
|
|
@ -41,9 +40,6 @@ public sealed partial class AdminLogsControl : Control
|
|||
SelectAllPlayersButton.OnPressed += SelectAllPlayers;
|
||||
SelectNoPlayersButton.OnPressed += SelectNoPlayers;
|
||||
|
||||
RenderRichTextButton.OnPressed += RenderRichTextChanged;
|
||||
RemoveMarkupButton.OnPressed += RemoveMarkupChanged;
|
||||
|
||||
RoundSpinBox.IsValid = i => i > 0 && i <= CurrentRound;
|
||||
RoundSpinBox.ValueChanged += RoundSpinBoxChanged;
|
||||
RoundSpinBox.InitDefaultButtons();
|
||||
|
|
@ -56,16 +52,13 @@ public sealed partial class AdminLogsControl : Control
|
|||
|
||||
private int CurrentRound { get; set; }
|
||||
|
||||
private Regex LogSearchRegex { get; set; } = new("");
|
||||
|
||||
public int SelectedRoundId => RoundSpinBox.Value;
|
||||
public string Search => LogSearch.Text;
|
||||
private int ShownLogs { get; set; }
|
||||
private int TotalLogs { get; set; }
|
||||
private int RoundLogs { get; set; }
|
||||
public bool IncludeNonPlayerLogs { get; set; }
|
||||
private bool RenderRichText { get; set; }
|
||||
private bool RemoveMarkup { get; set; }
|
||||
|
||||
public HashSet<LogType> SelectedTypes { get; } = new();
|
||||
|
||||
public HashSet<Guid> SelectedPlayers { get; } = new();
|
||||
|
|
@ -112,19 +105,6 @@ public sealed partial class AdminLogsControl : Control
|
|||
|
||||
private void LogSearchChanged(LineEditEventArgs args)
|
||||
{
|
||||
// This exception is thrown if the regex is invalid, which happens often, so we ignore it.
|
||||
try
|
||||
{
|
||||
LogSearchRegex = new Regex(
|
||||
"(" + LogSearch.Text + ")",
|
||||
RegexOptions.IgnoreCase,
|
||||
TimeSpan.FromSeconds(1));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateLogs();
|
||||
}
|
||||
|
||||
|
|
@ -206,26 +186,6 @@ public sealed partial class AdminLogsControl : Control
|
|||
UpdateLogs();
|
||||
}
|
||||
|
||||
private void RenderRichTextChanged(ButtonEventArgs args)
|
||||
{
|
||||
RenderRichText = args.Button.Pressed;
|
||||
|
||||
RemoveMarkup = RemoveMarkup && !RenderRichText;
|
||||
RemoveMarkupButton.Pressed = RemoveMarkup;
|
||||
|
||||
UpdateLogs();
|
||||
}
|
||||
|
||||
private void RemoveMarkupChanged(ButtonEventArgs args)
|
||||
{
|
||||
RemoveMarkup = args.Button.Pressed;
|
||||
|
||||
RenderRichText = !RemoveMarkup && RenderRichText;
|
||||
RenderRichTextButton.Pressed = RenderRichText;
|
||||
|
||||
UpdateLogs();
|
||||
}
|
||||
|
||||
public void SetTypesSelection(HashSet<LogType> selectedTypes, bool invert = false)
|
||||
{
|
||||
SelectedTypes.Clear();
|
||||
|
|
@ -284,15 +244,16 @@ public sealed partial class AdminLogsControl : Control
|
|||
|
||||
foreach (var child in LogsContainer.Children)
|
||||
{
|
||||
if (child is not AdminLogEntry log)
|
||||
if (child is not AdminLogLabel log)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
child.Visible = ShouldShowLog(log);
|
||||
if (!child.Visible)
|
||||
continue;
|
||||
|
||||
log.RenderResults(LogSearchRegex, RenderRichText, RemoveMarkup);
|
||||
ShownLogs++;
|
||||
if (child.Visible)
|
||||
{
|
||||
ShownLogs++;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateCount();
|
||||
|
|
@ -310,30 +271,30 @@ public sealed partial class AdminLogsControl : Control
|
|||
button.Text.Contains(PlayerSearch.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private bool LogMatchesPlayerFilter(AdminLogEntry entry)
|
||||
private bool LogMatchesPlayerFilter(AdminLogLabel label)
|
||||
{
|
||||
if (entry.Log.Players.Length == 0)
|
||||
if (label.Log.Players.Length == 0)
|
||||
return SelectedPlayers.Count == 0 || IncludeNonPlayerLogs;
|
||||
|
||||
return SelectedPlayers.Overlaps(entry.Log.Players);
|
||||
return SelectedPlayers.Overlaps(label.Log.Players);
|
||||
}
|
||||
|
||||
private bool ShouldShowLog(AdminLogEntry entry)
|
||||
private bool ShouldShowLog(AdminLogLabel label)
|
||||
{
|
||||
// Check log type
|
||||
if (!SelectedTypes.Contains(entry.Log.Type))
|
||||
if (!SelectedTypes.Contains(label.Log.Type))
|
||||
return false;
|
||||
|
||||
// Check players
|
||||
if (!LogMatchesPlayerFilter(entry))
|
||||
if (!LogMatchesPlayerFilter(label))
|
||||
return false;
|
||||
|
||||
// Check impact
|
||||
if (!SelectedImpacts.Contains(entry.Log.Impact))
|
||||
if (!SelectedImpacts.Contains(label.Log.Impact))
|
||||
return false;
|
||||
|
||||
// Check search
|
||||
if (!LogSearchRegex.IsMatch(entry.Log.Message))
|
||||
if (!label.Log.Message.Contains(LogSearch.Text, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
|
@ -509,11 +470,21 @@ public sealed partial class AdminLogsControl : Control
|
|||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
ref var log = ref span[i];
|
||||
var entry = new AdminLogEntry(ref log);
|
||||
var separator = new HSeparator();
|
||||
var label = new AdminLogLabel(ref log, separator);
|
||||
label.Visible = ShouldShowLog(label);
|
||||
|
||||
TotalLogs++;
|
||||
LogsContainer.AddChild(entry);
|
||||
if (label.Visible)
|
||||
{
|
||||
ShownLogs++;
|
||||
}
|
||||
|
||||
LogsContainer.AddChild(label);
|
||||
LogsContainer.AddChild(separator);
|
||||
}
|
||||
UpdateLogs();
|
||||
|
||||
UpdateCount();
|
||||
}
|
||||
|
||||
public void SetLogs(List<SharedAdminLog> logs)
|
||||
|
|
@ -557,7 +528,6 @@ public sealed partial class AdminLogsControl : Control
|
|||
SelectAllTypesButton.OnPressed -= SelectAllTypes;
|
||||
SelectNoTypesButton.OnPressed -= SelectNoTypes;
|
||||
|
||||
IncludeNonPlayersButton.OnPressed -= IncludeNonPlayers;
|
||||
IncludeNonPlayersButton.OnPressed -= IncludeNonPlayers;
|
||||
SelectAllPlayersButton.OnPressed -= SelectAllPlayers;
|
||||
SelectNoPlayersButton.OnPressed -= SelectNoPlayers;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Client.Administration.UI.Logs.Entries;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.Eui;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Eui;
|
||||
|
|
@ -22,7 +22,7 @@ public sealed class AdminLogsEui : BaseEui
|
|||
|
||||
private const char CsvSeparator = ',';
|
||||
private const string CsvQuote = "\"";
|
||||
private const string CsvHeader = "Date,ID,PlayerID,Severity,Type,Message,CurTime";
|
||||
private const string CsvHeader = "Date,ID,PlayerID,Severity,Type,Message";
|
||||
|
||||
private ISawmill _sawmill;
|
||||
|
||||
|
|
@ -109,10 +109,10 @@ public sealed class AdminLogsEui : BaseEui
|
|||
await writer.WriteLineAsync(CsvHeader);
|
||||
foreach (var child in LogsControl.LogsContainer.Children)
|
||||
{
|
||||
if (child is not AdminLogEntry entry || !child.Visible)
|
||||
if (child is not AdminLogLabel logLabel || !child.Visible)
|
||||
continue;
|
||||
|
||||
var log = entry.Log;
|
||||
var log = logLabel.Log;
|
||||
|
||||
// Date
|
||||
// I swear to god if someone adds ,s or "s to the other fields...
|
||||
|
|
@ -138,9 +138,6 @@ public sealed class AdminLogsEui : BaseEui
|
|||
await writer.WriteAsync(CsvQuote);
|
||||
await writer.WriteAsync(log.Message.Replace(CsvQuote, CsvQuote + CsvQuote));
|
||||
await writer.WriteAsync(CsvQuote);
|
||||
await writer.WriteAsync(CsvSeparator);
|
||||
// CurTime
|
||||
await writer.WriteAsync(log.CurTime.ToString());
|
||||
|
||||
await writer.WriteLineAsync();
|
||||
}
|
||||
|
|
@ -169,7 +166,7 @@ public sealed class AdminLogsEui : BaseEui
|
|||
ClydeWindow = _clyde.CreateWindow(new WindowCreateParameters
|
||||
{
|
||||
Maximized = false,
|
||||
Title = "Admin Logs",
|
||||
Title = Loc.GetString("admin-logs-title"),
|
||||
Monitor = monitor,
|
||||
Width = 1100,
|
||||
Height = 400
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Margin="2">
|
||||
<Collapsible>
|
||||
<CollapsibleHeading Name="DetailsHeading" Access="Public">
|
||||
<RichTextLabel Margin="20 0 0 0" Name="Message" MinSize="50 10" VerticalExpand="True" Access="Public" />
|
||||
</CollapsibleHeading>
|
||||
<CollapsibleBody Name="DetailsBody" Access="Public" />
|
||||
</Collapsible>
|
||||
</BoxContainer>
|
||||
<cc:HSeparator/>
|
||||
</BoxContainer>
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Administration.UI.Logs.Entries;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AdminLogEntry : BoxContainer
|
||||
{
|
||||
private readonly IConfigurationManager _cfgManager;
|
||||
|
||||
public SharedAdminLog Log { get; }
|
||||
|
||||
private readonly string _rawMessage;
|
||||
|
||||
public AdminLogEntry(ref SharedAdminLog log)
|
||||
{
|
||||
_cfgManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Log = log;
|
||||
|
||||
_rawMessage = $"{log.Date:HH:mm:ss}: {log.Message}";
|
||||
Message.SetMessage(_rawMessage);
|
||||
|
||||
DetailsHeading.OnToggled += DetailsToggled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets text to be highlighted from a search result, and renders rich text, or removes all rich text markup.
|
||||
/// </summary>
|
||||
public void RenderResults(Regex highlightRegex, bool renderRichText, bool removeMarkup)
|
||||
{
|
||||
var color = _cfgManager.GetCVar(CCVars.AdminLogsHighlightColor);
|
||||
var formattedMessage = renderRichText
|
||||
? _rawMessage
|
||||
: removeMarkup
|
||||
? FormattedMessage.RemoveMarkupPermissive(_rawMessage)
|
||||
: FormattedMessage.EscapeText(_rawMessage);
|
||||
|
||||
// Want to avoid highlighting smaller strings
|
||||
if (highlightRegex.ToString().Length > 4)
|
||||
{
|
||||
try
|
||||
{
|
||||
formattedMessage = highlightRegex.Replace(formattedMessage, $"[color={color}]$1[/color]", 3);
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
// if we time out then don't bother highlighting results
|
||||
}
|
||||
}
|
||||
|
||||
if (!FormattedMessage.TryFromMarkup(formattedMessage, out var outputMessage))
|
||||
return;
|
||||
|
||||
Message.SetMessage(outputMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// We perform some extra calculations in the dropdown, so we want to render that only when
|
||||
/// the dropdown is actually opened.
|
||||
/// This also removes itself from the event listener so it doesn't trigger again.
|
||||
/// </summary>
|
||||
private void DetailsToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (!args.Pressed || DetailsBody.ChildCount > 0)
|
||||
return;
|
||||
DetailsBody.AddChild(new AdminLogEntryDetails(Log));
|
||||
DetailsHeading.OnToggled -= DetailsToggled;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
StyleClasses="BackgroundDark">
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc admin-logs-field-type}" Margin="0 0 4 0"/>
|
||||
<Label Name="Type" Text="None" StyleClasses="LabelSecondaryColor" Access="Public"/>
|
||||
</BoxContainer>
|
||||
<cc:HSeparator/>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc admin-logs-field-impact}" Margin="0 0 4 0"/>
|
||||
<Label Name="Impact" Text="None" StyleClasses="LabelSecondaryColor" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<cc:VSeparator/>
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<Label Text="{Loc admin-logs-field-time-header}"/>
|
||||
<cc:HSeparator/>
|
||||
<BoxContainer>
|
||||
<Label Text="{Loc admin-logs-field-time-local}" Margin="0 0 4 0" HorizontalExpand="True"/>
|
||||
<Label Name="LocalTime" Text="None" StyleClasses="LabelSecondaryColor" Access="Public" HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<cc:HSeparator/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc admin-logs-field-time-utc}" Margin="0 0 4 0" HorizontalExpand="True"/>
|
||||
<Label Name="UTCTime" Text="None" StyleClasses="LabelSecondaryColor" Access="Public" HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<cc:HSeparator/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc admin-logs-field-time-round}" Margin="0 0 4 0" HorizontalExpand="True"/>
|
||||
<Label Name="CurTime" Text="None" StyleClasses="LabelSecondaryColor" Access="Public" HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<cc:VSeparator/>
|
||||
<BoxContainer Orientation="Vertical" Margin="4" HorizontalExpand="True">
|
||||
<Label Text="{Loc admin-logs-field-players-header}"/>
|
||||
<cc:HSeparator/>
|
||||
<BoxContainer Orientation="Horizontal" Margin="4" VerticalExpand="True">
|
||||
<controls:ListContainer Name="PlayerListContainer" Access="Public" HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
using System.Linq;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Client.Administration.UI.Logs.Entries;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AdminLogEntryDetails : BoxContainer
|
||||
{
|
||||
private readonly AdminSystem _adminSystem;
|
||||
private readonly IUserInterfaceManager _uiManager;
|
||||
private readonly IEntityManager _entManager;
|
||||
|
||||
public AdminLogEntryDetails(SharedAdminLog log)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_uiManager = IoCManager.Resolve<IUserInterfaceManager>();
|
||||
_adminSystem = _entManager.System<AdminSystem>();
|
||||
|
||||
Type.Text = log.Type.ToString();
|
||||
Impact.Text = log.Impact.ToString();
|
||||
|
||||
LocalTime.Text = $"{log.Date.ToLocalTime():HH:mm:ss}";
|
||||
UTCTime.Text = $"{log.Date:HH:mm:ss}";
|
||||
// TimeSpan and DateTime use different formatting string conventions for some completely logical reason
|
||||
// that mere mortals such as myself will never be able to understand.
|
||||
CurTime.Text = new TimeSpan(log.CurTime).ToString(@"hh\:mm\:ss");
|
||||
|
||||
PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
|
||||
PlayerListContainer.GenerateItem += GenerateButton;
|
||||
PopulateList(log.Players);
|
||||
}
|
||||
|
||||
private void PopulateList(Guid[] players)
|
||||
{
|
||||
if (players.Length == 0)
|
||||
return;
|
||||
|
||||
if (_adminSystem.PlayerList is not { } allPlayers || allPlayers.Count == 0)
|
||||
return;
|
||||
|
||||
var listData = new List<PlayerListData>();
|
||||
foreach (var playerGuid in players)
|
||||
{
|
||||
var netUserId = new NetUserId(playerGuid);
|
||||
|
||||
// Linq here is fine since this runs in response to admin input in the UI and
|
||||
// this loop only tends to go through 1-4 iterations.
|
||||
if (allPlayers.FirstOrDefault(player => player.SessionId == netUserId) is not { } playerInfo)
|
||||
continue;
|
||||
|
||||
listData.Add(new PlayerListData(playerInfo));
|
||||
}
|
||||
|
||||
if (listData.Count == 0)
|
||||
return;
|
||||
|
||||
PlayerListContainer.PopulateList(listData);
|
||||
}
|
||||
|
||||
private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data)
|
||||
{
|
||||
if (args == null || data is not PlayerListData { Info: var selectedPlayer })
|
||||
return;
|
||||
|
||||
if (!(args.Function == EngineKeyFunctions.UIRightClick
|
||||
|| args.Function == EngineKeyFunctions.UIClick)
|
||||
|| selectedPlayer.NetEntity == null)
|
||||
return;
|
||||
|
||||
_uiManager.GetUIController<VerbMenuUIController>().OpenVerbMenu(selectedPlayer.NetEntity.Value, true);
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
private void GenerateButton(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not PlayerListData { Info: var info })
|
||||
return;
|
||||
|
||||
var entryLabel = new Label();
|
||||
entryLabel.Text = $"{info.CharacterName} ({info.Username})";
|
||||
var entry = new BoxContainer();
|
||||
entry.AddChild(entryLabel);
|
||||
|
||||
button.AddChild(entry);
|
||||
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
|
|||
/// </summary>
|
||||
public void UpdateReagents()
|
||||
{
|
||||
ReagentList.DisposeAllChildren();
|
||||
ReagentList.RemoveAllChildren();
|
||||
|
||||
if (_selectedSolution == null || _solutions == null)
|
||||
return;
|
||||
|
|
@ -93,7 +93,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
|
|||
/// <param name="solution">The selected solution.</param>
|
||||
private void UpdateVolumeBox(Solution solution)
|
||||
{
|
||||
VolumeBox.DisposeAllChildren();
|
||||
VolumeBox.RemoveAllChildren();
|
||||
|
||||
var volumeLabel = new Label();
|
||||
volumeLabel.HorizontalExpand = true;
|
||||
|
|
@ -132,7 +132,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
|
|||
/// <param name="solution">The selected solution.</param>
|
||||
private void UpdateThermalBox(Solution solution)
|
||||
{
|
||||
ThermalBox.DisposeAllChildren();
|
||||
ThermalBox.RemoveAllChildren();
|
||||
var heatCap = solution.GetHeatCapacity(null);
|
||||
var specificHeatLabel = new Label();
|
||||
specificHeatLabel.HorizontalExpand = true;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<BoxContainer Orientation="Vertical" Name="Notes" Access="Public" VerticalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
<Button Name="ShowMoreButton" Text="{Loc admin-notes-show-more}" Visible="False" HorizontalAlignment="Center" />
|
||||
<Button Name="NewNoteButton" Text="{Loc admin-notes-new-note}" Disabled="True" />
|
||||
<Button Name="NewNoteButton" Access="Public" Text="{Loc admin-notes-new-note}" Disabled="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,11 @@ public sealed partial class AdminNotesLine : BoxContainer
|
|||
|
||||
if (Note.UnbannedTime is not null)
|
||||
{
|
||||
ExtraLabel.Text = Loc.GetString("admin-notes-unbanned", ("admin", Note.UnbannedByName ?? "[error]"), ("date", Note.UnbannedTime));
|
||||
ExtraLabel.Text = Loc.GetString(
|
||||
"admin-notes-unbanned",
|
||||
("admin", Note.UnbannedByName ?? "[error]"),
|
||||
("date", Note.UnbannedTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))
|
||||
);
|
||||
ExtraLabel.Visible = true;
|
||||
}
|
||||
else if (Note.ExpiryTime is not null)
|
||||
|
|
@ -139,7 +143,7 @@ public sealed partial class AdminNotesLine : BoxContainer
|
|||
|
||||
private string FormatRoleBanMessage()
|
||||
{
|
||||
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new []{"unknown"})} ");
|
||||
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} ");
|
||||
return FormatBanMessageCommon(banMessage);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Title="Loading..."
|
||||
MinSize="400 200">
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<TextEdit Name="NoteTextEdit" HorizontalExpand="True" VerticalExpand="True" />
|
||||
<TextEdit Name="NoteTextEdit" Access="Public" HorizontalExpand="True" VerticalExpand="True" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Name="ExpiryLabel" Text="{Loc admin-note-editor-expiry-label}" Visible="False" />
|
||||
<HistoryLineEdit Name="ExpiryLineEdit" PlaceHolder="{Loc admin-note-editor-expiry-placeholder}"
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
ToolTip="{Loc admin-note-editor-secret-tooltip}" />
|
||||
<CheckBox Name="PermanentCheckBox" Pressed="True" Text="{Loc admin-note-editor-expiry-checkbox}"
|
||||
ToolTip="{Loc admin-note-editor-expiry-checkbox-tooltip}" />
|
||||
<Button Name="SubmitButton" Text="{Loc admin-note-editor-submit}" HorizontalAlignment="Right" />
|
||||
<Button Name="SubmitButton" Access="Public" Text="{Loc admin-note-editor-submit}" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ public sealed partial class NoteEdit : FancyWindow
|
|||
private bool IsSecret { get; set; }
|
||||
private NoteType NoteType { get; set; }
|
||||
|
||||
private NoteSeverity? NoteSeverity
|
||||
public NoteSeverity? NoteSeverity
|
||||
{
|
||||
get => _noteSeverity;
|
||||
set
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ namespace Content.Client.Administration.UI
|
|||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
|
||||
private readonly Menu _menu;
|
||||
private readonly List<DefaultWindow> _subWindows = new();
|
||||
private readonly List<BaseWindow> _subWindows = new();
|
||||
|
||||
private Dictionary<int, PermissionsEuiState.AdminRankData> _ranks =
|
||||
new();
|
||||
|
|
@ -216,7 +216,7 @@ namespace Content.Client.Administration.UI
|
|||
var titleControl = new Label { Text = admin.Title ?? Loc.GetString("permissions-eui-edit-admin-title-control-text").ToLowerInvariant() };
|
||||
if (admin.Title == null) // none
|
||||
{
|
||||
titleControl.StyleClasses.Add(StyleBase.StyleClassItalic);
|
||||
titleControl.StyleClasses.Add(StyleClass.Italic);
|
||||
}
|
||||
|
||||
al.AddChild(titleControl);
|
||||
|
|
@ -240,7 +240,7 @@ namespace Content.Client.Administration.UI
|
|||
var rankControl = new Label { Text = rank };
|
||||
if (italic)
|
||||
{
|
||||
rankControl.StyleClasses.Add(StyleBase.StyleClassItalic);
|
||||
rankControl.StyleClasses.Add(StyleClass.Italic);
|
||||
}
|
||||
|
||||
al.AddChild(rankControl);
|
||||
|
|
@ -340,10 +340,9 @@ namespace Content.Client.Administration.UI
|
|||
tab.AddChild(adminVBox);
|
||||
tab.AddChild(rankVBox);
|
||||
|
||||
Contents.AddChild(tab);
|
||||
ContentsContainer.AddChild(tab);
|
||||
ContentsContainer.MinSize = new(600, 400);
|
||||
}
|
||||
|
||||
protected override Vector2 ContentsMinimumSize => new Vector2(600, 400);
|
||||
}
|
||||
|
||||
private sealed class EditAdminWindow : DefaultWindow
|
||||
|
|
@ -419,21 +418,21 @@ namespace Content.Client.Administration.UI
|
|||
var inherit = new Button
|
||||
{
|
||||
Text = "I",
|
||||
StyleClasses = { StyleBase.ButtonOpenRight },
|
||||
StyleClasses = { StyleClass.ButtonOpenRight },
|
||||
Disabled = disable,
|
||||
Group = group,
|
||||
};
|
||||
var sub = new Button
|
||||
{
|
||||
Text = "-",
|
||||
StyleClasses = { StyleBase.ButtonOpenBoth },
|
||||
StyleClasses = { StyleClass.ButtonOpenBoth },
|
||||
Disabled = disable,
|
||||
Group = group
|
||||
};
|
||||
var plus = new Button
|
||||
{
|
||||
Text = "+",
|
||||
StyleClasses = { StyleBase.ButtonOpenLeft },
|
||||
StyleClasses = { StyleClass.ButtonOpenLeft },
|
||||
Disabled = disable,
|
||||
Group = group
|
||||
};
|
||||
|
|
@ -479,7 +478,7 @@ namespace Content.Client.Administration.UI
|
|||
|
||||
bottomButtons.AddChild(SaveButton);
|
||||
|
||||
Contents.AddChild(new BoxContainer
|
||||
ContentsContainer.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Children =
|
||||
|
|
@ -605,7 +604,7 @@ namespace Content.Client.Administration.UI
|
|||
|
||||
bottomButtons.AddChild(SaveButton);
|
||||
|
||||
Contents.AddChild(new BoxContainer
|
||||
ContentsContainer.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Children =
|
||||
|
|
|
|||
|
|
@ -7,10 +7,8 @@
|
|||
MinSize="50 50">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<GridContainer Columns="3">
|
||||
<cc:UICommandButton Command="kick" Text="{Loc admin-player-actions-window-title}" WindowType="{x:Type at:PlayerActionsWindow}" />
|
||||
<cc:CommandButton Command="banpanel" Text="{Loc admin-player-actions-window-ban}" />
|
||||
<cc:CommandButton Command="aghost" Text="{Loc admin-player-actions-window-admin-ghost}" />
|
||||
<cc:UICommandButton Command="tpto" Text="{Loc admin-player-actions-window-teleport}" WindowType="{x:Type at:TeleportWindow}" />
|
||||
<cc:CommandButton Command="permissions" Text="{Loc admin-player-actions-window-permissions}" />
|
||||
<cc:CommandButton Command="announceui" Text="{Loc admin-player-actions-window-announce}"/>
|
||||
<cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc admin-player-actions-window-title}" MinSize="425 272">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc admin-player-actions-reason}" MinWidth="100" />
|
||||
<Control MinWidth="50" />
|
||||
<LineEdit Name="ReasonLine" MinWidth="100" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<cc:PlayerListControl Name="PlayerList" VerticalExpand="True" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<controls:ConfirmButton Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
|
||||
<Button Name="SubmitAHelpButton" Text="{Loc admin-player-actions-ahelp}" Disabled="True"/>
|
||||
<controls:ConfirmButton Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
using Content.Shared.Administration;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.AdminTab
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
[UsedImplicitly]
|
||||
public sealed partial class PlayerActionsWindow : DefaultWindow
|
||||
{
|
||||
private PlayerInfo? _selectedPlayer;
|
||||
|
||||
public PlayerActionsWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
SubmitKickButton.OnPressed += SubmitKickButtonOnPressed;
|
||||
SubmitAHelpButton.OnPressed += SubmitAhelpButtonOnPressed;
|
||||
SubmitRespawnButton.OnPressed += SubmitRespawnButtonOnPressed;
|
||||
PlayerList.OnSelectionChanged += OnListOnOnSelectionChanged;
|
||||
}
|
||||
|
||||
private void OnListOnOnSelectionChanged(PlayerInfo? obj)
|
||||
{
|
||||
_selectedPlayer = obj;
|
||||
var disableButtons = _selectedPlayer == null;
|
||||
SubmitKickButton.Disabled = disableButtons;
|
||||
SubmitAHelpButton.Disabled = disableButtons;
|
||||
SubmitRespawnButton.Disabled = disableButtons;
|
||||
}
|
||||
|
||||
private void SubmitKickButtonOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (_selectedPlayer == null)
|
||||
return;
|
||||
|
||||
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
|
||||
$"kick \"{_selectedPlayer.Username}\" \"{CommandParsing.Escape(ReasonLine.Text)}\"");
|
||||
}
|
||||
|
||||
private void SubmitAhelpButtonOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (_selectedPlayer == null)
|
||||
return;
|
||||
|
||||
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
|
||||
$"openahelp \"{_selectedPlayer.SessionId}\"");
|
||||
}
|
||||
|
||||
private void SubmitRespawnButtonOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (_selectedPlayer == null)
|
||||
return;
|
||||
|
||||
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
|
||||
$"respawn \"{_selectedPlayer.Username}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Title="{Loc admin-ui-teleport}" MinSize="425 230">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<cc:PlayerListControl Name="PlayerList" />
|
||||
<Button Name="SubmitButton" Text="{Loc admin-ui-teleport}" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
using Content.Shared.Administration;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.AdminTab
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
[UsedImplicitly]
|
||||
public sealed partial class TeleportWindow : DefaultWindow
|
||||
{
|
||||
private PlayerInfo? _selectedPlayer;
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
SubmitButton.OnPressed += SubmitButtonOnOnPressed;
|
||||
PlayerList.OnSelectionChanged += OnListOnOnSelectionChanged;
|
||||
}
|
||||
|
||||
private void OnListOnOnSelectionChanged(PlayerInfo? obj)
|
||||
{
|
||||
_selectedPlayer = obj;
|
||||
SubmitButton.Disabled = _selectedPlayer == null;
|
||||
}
|
||||
|
||||
private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (_selectedPlayer == null)
|
||||
return;
|
||||
// Execute command
|
||||
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
|
||||
$"tpto \"{_selectedPlayer.Username}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ public sealed partial class PlayerTab : Control
|
|||
_config.OnValueChanged(CCVars.AdminPlayerTabColorSetting, ColorSettingChanged, true);
|
||||
_config.OnValueChanged(CCVars.AdminPlayerTabSymbolSetting, SymbolSettingChanged, true);
|
||||
|
||||
|
||||
OverlayButton.OnPressed += OverlayButtonPressed;
|
||||
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
|
||||
|
||||
|
|
|
|||
40
Content.Client/Anomaly/AnomalyScannerScreenComponent.cs
Normal file
40
Content.Client/Anomaly/AnomalyScannerScreenComponent.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using Robust.Client.Graphics;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Content.Client.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This component creates and handles the drawing of a ScreenTexture to be used on the Anomaly Scanner
|
||||
/// for an indicator of Anomaly Severity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In the future I would like to make this a more generic "DynamicTextureComponent" that can contain a dictionary
|
||||
/// of texture components like "Bar(offset, size, minimumValue, maximumValue, AppearanceKey, LayerMapKey)" that can
|
||||
/// just draw a bar or other basic drawn element that will show up on a texture layer.
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(AnomalyScannerSystem))]
|
||||
public sealed partial class AnomalyScannerScreenComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the texture drawn as a layer on the Anomaly Scanner device.
|
||||
/// </summary>
|
||||
public OwnedTexture? ScreenTexture;
|
||||
|
||||
/// <summary>
|
||||
/// A small buffer that we can reuse to draw the severity bar.
|
||||
/// </summary>
|
||||
public Rgba32[]? BarBuf;
|
||||
|
||||
/// <summary>
|
||||
/// The position of the top-left of the severity bar in pixels.
|
||||
/// </summary>
|
||||
[DataField(readOnly: true)]
|
||||
public Vector2i Offset = new Vector2i(12, 17);
|
||||
|
||||
/// <summary>
|
||||
/// The width and height of the severity bar in pixels.
|
||||
/// </summary>
|
||||
[DataField(readOnly: true)]
|
||||
public Vector2i Size = new Vector2i(10, 3);
|
||||
}
|
||||
110
Content.Client/Anomaly/AnomalyScannerSystem.cs
Normal file
110
Content.Client/Anomaly/AnomalyScannerSystem.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using System.Numerics;
|
||||
using Content.Shared.Anomaly;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Content.Client.Anomaly;
|
||||
|
||||
/// <inheritdoc cref="SharedAnomalyScannerSystem"/>
|
||||
public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
private const float MaxHueDegrees = 360f;
|
||||
private const float GreenHueDegrees = 110f;
|
||||
private const float RedHueDegrees = 0f;
|
||||
private const float GreenHue = GreenHueDegrees / MaxHueDegrees;
|
||||
private const float RedHue = RedHueDegrees / MaxHueDegrees;
|
||||
|
||||
|
||||
// Just an array to initialize the pixels of a new OwnedTexture
|
||||
private static readonly Rgba32[] EmptyTexture = new Rgba32[32*32];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AnomalyScannerScreenComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<AnomalyScannerScreenComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<AnomalyScannerScreenComponent, AppearanceChangeEvent>(OnScannerAppearanceChanged);
|
||||
}
|
||||
|
||||
private void OnComponentInit(Entity<AnomalyScannerScreenComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
if(!_sprite.TryGetLayer(ent.Owner, AnomalyScannerVisualLayers.Base, out var layer, true))
|
||||
return;
|
||||
|
||||
// Allocate the OwnedTexture
|
||||
ent.Comp.ScreenTexture = _clyde.CreateBlankTexture<Rgba32>(layer.PixelSize);
|
||||
|
||||
if (layer.PixelSize.X < ent.Comp.Offset.X + ent.Comp.Size.X ||
|
||||
layer.PixelSize.Y < ent.Comp.Offset.Y + ent.Comp.Size.Y)
|
||||
{
|
||||
// If the bar doesn't fit, just bail here, ScreenTexture and BarBuf will remain null, and appearance updates
|
||||
// will do nothing.
|
||||
DebugTools.Assert(false, "AnomalyScannerScreenComponent: Bar does not fit within sprite");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Initialize the texture
|
||||
ent.Comp.ScreenTexture.SetSubImage((0, 0), layer.PixelSize, new ReadOnlySpan<Rgba32>(EmptyTexture));
|
||||
|
||||
// Initialize bar drawing buffer
|
||||
ent.Comp.BarBuf = new Rgba32[ent.Comp.Size.X * ent.Comp.Size.Y];
|
||||
}
|
||||
|
||||
private void OnComponentStartup(Entity<AnomalyScannerScreenComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
return;
|
||||
|
||||
_sprite.LayerSetTexture((ent, sprite), AnomalyScannerVisualLayers.Screen, ent.Comp.ScreenTexture);
|
||||
}
|
||||
|
||||
private void OnScannerAppearanceChanged(Entity<AnomalyScannerScreenComponent> ent, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite is null || ent.Comp.ScreenTexture is null || ent.Comp.BarBuf is null)
|
||||
return;
|
||||
|
||||
args.AppearanceData.TryGetValue(AnomalyScannerVisuals.AnomalySeverity, out var severityObj);
|
||||
if (severityObj is not float severity)
|
||||
severity = 0;
|
||||
|
||||
// Get the bar length
|
||||
var barLength = (int)(severity * ent.Comp.Size.X);
|
||||
|
||||
// Calculate the bar color
|
||||
// Hue "angle" of two colors to interpolate between depending on severity
|
||||
// Just a lerp from Green hue at severity = 0.5 to Red hue at 1.0
|
||||
var hue = Math.Clamp(2*GreenHue * (1 - severity), RedHue, GreenHue);
|
||||
var color = new Rgba32(Color.FromHsv(new Vector4(hue, 1f, 1f, 1f)).RGBA);
|
||||
|
||||
var transparent = new Rgba32(0, 0, 0, 255);
|
||||
|
||||
for(var y = 0; y < ent.Comp.Size.Y; y++)
|
||||
{
|
||||
for (var x = 0; x < ent.Comp.Size.X; x++)
|
||||
{
|
||||
ent.Comp.BarBuf[y*ent.Comp.Size.X + x] = x < barLength ? color : transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the buffer to the texture
|
||||
try
|
||||
{
|
||||
ent.Comp.ScreenTexture.SetSubImage(
|
||||
ent.Comp.Offset,
|
||||
ent.Comp.Size,
|
||||
new ReadOnlySpan<Rgba32>(ent.Comp.BarBuf)
|
||||
);
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
Log.Warning($"Bar dimensions out of bounds with the texture on entity {ent.Owner}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ using Robust.Shared.Timing;
|
|||
|
||||
namespace Content.Client.Anomaly;
|
||||
|
||||
public sealed class AnomalySystem : SharedAnomalySystem
|
||||
public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly FloatingVisualizerSystem _floating = default!;
|
||||
|
|
@ -24,6 +24,7 @@ public sealed class AnomalySystem : SharedAnomalySystem
|
|||
|
||||
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
|
||||
{
|
||||
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
VerticalExpand="True"
|
||||
Margin="0 0 0 0"
|
||||
VerticalAlignment="Center">
|
||||
<Label Text="{Loc 'anomaly-generator-fuel-display'}" StyleClasses="StatusFieldTitle" />
|
||||
<Label Text="{Loc 'anomaly-generator-fuel-display'}" StyleClasses="highlight" />
|
||||
<ProgressBar Name="FuelBar"
|
||||
HorizontalExpand="True"
|
||||
MaxValue="1"
|
||||
|
|
@ -27,8 +27,8 @@
|
|||
Text="0 %" />
|
||||
</ProgressBar>
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="CooldownLabel" StyleClasses="StatusFieldTitle" />
|
||||
<RichTextLabel Name="ReadyLabel" StyleClasses="StatusFieldTitle" />
|
||||
<RichTextLabel Name="CooldownLabel" StyleClasses="highlight" />
|
||||
<RichTextLabel Name="ReadyLabel" StyleClasses="highlight" />
|
||||
</BoxContainer>
|
||||
<!--Sprite View-->
|
||||
<PanelContainer Margin="12 0 0 0"
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ namespace Content.Client.Arcade
|
|||
menuInnerPanel.AddChild(menuContainer);
|
||||
#endregion
|
||||
|
||||
Contents.AddChild(_mainPanel);
|
||||
ContentsContainer.AddChild(_mainPanel);
|
||||
|
||||
CanKeyboardFocus = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ namespace Content.Client.Arcade
|
|||
newGame.OnPressed += _ => OnPlayerAction?.Invoke(SharedSpaceVillainArcadeComponent.PlayerAction.NewGame);
|
||||
grid.AddChild(newGame);
|
||||
|
||||
Contents.AddChild(grid);
|
||||
ContentsContainer.AddChild(grid);
|
||||
}
|
||||
|
||||
private void UpdateMetadata(SharedSpaceVillainArcadeComponent.SpaceVillainArcadeMetaDataUpdateMessage message)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Content.Tests")]
|
||||
[assembly: InternalsVisibleTo("Content.IntegrationTests")]
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ public sealed class AlignAtmosPipeLayers : SnapgridCenter
|
|||
|
||||
var newProtoId = altPrototypes[(int)layer];
|
||||
|
||||
if (!_protoManager.TryIndex(newProtoId, out var newProto))
|
||||
if (!_protoManager.Resolve(newProtoId, out var newProto))
|
||||
return;
|
||||
|
||||
if (newProto.Type != ConstructionType.Structure)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Atmos.EntitySystems;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed class DeltaPressureSystem : SharedDeltaPressureSystem;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Content.Client.Atmos.Monitor.UI.Widgets;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.Stylesheets.Palette;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
|
|
@ -17,7 +17,7 @@ namespace Content.Client.Atmos.Monitor.UI;
|
|||
public sealed partial class AirAlarmWindow : FancyWindow
|
||||
{
|
||||
public event Action<string, IAtmosDeviceData>? AtmosDeviceDataChanged;
|
||||
public event Action<IAtmosDeviceData>? AtmosDeviceDataCopied;
|
||||
public event Action<IAtmosDeviceData>? AtmosDeviceDataCopied;
|
||||
public event Action<string, AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? AtmosAlarmThresholdChanged;
|
||||
public event Action<AirAlarmMode>? AirAlarmModeChanged;
|
||||
public event Action<bool>? AutoModeChanged;
|
||||
|
|
@ -131,7 +131,7 @@ public sealed partial class AirAlarmWindow : FancyWindow
|
|||
case GasVentPumpData pump:
|
||||
if (!_pumps.TryGetValue(addr, out var pumpControl))
|
||||
{
|
||||
var control= new PumpControl(pump, addr);
|
||||
var control = new PumpControl(pump, addr);
|
||||
control.PumpDataChanged += AtmosDeviceDataChanged;
|
||||
control.PumpDataCopied += AtmosDeviceDataCopied;
|
||||
_pumps.Add(addr, control);
|
||||
|
|
@ -186,11 +186,9 @@ public sealed partial class AirAlarmWindow : FancyWindow
|
|||
{
|
||||
return curAlarm switch
|
||||
{
|
||||
AtmosAlarmType.Danger => StyleNano.DangerousRedFore,
|
||||
AtmosAlarmType.Warning => StyleNano.ConcerningOrangeFore,
|
||||
_ => StyleNano.GoodGreenFore,
|
||||
AtmosAlarmType.Danger => Palettes.Status.Critical,
|
||||
AtmosAlarmType.Warning => Palettes.Status.Warning,
|
||||
_ => Palettes.Status.Good,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
|
@ -30,7 +31,10 @@ public sealed partial class ThresholdBoundControl : BoxContainer
|
|||
public void SetValue(float value)
|
||||
{
|
||||
_value = value;
|
||||
CSpinner.Value = ScaledValue;
|
||||
if (!CSpinner.HasKeyboardFocus())
|
||||
{
|
||||
CSpinner.Value = ScaledValue;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEnabled(bool enabled)
|
||||
|
|
@ -39,11 +43,11 @@ public sealed partial class ThresholdBoundControl : BoxContainer
|
|||
|
||||
if (enabled)
|
||||
{
|
||||
CBoundLabel.RemoveStyleClass("Disabled");
|
||||
CBoundLabel.RemoveStyleClass(StyleClass.LabelWeak);
|
||||
}
|
||||
else
|
||||
{
|
||||
CBoundLabel.SetOnlyStyleClass("Disabled");
|
||||
CBoundLabel.SetOnlyStyleClass(StyleClass.LabelWeak);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Content.Client.Atmos.Components;
|
|||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
|
|
@ -23,6 +24,7 @@ namespace Content.Client.Atmos.Overlays
|
|||
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly SharedAtmosphereSystem _atmosphereSystem;
|
||||
private readonly SharedMapSystem _mapSystem;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
|
||||
|
|
@ -54,6 +56,7 @@ namespace Content.Client.Atmos.Overlays
|
|||
{
|
||||
_entManager = entManager;
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
_atmosphereSystem = entManager.System<SharedAtmosphereSystem>();
|
||||
_mapSystem = entManager.System<SharedMapSystem>();
|
||||
_xformSys = xformSys;
|
||||
_shader = protoMan.Index(UnshadedShader).Instance();
|
||||
|
|
@ -67,7 +70,7 @@ namespace Content.Client.Atmos.Overlays
|
|||
|
||||
for (var i = 0; i < _gasCount; i++)
|
||||
{
|
||||
var gasPrototype = protoMan.Index<GasPrototype>(system.VisibleGasId[i].ToString());
|
||||
var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]);
|
||||
|
||||
SpriteSpecifier overlay;
|
||||
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ namespace Content.Client.Atmos.UI
|
|||
});
|
||||
presBox.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.##}")),
|
||||
Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.00}")),
|
||||
Align = Label.AlignMode.Right,
|
||||
HorizontalExpand = true
|
||||
});
|
||||
|
|
@ -232,8 +232,8 @@ namespace Content.Client.Atmos.UI
|
|||
tempBox.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
|
||||
("tempK", $"{gasMix.Temperature:0.#}"),
|
||||
("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.#}")),
|
||||
("tempK", $"{gasMix.Temperature:0.0}"),
|
||||
("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.0}")),
|
||||
Align = Label.AlignMode.Right,
|
||||
HorizontalExpand = true
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
MinSize="480 400" Title="Canister">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<Label Text="{Loc comp-gas-canister-ui-canister-status}" FontColorOverride="{x:Static s:StyleNano.NanoGold}" StyleClasses="LabelBig"/>
|
||||
<Label Text="{Loc comp-gas-canister-ui-canister-status}" StyleClasses="LabelHeading"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc comp-gas-canister-ui-canister-pressure}"/>
|
||||
<Label Name="CanisterPressureLabel" Align="Center" HorizontalExpand="True"/>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<Label Text="{Loc comp-gas-canister-ui-holding-tank-status}" FontColorOverride="{x:Static s:StyleNano.NanoGold}" StyleClasses="LabelBig"/>
|
||||
<Label Text="{Loc comp-gas-canister-ui-holding-tank-status}" StyleClasses="LabelHeading"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc comp-gas-canister-ui-holding-tank-label}"/>
|
||||
<Label Name="TankLabelLabel" Text="{Loc comp-gas-canister-ui-holding-tank-label-empty}" Align="Center" HorizontalExpand="True"/>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<Label Text="{Loc comp-gas-canister-ui-release-valve-status}" FontColorOverride="{x:Static s:StyleNano.NanoGold}" StyleClasses="LabelBig"/>
|
||||
<Label Text="{Loc comp-gas-canister-ui-release-valve-status}" StyleClasses="LabelHeading"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc comp-gas-canister-ui-release-pressure}"/>
|
||||
|
|
|
|||
|
|
@ -52,14 +52,18 @@ namespace Content.Client.Atmos.UI
|
|||
|
||||
private void OnSelectGasPressed()
|
||||
{
|
||||
if (_window is null) return;
|
||||
if (_window is null)
|
||||
return;
|
||||
|
||||
if (_window.SelectedGas is null)
|
||||
{
|
||||
SendMessage(new GasFilterSelectGasMessage(null));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!int.TryParse(_window.SelectedGas, out var gas)) return;
|
||||
if (!Enum.TryParse<Gas>(_window.SelectedGas, out var gas))
|
||||
return;
|
||||
|
||||
SendMessage(new GasFilterSelectGasMessage(gas));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public sealed class JukeboxBoundUserInterface : BoundUserInterface
|
|||
|
||||
_menu.SetAudioStream(jukebox.AudioStream);
|
||||
|
||||
if (_protoManager.TryIndex(jukebox.SelectedSongId, out var songProto))
|
||||
if (_protoManager.Resolve(jukebox.SelectedSongId, out var songProto))
|
||||
{
|
||||
var length = EntMan.System<AudioSystem>().GetAudioLength(songProto.Path.Path.ToString());
|
||||
_menu.SetSelectedSong(songProto.Name, (float) length.TotalSeconds);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public sealed class BarSignSystem : VisualizerSystem<BarSignComponent>
|
|||
|
||||
if (powered
|
||||
&& sign.Current != null
|
||||
&& _prototypeManager.TryIndex(sign.Current, out var proto))
|
||||
&& _prototypeManager.Resolve(sign.Current, out var proto))
|
||||
{
|
||||
SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
|
||||
sprite.LayerSetShader(0, "unshaded");
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
|
|||
|
||||
public void Update(ProtoId<BarSignPrototype>? sign)
|
||||
{
|
||||
if (_prototype.TryIndex(sign, out var signPrototype))
|
||||
if (_prototype.Resolve(sign, out var signPrototype))
|
||||
_menu?.UpdateState(signPrototype);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
using Content.Shared.Bed;
|
||||
|
||||
namespace Content.Client.Bed;
|
||||
|
||||
public sealed class BedSystem : SharedBedSystem
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -7,10 +7,7 @@
|
|||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 5">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="{xNamespace:Static style:StyleNano.ButtonColorDisabled}" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<PanelContainer StyleClasses="PanelDark">
|
||||
<Collapsible Name="Collapsible">
|
||||
<CollapsibleHeading Name="Heading" MinHeight="35"/>
|
||||
<CollapsibleBody Name="Body">
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@
|
|||
<PanelContainer
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="15">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="{xNamespace:Static style:StyleNano.PanelDark}" />
|
||||
</PanelContainer.PanelOverride>
|
||||
Margin="15"
|
||||
StyleClasses="PanelDark">
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<Control>
|
||||
<Label Text="{Loc 'cryostorage-ui-label-no-bodies'}" Name="EmptyLabel" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using Content.Shared.IdentityManagement;
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Botany.PlantAnalyzer;
|
||||
|
|
@ -14,8 +13,8 @@ namespace Content.Client.Botany.PlantAnalyzer;
|
|||
public sealed partial class PlantAnalyzerWindow : FancyWindow
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly PlantAnalyzerLocalizationHelper _localizationHelper;
|
||||
|
||||
public PlantAnalyzerWindow()
|
||||
{
|
||||
|
|
@ -23,8 +22,8 @@ public sealed partial class PlantAnalyzerWindow : FancyWindow
|
|||
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_prototypeManager = dependencies.Resolve<IPrototypeManager>();
|
||||
_gameTiming = dependencies.Resolve<IGameTiming>();
|
||||
_localizationHelper = _entityManager.System<PlantAnalyzerLocalizationHelper>();
|
||||
}
|
||||
|
||||
public void Populate(PlantAnalyzerScannedUserMessage msg)
|
||||
|
|
@ -136,7 +135,7 @@ public sealed partial class PlantAnalyzerWindow : FancyWindow
|
|||
{
|
||||
var count = msg.TrayData.Chemicals.Count;
|
||||
var holder = ContainerLabel.Text;
|
||||
var chemicals = PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(msg.TrayData.Chemicals, _prototypeManager);
|
||||
var chemicals = _localizationHelper.ChemicalsToLocalizedStrings(msg.TrayData.Chemicals);
|
||||
if (count == 0)
|
||||
ChemicalsInWaterLabel.Text = Loc.GetString("plant-analyzer-soil-empty", ("holder", holder));
|
||||
else
|
||||
|
|
@ -155,7 +154,7 @@ public sealed partial class PlantAnalyzerWindow : FancyWindow
|
|||
{
|
||||
(string, string)[] parameters = [
|
||||
("seedName", SeedLabel.Text),
|
||||
("gases", PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(msg.TolerancesData.ConsumeGasses, _prototypeManager)),
|
||||
("gases", _localizationHelper.GasesToLocalizedStrings(msg.TolerancesData.ConsumeGasses)),
|
||||
("kpa", msg.TolerancesData.IdealPressure.ToString("0.00")),
|
||||
("kpaTolerance", msg.TolerancesData.PressureTolerance.ToString("0.00")),
|
||||
("temp", msg.TolerancesData.IdealHeat.ToString("0.00")),
|
||||
|
|
@ -180,9 +179,9 @@ public sealed partial class PlantAnalyzerWindow : FancyWindow
|
|||
// Section 5: Output
|
||||
if (msg.ProduceData is not null)
|
||||
{
|
||||
var gases = PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(msg.ProduceData.ExudeGasses, _prototypeManager);
|
||||
var (produce, producePlural) = PlantAnalyzerLocalizationHelper.ProduceToLocalizedStrings(msg.ProduceData.Produce, _prototypeManager);
|
||||
var chemicals = PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(msg.ProduceData.Chemicals, _prototypeManager);
|
||||
var gases = _localizationHelper.GasesToLocalizedStrings(msg.ProduceData.ExudeGasses);
|
||||
var (produce, producePlural, firstProduce) = _localizationHelper.ProduceToLocalizedStrings(msg.ProduceData.Produce);
|
||||
var chemicals = _localizationHelper.ChemicalsToLocalizedStrings(msg.ProduceData.Chemicals);
|
||||
|
||||
(string, object)[] parameters = [
|
||||
("yield", msg.ProduceData.Yield),
|
||||
|
|
@ -190,7 +189,7 @@ public sealed partial class PlantAnalyzerWindow : FancyWindow
|
|||
("gases", gases),
|
||||
("potency", Loc.GetString(msg.ProduceData.Potency)),
|
||||
("seedless", msg.ProduceData.Seedless),
|
||||
("firstProduce", msg.ProduceData.Produce.FirstOrDefault().Id ?? ""),
|
||||
("firstProduce", firstProduce),
|
||||
("produce", produce),
|
||||
("producePlural", producePlural),
|
||||
("chemCount", msg.ProduceData.Chemicals.Count),
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue