diff --git a/.github/workflows/build-test-debug.yml b/.github/workflows/build-test-debug.yml index 17543338f0b..63c9fa75c70 100644 --- a/.github/workflows/build-test-debug.yml +++ b/.github/workflows/build-test-debug.yml @@ -45,14 +45,24 @@ jobs: - name: Сборка проекта run: dotnet build --configuration DebugOpt --no-restore /m - - name: Запуск Content.Tests - run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 + - name: Run Content.Tests + shell: pwsh + run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results" - name: Запуск Content.IntegrationTests shell: pwsh run: | $env:DOTNET_gcServer=1 - dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed + dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results" + + - name: Archive NUnit3 test results. + if: always() + uses: actions/upload-artifact@v4 + with: + name: nunit3-results-${{ matrix.os }} + path: test_results/* + retention-days: 7 + compression-level: 9 ci-success: name: CI успешно needs: diff --git a/.github/workflows/close-master-pr.yml b/.github/workflows/close-master-pr.yml index 90fb221eb60..15704ad0ce9 100644 --- a/.github/workflows/close-master-pr.yml +++ b/.github/workflows/close-master-pr.yml @@ -3,20 +3,21 @@ name: Закрытие PR из ветки master on: pull_request_target: types: [ opened, ready_for_review ] - + jobs: run: name: Закрыть PR runs-on: ubuntu-latest - if: ${{github.head_ref == 'master' || github.head_ref == 'main' || github.head_ref == 'develop'}} - - steps: + if: ${{(github.head_ref == 'master' || github.head_ref == 'main' || github.head_ref == 'develop' || github.head_ref == 'stable' || github.head_ref == 'staging') + && github.event.pull_request.head.repo.fork}} + + steps: - uses: superbrothers/close-pull-request@v3 with: - comment: "Благодарим вас за вклад в репозиторий Space Station 14. К сожалению, похоже, что вы отправили свой PR из master-ветки. Мы предлагаем вам следовать [нашей документации по использованию git](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html) \n\n Вы можете переместить текущую работу из master-ветки в другую ветку, выполнив команду `git branch <название_ветки>` и сбросив измененив в master-ветке." + comment: "Спасибо за ваш вклад! Похоже, вы создали запрос на удаление из основной ветки или другой основной ветки разработки. Это [то, чего вам следует избегать] (https://jmeridth.com/posts/do-not-issue-pull-requests-from-your-master-branch/), и, таким образом, этот запрос на удаление был автоматически закрыт. \n \n Мы рекомендуем вам следовать [нашему использованию git documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html). \n \n Вы можете перенести свою текущую работу в другую ветку, выполнив [эти команды](https://ohshitgit.com/#accidental-commit-master). Затем вы можете повторно создать свой запрос на извлечение, используя новую ветку." + + # If you prefer to just comment on the pr and not close it, uncomment the below and comment the above - # If you prefer to just comment on the pr and not close it, uncomment the bellow and comment the above - # - uses: actions/github-script@v7 # with: # script: | @@ -24,5 +25,4 @@ jobs: # issue_number: ${{ github.event.number }}, # owner: context.repo.owner, # repo: context.repo.repo, - # body: "Thank you for contributing to the Space Station 14 repository. Unfortunately, it looks like you submitted your pull request from the master branch. We suggest you follow [our git usage documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html) \n\n You can move your current work from the master branch to another branch by doing `git branch /dev/null + + token=$(curl -sS -b "$cookiejar" \ + --data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken') + + page_text=$(cat <<'EOF' + == Краткое описание == + {{Файл + |Id = {{safesubst:#replaceset:{{subst:PAGENAME}}|/^(?:[^-]+-)?(.+?)(?:\.[^.]+)?$/=$1}} + |Проект = {{safesubst:#replaceset:{{subst:PAGENAME}}|/^(?:([^-]+)-)?.*$/=\1}} + }} + + == Лицензирование == + {{CC-BY-SA-3.0}} + EOF + ) + + batch_size=50 + + for ((i=0; i<${#files[@]}; i+=batch_size)); do + batch_files=("${files[@]:i:batch_size}") + + titles=$( + for file in "${batch_files[@]}"; do + filename="$(basename "$file")" + filename_norm="$(normalize_filename "$filename")" + if [[ -n "$NAMESPACE" ]]; then + printf 'File:%s%s\n' "$NAMESPACE" "$filename_norm" + else + printf 'File:%s\n' "$filename_norm" + fi + done | paste -sd'|' - + ) + + existing_files=$( + curl -sS -b "$cookiejar" \ + --data-urlencode "action=query" \ + --data-urlencode "format=json" \ + --data-urlencode "titles=$titles" \ + --data-urlencode "prop=imageinfo" \ + --data-urlencode "iiprop=timestamp" \ + "$API" | jq -r ' + .query.pages[] + | select((has("missing") | not) and (.imageinfo? | type == "array")) + | .title + | sub("^[^:]+:"; "") + ' + ) + + for file in "${batch_files[@]}"; do + filename="$(basename "$file")" + filename_norm="$(normalize_filename "$filename")" + if [[ -n "$NAMESPACE" ]]; then + wiki_filename="${NAMESPACE}${filename_norm}" + else + wiki_filename="$filename_norm" + fi + + if grep -Fxq "$wiki_filename" <<< "$existing_files"; then + echo "Skipping existing file: $wiki_filename" + continue + fi + + echo "Uploading File:${wiki_filename}" + + curl -sS -b "$cookiejar" \ + -F "action=upload" \ + -F "createonly=true" \ + -F "filename=${wiki_filename}" \ + -F "comment=Upload $filename via GitHub Actions" \ + -F "text=$page_text" \ + -F "token=$token" \ + -F "ignorewarnings=0" \ + -F "format=json" \ + -F "file=@$file;type=image/png" \ + "$API" | jq -r '.upload.result // .error.info // "null"' || true + done + done diff --git a/.github/workflows/update-wiki.yml b/.github/workflows/update-wiki.yml index 6cc33af1ce7..e40e102e442 100644 --- a/.github/workflows/update-wiki.yml +++ b/.github/workflows/update-wiki.yml @@ -78,7 +78,7 @@ jobs: BASE="./bin/Content.Server/data" ROOT="${{ secrets.WIKI_PAGE_ROOT }}" - MODULE_ROOT="Модуль:IanComradeBot" + NAMESPACE="${{ secrets.WIKI_BOT_NAMESPACE }}" API="${{ secrets.WIKI_ROOT_URL }}/api.php" USER="${{ secrets.WIKI_BOT_USER }}" PASS="${{ secrets.WIKI_BOT_PASS }}" @@ -87,6 +87,13 @@ jobs: USER="$(printf "%s" "$USER" | tr -d '\r\n')" PASS="$(printf "%s" "$PASS" | tr -d '\r\n')" ROOT="$(printf "%s" "$ROOT" | tr -d '\r\n' | sed 's/[[:space:]]*$//')" + NAMESPACE="$(printf "%s" "$NAMESPACE" | tr -d '\r\n')" + + if [[ -n "$NAMESPACE" ]]; then + MODULE_ROOT="Модуль:IanComradeBot/$NAMESPACE" + else + MODULE_ROOT="Модуль:IanComradeBot" + fi cookiejar="$(mktemp)" trap 'rm -f "$cookiejar"' EXIT @@ -252,10 +259,16 @@ jobs: API="${{ secrets.WIKI_ROOT_URL }}/api.php" USER="${{ secrets.WIKI_BOT_USER }}" PASS="${{ secrets.WIKI_BOT_PASS }}" + NAMESPACE="${{ secrets.WIKI_BOT_NAMESPACE }}" API="$(printf "%s" "$API" | tr -d '\r\n')" USER="$(printf "%s" "$USER" | tr -d '\r\n')" PASS="$(printf "%s" "$PASS" | tr -d '\r\n')" + NAMESPACE="$(printf "%s" "$NAMESPACE" | tr -d '\r\n')" + + if [[ -n "$NAMESPACE" && "${NAMESPACE: -1}" != ":" ]]; then + NAMESPACE="${NAMESPACE}:" + fi cookiejar="$(mktemp)" trap 'rm -f "$cookiejar"' EXIT @@ -286,7 +299,17 @@ jobs: for ((i=0; i<${#pages[@]}; i+=batch_size)); do batch=("${pages[@]:i:batch_size}") - titles=$(printf '%s\n' "${batch[@]}" | cut -d'|' -f1 | paste -sd'|' -) + prefix="" + if [[ -n "$NAMESPACE" ]]; then + prefix="$NAMESPACE" + fi + + titles=$( + printf '%s\n' "${batch[@]}" | + cut -d'|' -f1 | + awk -v p="$prefix" '{print p $0}' | + paste -sd'|' - + ) existing_titles=$( curl -sS -b "$cookiejar" \ @@ -295,7 +318,7 @@ jobs: --data-urlencode "titles=$titles" \ "$API" | jq -r ' .query.pages[] - | select(.missing != true) + | select(has("missing") | not) | .title ' ) @@ -304,18 +327,18 @@ jobs: [[ -z "${name:-}" ]] && continue [[ -z "${id:-}" ]] && continue - if grep -Fxq "$name" <<< "$existing_titles"; then - echo "Skipping existing page: $name" + if grep -Fxq "$NAMESPACE$name" <<< "$existing_titles"; then + echo "Skipping existing page: $NAMESPACE$name" continue fi content="{{сущность|$id}}" - echo "Creating page: $name → $id" + echo "Creating page: $NAMESPACE$name → $id" curl -sS -b "$cookiejar" \ --data-urlencode "action=edit" \ --data-urlencode "createonly=true" \ - --data-urlencode "title=$name" \ + --data-urlencode "title=$NAMESPACE$name" \ --data-urlencode "text=$content" \ --data-urlencode "summary=Create $id via GitHub Actions" \ --data-urlencode "token=$token" \ diff --git a/Content.Benchmarks/Content.Benchmarks.csproj b/Content.Benchmarks/Content.Benchmarks.csproj index 8d4dfa31bd6..dd0b760f4ad 100644 --- a/Content.Benchmarks/Content.Benchmarks.csproj +++ b/Content.Benchmarks/Content.Benchmarks.csproj @@ -7,6 +7,7 @@ true false disable + $(DefineConstants);ALLOW_BAD_PRACTICES diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs index b31b3ed1a24..8d4929c47ff 100644 --- a/Content.Benchmarks/DeltaPressureBenchmark.cs +++ b/Content.Benchmarks/DeltaPressureBenchmark.cs @@ -1,11 +1,10 @@ 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.Atmos.EntitySystems; using Content.Shared.CCVar; using Robust.Shared; using Robust.Shared.Analyzers; diff --git a/Content.Client/Access/Systems/JobStatusSystem.cs b/Content.Client/Access/Systems/JobStatusSystem.cs new file mode 100644 index 00000000000..8327a6c198d --- /dev/null +++ b/Content.Client/Access/Systems/JobStatusSystem.cs @@ -0,0 +1,39 @@ +using Content.Client.Overlays; +using Content.Shared.Access.Systems; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; + +namespace Content.Client.Access.Systems; + +public sealed class JobStatusSystem : SharedJobStatusSystem +{ + [Dependency] private readonly ShowJobIconsSystem _showJobIcons = default!; + [Dependency] private readonly ShowCrewIconsSystem _showCrewIcons = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private static readonly ProtoId CrewBorderIcon = "CrewBorderIcon"; + private static readonly ProtoId CrewUncertainBorderIcon = "CrewUncertainBorderIcon"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetStatusIconsEvent); + } + + // show the status icons if the player has the correponding HUDs + private void OnGetStatusIconsEvent(Entity ent, ref GetStatusIconsEvent ev) + { + if (_showJobIcons.IsActive && ent.Comp.JobStatusIcon != null) + ev.StatusIcons.Add(_prototype.Index(ent.Comp.JobStatusIcon)); + + if (_showCrewIcons.IsActive) + { + if (_showCrewIcons.UncertainCrewBorder) + ev.StatusIcons.Add(_prototype.Index(CrewUncertainBorderIcon)); + else if (ent.Comp.IsCrew) + ev.StatusIcons.Add(_prototype.Index(CrewBorderIcon)); + } + } +} diff --git a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs index 801140f5172..81b1c087d88 100644 --- a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs +++ b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs @@ -1,11 +1,9 @@ using Content.Shared.Access; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; -using Content.Shared.CCVar; using Content.Shared.Containers.ItemSlots; using Content.Shared.CrewManifest; using Content.Shared.Roles; -using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using static Content.Shared.Access.Components.IdCardConsoleComponent; @@ -14,21 +12,13 @@ namespace Content.Client.Access.UI public sealed class IdCardConsoleBoundUserInterface : BoundUserInterface { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IConfigurationManager _cfgManager = default!; private readonly SharedIdCardConsoleSystem _idCardConsoleSystem = default!; private IdCardConsoleWindow? _window; - // CCVar. - private int _maxNameLength; - private int _maxIdJobLength; - public IdCardConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { _idCardConsoleSystem = EntMan.System(); - - _maxNameLength =_cfgManager.GetCVar(CCVars.MaxNameLength); - _maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength); } protected override void Open() @@ -77,12 +67,6 @@ protected override void UpdateState(BoundUserInterfaceState state) public void SubmitData(string newFullName, string newJobTitle, List> newAccessList, ProtoId newJobPrototype) { - if (newFullName.Length > _maxNameLength) - newFullName = newFullName[.._maxNameLength]; - - if (newJobTitle.Length > _maxIdJobLength) - newJobTitle = newJobTitle[.._maxIdJobLength]; - SendMessage(new WriteToTargetIdMessage( newFullName, newJobTitle, diff --git a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs index 30a7d969b61..bb44ae26155 100644 --- a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs +++ b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs @@ -23,10 +23,6 @@ public sealed partial class IdCardConsoleWindow : DefaultWindow private readonly IdCardConsoleBoundUserInterface _owner; - // CCVar. - private int _maxNameLength; - private int _maxIdJobLength; - private AccessLevelControl _accessButtons = new(); private readonly List _jobPrototypeIds = new(); @@ -46,11 +42,8 @@ public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeMana _owner = owner; - _maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength); - _maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength); - FullNameLineEdit.OnTextEntered += _ => SubmitData(); - FullNameLineEdit.IsValid = s => s.Length <= _maxNameLength; + FullNameLineEdit.IsValid = s => s.Length <= _cfgManager.GetCVar(CCVars.MaxNameLength); FullNameLineEdit.OnTextChanged += _ => { FullNameSaveButton.Disabled = FullNameSaveButton.Text == _lastFullName; @@ -58,7 +51,7 @@ public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeMana FullNameSaveButton.OnPressed += _ => SubmitData(); JobTitleLineEdit.OnTextEntered += _ => SubmitData(); - JobTitleLineEdit.IsValid = s => s.Length <= _maxIdJobLength; + JobTitleLineEdit.IsValid = s => s.Length <= _cfgManager.GetCVar(CCVars.MaxIdJobLength); JobTitleLineEdit.OnTextChanged += _ => { JobTitleSaveButton.Disabled = JobTitleLineEdit.Text == _lastJobTitle; diff --git a/Content.Client/Administration/UI/BanList/BanListEui.cs b/Content.Client/Administration/UI/BanList/BanListEui.cs index 2fca1dee523..00b27cd173f 100644 --- a/Content.Client/Administration/UI/BanList/BanListEui.cs +++ b/Content.Client/Administration/UI/BanList/BanListEui.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Linq; +using System.Numerics; using Content.Client.Administration.UI.BanList.Bans; using Content.Client.Administration.UI.BanList.RoleBans; using Content.Client.Eui; @@ -73,7 +74,7 @@ private static string FormatDate(DateTimeOffset date) return date.ToString("MM/dd/yyyy h:mm tt"); } - public static void SetData(IBanListLine line, SharedServerBan ban) where T : SharedServerBan + public static void SetData(IBanListLine line, SharedBan ban) where T : SharedBan { line.Reason.Text = ban.Reason; line.BanTime.Text = FormatDate(ban.BanTime); @@ -94,20 +95,20 @@ public static void SetData(IBanListLine line, SharedServerBan ban) where T line.BanningAdmin.Text = ban.BanningAdminName; } - private void OnLineIdsClicked(IBanListLine line) where T : SharedServerBan + private void OnLineIdsClicked(IBanListLine line) where T : SharedBan { _popup?.Close(); _popup = null; var ban = line.Ban; var id = ban.Id == null ? string.Empty : Loc.GetString("ban-list-id", ("id", ban.Id.Value)); - var ip = ban.Address == null + var ip = ban.Addresses.Length == 0 ? string.Empty - : Loc.GetString("ban-list-ip", ("ip", ban.Address.Value.address)); - var hwid = ban.HWId == null ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", ban.HWId)); - var guid = ban.UserId == null + : Loc.GetString("ban-list-ip", ("ip", string.Join(',', ban.Addresses.Select(a => a.address)))); + var hwid = ban.HWIds.Length == 0 ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", string.Join(',', ban.HWIds))); + var guid = ban.UserIds.Length == 0 ? string.Empty - : Loc.GetString("ban-list-guid", ("guid", ban.UserId.Value.ToString())); + : Loc.GetString("ban-list-guid", ("guid", string.Join(',', ban.UserIds))); _popup = new BanListIdsPopup(id, ip, hwid, guid); diff --git a/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs b/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs index 431087568a1..a79fc4a137e 100644 --- a/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs +++ b/Content.Client/Administration/UI/BanList/Bans/BanListControl.xaml.cs @@ -16,7 +16,7 @@ public BanListControl() RobustXamlLoader.Load(this); } - public void SetBans(List bans) + public void SetBans(List bans) { for (var i = Bans.ChildCount - 1; i >= 1; i--) { diff --git a/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs b/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs index 0c4e6e60d00..f1320ef7b99 100644 --- a/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs +++ b/Content.Client/Administration/UI/BanList/Bans/BanListLine.xaml.cs @@ -7,13 +7,13 @@ namespace Content.Client.Administration.UI.BanList.Bans; [GenerateTypedNameReferences] -public sealed partial class BanListLine : BoxContainer, IBanListLine +public sealed partial class BanListLine : BoxContainer, IBanListLine { - public SharedServerBan Ban { get; } + public SharedBan Ban { get; } public event Action? IdsClicked; - public BanListLine(SharedServerBan ban) + public BanListLine(SharedBan ban) { RobustXamlLoader.Load(this); diff --git a/Content.Client/Administration/UI/BanList/IBanListLine.cs b/Content.Client/Administration/UI/BanList/IBanListLine.cs index 097bae15df7..565e7072184 100644 --- a/Content.Client/Administration/UI/BanList/IBanListLine.cs +++ b/Content.Client/Administration/UI/BanList/IBanListLine.cs @@ -3,7 +3,7 @@ namespace Content.Client.Administration.UI.BanList; -public interface IBanListLine where T : SharedServerBan +public interface IBanListLine where T : SharedBan { T Ban { get; } Label Reason { get; } diff --git a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs index 1ea751deb7f..f217dec5e66 100644 --- a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs +++ b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListControl.xaml.cs @@ -16,7 +16,7 @@ public RoleBanListControl() RobustXamlLoader.Load(this); } - public void SetRoleBans(List bans) + public void SetRoleBans(List bans) { for (var i = RoleBans.ChildCount - 1; i >= 1; i--) { diff --git a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs index 4f77d662e1f..ca0d214e31e 100644 --- a/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs +++ b/Content.Client/Administration/UI/BanList/RoleBans/RoleBanListLine.xaml.cs @@ -7,13 +7,13 @@ namespace Content.Client.Administration.UI.BanList.RoleBans; [GenerateTypedNameReferences] -public sealed partial class RoleBanListLine : BoxContainer, IBanListLine +public sealed partial class RoleBanListLine : BoxContainer, IBanListLine { - public SharedServerRoleBan Ban { get; } + public SharedBan Ban { get; } public event Action? IdsClicked; - public RoleBanListLine(SharedServerRoleBan ban) + public RoleBanListLine(SharedBan ban) { RobustXamlLoader.Load(this); @@ -21,7 +21,7 @@ public RoleBanListLine(SharedServerRoleBan ban) IdsHidden.OnPressed += IdsPressed; BanListEui.SetData(this, ban); - Role.Text = ban.Role; + Role.Text = string.Join(", ", ban.Roles ?? []); } private void IdsPressed(ButtonEventArgs buttonEventArgs) diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs index 97ddc150007..569b8c9b1bf 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs @@ -70,7 +70,7 @@ private void Refresh() TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); ServerLabel.Text = Note.ServerName ?? "Unknown"; - RoundLabel.Text = Note.Round == null ? "Unknown round" : "Round " + Note.Round; + RoundLabel.Text = Note.Rounds.Length == 0 ? "Unknown round" : "Round " + string.Join(',', Note.Rounds); AdminLabel.Text = Note.CreatedByName; PlaytimeLabel.Text = $"{Note.PlaytimeAtNote.TotalHours: 0.0}h"; @@ -143,7 +143,12 @@ private string FormatBanMessage() private string FormatRoleBanMessage() { - var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} "); + var rolesText = string.Join( + ", ", + // Explicit cast here to avoid sandbox violation. + (IEnumerable?)Note.BannedRoles ?? [new BanRoleDef("what", "You should not be seeing this")]); + + var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {rolesText} "); return FormatBanMessageCommon(banMessage); } diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs index 18a50031582..e82b85acb6a 100644 --- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs +++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs @@ -32,9 +32,9 @@ public AdminNotesLinePopup(SharedAdminNote note, string playerName, bool showDel IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id)); TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType)); SeverityLabel.Text = Loc.GetString("admin-notes-severity", ("severity", note.NoteSeverity ?? NoteSeverity.None)); - RoundIdLabel.Text = note.Round == null + RoundIdLabel.Text = note.Rounds.Length == 0 ? Loc.GetString("admin-notes-round-id-unknown") - : Loc.GetString("admin-notes-round-id", ("id", note.Round)); + : Loc.GetString("admin-notes-round-id", ("id", string.Join(',', note.Rounds))); CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName)); CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))); EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName)); diff --git a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs index d96980fb1d2..a9dcfaf2b06 100644 --- a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs +++ b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs @@ -25,8 +25,8 @@ private void OnAfterHandleState(Entity ent, ref After var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap); - if (TryComp(ent, out var humanoidAppearance) && - ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite)) + if (TryComp(ent, out var humanoid) && + ent.Comp.SpeciesSprites.TryGetValue(humanoid.Species, out var speciesSprite)) { _sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite); } diff --git a/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml index 88df194ecb1..269b44f62c4 100644 --- a/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml +++ b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml @@ -1,4 +1,4 @@ - TileData = new(); + [Dependency] private readonly IOverlayManager _overlayManager = default!; + + public readonly Dictionary TileData = []; // Configuration set by debug commands and used by AtmosDebugOverlay { /// Value source for display @@ -25,6 +27,8 @@ internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem public bool CfgCBM = false; // } + private AtmosDebugOverlay? _overlay; + public override void Initialize() { base.Initialize(); @@ -34,10 +38,6 @@ public override void Initialize() SubscribeNetworkEvent(HandleAtmosDebugOverlayDisableMessage); SubscribeLocalEvent(OnGridRemoved); - - var overlayManager = IoCManager.Resolve(); - if(!overlayManager.HasOverlay()) - overlayManager.AddOverlay(new AtmosDebugOverlay(this)); } private void OnGridRemoved(GridRemovalEvent ev) @@ -51,19 +51,25 @@ private void OnGridRemoved(GridRemovalEvent ev) private void HandleAtmosDebugOverlayMessage(AtmosDebugOverlayMessage message) { TileData[GetEntity(message.GridId)] = message; + + if (_overlay is not null) + return; + + _overlay = new AtmosDebugOverlay(this); + _overlayManager.AddOverlay(_overlay); } private void HandleAtmosDebugOverlayDisableMessage(AtmosDebugOverlayDisableMessage ev) { TileData.Clear(); + RemoveOverlay(); } public override void Shutdown() { base.Shutdown(); - var overlayManager = IoCManager.Resolve(); - if (overlayManager.HasOverlay()) - overlayManager.RemoveOverlay(); + + RemoveOverlay(); } public void Reset(RoundRestartCleanupEvent ev) @@ -75,6 +81,15 @@ public bool HasData(EntityUid gridId) { return TileData.ContainsKey(gridId); } + + private void RemoveOverlay() + { + if (_overlay is null) + return; + + _overlayManager.RemoveOverlay(_overlay); + _overlay = null; + } } internal enum AtmosDebugOverlayMode : byte diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs index 17b994e64f6..d950108922c 100644 --- a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs +++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using Content.Shared.Atmos; +using Content.Shared.Atmos.Reactions; namespace Content.Client.Atmos.EntitySystems; @@ -13,6 +14,29 @@ code that would escape sandbox. As such these methods are overridden here with a implementation. */ + /// + /// No-op on client as reactions aren't entirely in shared. + /// Don't call it. Smile. + public override ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder) + { + // Reactions don't work on client so don't even try. + throw new NotImplementedException(); + } + + public override bool IsMixtureFuel(GasMixture mixture, float epsilon = Atmospherics.Epsilon) + { + var tmp = new float[Atmospherics.AdjustedNumberOfGases]; + NumericsHelpers.Multiply(mixture.Moles, GasFuelMask, tmp); + return NumericsHelpers.HorizontalAdd(tmp) > epsilon; + } + + public override bool IsMixtureOxidizer(GasMixture mixture, float epsilon = Atmospherics.Epsilon) + { + var tmp = new float[Atmospherics.AdjustedNumberOfGases]; + NumericsHelpers.Multiply(mixture.Moles, GasOxidizerMask, tmp); + return NumericsHelpers.HorizontalAdd(tmp) > epsilon; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override float GetHeatCapacityCalculation(float[] moles, bool space) { diff --git a/Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs new file mode 100644 index 00000000000..b1bfdb70abf --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/GasTileFireOverlaySystem.cs @@ -0,0 +1,30 @@ +using Content.Client.Atmos.Overlays; +using JetBrains.Annotations; +using Robust.Client.Graphics; + +namespace Content.Client.Atmos.EntitySystems; + +/// +/// System responsible for rendering atmos fire animations using . +/// +[UsedImplicitly] +public sealed class GasTileFireOverlaySystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private GasTileFireOverlay _fireOverlay = default!; + + public override void Initialize() + { + base.Initialize(); + + _fireOverlay = new GasTileFireOverlay(); + _overlayMan.AddOverlay(_fireOverlay); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlayMan.RemoveOverlay(); + } +} diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs index ad264369467..8de649b81c7 100644 --- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -1,106 +1,85 @@ -using Content.Client.Atmos.Overlays; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; using JetBrains.Annotations; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.ResourceManagement; using Robust.Shared.GameStates; -namespace Content.Client.Atmos.EntitySystems +namespace Content.Client.Atmos.EntitySystems; + +[UsedImplicitly] +public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem { - [UsedImplicitly] - public sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem + public override void Initialize() { - [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly IOverlayManager _overlayMan = default!; - [Dependency] private readonly SpriteSystem _spriteSys = default!; - [Dependency] private readonly SharedTransformSystem _xformSys = default!; + base.Initialize(); + SubscribeNetworkEvent(HandleGasOverlayUpdate); + SubscribeLocalEvent(OnHandleState); + } - private GasTileOverlay _overlay = default!; + private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args) + { + Dictionary modifiedChunks; - public override void Initialize() + switch (args.Current) { - base.Initialize(); - SubscribeNetworkEvent(HandleGasOverlayUpdate); - SubscribeLocalEvent(OnHandleState); + // is this a delta or full state? + case GasTileOverlayDeltaState delta: + { + modifiedChunks = delta.ModifiedChunks; + foreach (var index in comp.Chunks.Keys) + { + if (!delta.AllChunks.Contains(index)) + comp.Chunks.Remove(index); + } + + break; + } + case GasTileOverlayState state: + { + modifiedChunks = state.Chunks; + foreach (var index in comp.Chunks.Keys) + { + if (!state.Chunks.ContainsKey(index)) + comp.Chunks.Remove(index); + } - _overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys); - _overlayMan.AddOverlay(_overlay); + break; + } + default: + return; } - public override void Shutdown() + foreach (var (index, data) in modifiedChunks) { - base.Shutdown(); - _overlayMan.RemoveOverlay(); + comp.Chunks[index] = data; } + } - private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args) + private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev) + { + foreach (var (nent, removedIndicies) in ev.RemovedChunks) { - Dictionary modifiedChunks; + var grid = GetEntity(nent); - switch (args.Current) - { - // is this a delta or full state? - case GasTileOverlayDeltaState delta: - { - modifiedChunks = delta.ModifiedChunks; - foreach (var index in comp.Chunks.Keys) - { - if (!delta.AllChunks.Contains(index)) - comp.Chunks.Remove(index); - } + if (!TryComp(grid, out GasTileOverlayComponent? comp)) + continue; - break; - } - case GasTileOverlayState state: - { - modifiedChunks = state.Chunks; - foreach (var index in comp.Chunks.Keys) - { - if (!state.Chunks.ContainsKey(index)) - comp.Chunks.Remove(index); - } - - break; - } - default: - return; - } - - foreach (var (index, data) in modifiedChunks) + foreach (var index in removedIndicies) { - comp.Chunks[index] = data; + comp.Chunks.Remove(index); } } - private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev) + foreach (var (nent, gridData) in ev.UpdatedChunks) { - foreach (var (nent, removedIndicies) in ev.RemovedChunks) - { - var grid = GetEntity(nent); + var grid = GetEntity(nent); - if (!TryComp(grid, out GasTileOverlayComponent? comp)) - continue; + if (!TryComp(grid, out GasTileOverlayComponent? comp)) + continue; - foreach (var index in removedIndicies) - { - comp.Chunks.Remove(index); - } - } - - foreach (var (nent, gridData) in ev.UpdatedChunks) + foreach (var chunkData in gridData) { - var grid = GetEntity(nent); - - if (!TryComp(grid, out GasTileOverlayComponent? comp)) - continue; - - foreach (var chunkData in gridData) - { - comp.Chunks[chunkData.Index] = chunkData; - } + comp.Chunks[chunkData.Index] = chunkData; } } } diff --git a/Content.Client/Atmos/EntitySystems/GasTileVisibleGasOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileVisibleGasOverlaySystem.cs new file mode 100644 index 00000000000..3d7f26cc30d --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/GasTileVisibleGasOverlaySystem.cs @@ -0,0 +1,31 @@ +using Content.Client.Atmos.Overlays; +using JetBrains.Annotations; +using Robust.Client.Graphics; + +namespace Content.Client.Atmos.EntitySystems; + +/// +/// System responsible for rendering visible atmos gasses (like plasma for example) using . +/// +[UsedImplicitly] +public sealed class GasTileVisibleGasOverlaySystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private GasTileVisibleGasOverlay _visibleGasOverlay = default!; + + public override void Initialize() + { + base.Initialize(); + + _visibleGasOverlay = new GasTileVisibleGasOverlay(); + _overlayMan.AddOverlay(_visibleGasOverlay); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlayMan.RemoveOverlay(); + } + +} diff --git a/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml b/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml index f0dc29facfe..dc547dda6b1 100644 --- a/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml +++ b/Content.Client/Atmos/Monitor/UI/AirAlarmWindow.xaml @@ -7,7 +7,7 @@ - + diff --git a/Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs b/Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs new file mode 100644 index 00000000000..778a2a83b21 --- /dev/null +++ b/Content.Client/Atmos/Overlays/GasTileDangerousTemperatureOverlay.cs @@ -0,0 +1,253 @@ +using Content.Client.Atmos.EntitySystems; +using Content.Client.Graphics; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using System.Numerics; + +namespace Content.Client.Atmos.Overlays; + +/// +/// Renders a thermal heatmap overlay for gas tiles, used for equipment like thermal glasses. +/// /// +public sealed class GasTileDangerousTemperatureOverlay : Overlay +{ + public override bool RequestScreenTexture { get; set; } = false; + + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IClyde _clyde = default!; + + private GasTileOverlaySystem? _gasTileOverlay; + private readonly SharedTransformSystem _xformSys; + private EntityQuery _overlayQuery; + + private readonly OverlayResourceCache _resources = new(); + private List> _grids = new(); + + // Cache used to transform ThermalByte into Color for overlay + private readonly Color[] _colorCache = new Color[256]; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + public GasTileDangerousTemperatureOverlay() + { + IoCManager.InjectDependencies(this); + _xformSys = _entManager.System(); + + _overlayQuery = _entManager.GetEntityQuery(); + + for (byte i = 0; i <= ThermalByte.TempResolution; i++) + { + _colorCache[i] = PreCalculateColor(i); + } + + _colorCache[ThermalByte.StateVacuum] = Color.Teal; + _colorCache[ThermalByte.StateVacuum].A = 0.6f; + _colorCache[ThermalByte.AtmosImpossible] = Color.Transparent; + +#if DEBUG // This shouldn't happend so tell me if you see this LimeGreen on the screen + _colorCache[ThermalByte.ReservedFuture0] = Color.LimeGreen; + _colorCache[ThermalByte.ReservedFuture1] = Color.LimeGreen; + _colorCache[ThermalByte.ReservedFuture2] = Color.LimeGreen; +#else + _colorCache[ThermalByte.ReservedFuture0] = Color.Transparent; + _colorCache[ThermalByte.ReservedFuture1] = Color.Transparent; + _colorCache[ThermalByte.ReservedFuture2] = Color.Transparent; +#endif + } + + + /// + /// Used for Calculating onscreen color from ThermalByte core value + /// /// + private static Color PreCalculateColor(byte byteTemp) + { + // Color Thresholds in Kelvin + // -150 C + const float deepFreezeK = 123.15f; + // -50 C + const float freezeStartK = 223.15f; + // 0 C + const float waterFreezeK = 273.15f; + // 50 C + const float heatStartK = 323.15f; + // 100 C + const float waterBoilK = 373.15f; + // 300 C + const float superHeatK = 573.15f; + + var tempK = byteTemp * ThermalByte.TempDegreeResolution; + + // Neutral Zone Check (0C to 50C) + // If between 273.15K and 323.15K, it's transparent. + if (tempK >= waterFreezeK && tempK < heatStartK) + { + return Color.Transparent; + } + + Color resultingColor; + + switch (tempK) + { + case < deepFreezeK: + resultingColor = Color.FromHex("#330066"); + resultingColor.A = 0.7f; + break; + case < freezeStartK: + // Interpolate Deep Purple -> Blue + // Range: 123.15 to 223.15 (Span: 100) + resultingColor = Color.InterpolateBetween( + Color.FromHex("#330066"), + Color.Blue, + (tempK - deepFreezeK) * 0.01f); + resultingColor.A = 0.6f; + break; + case < waterFreezeK: + // Interpolate Blue -> Transparent + // Range: 223.15 to 273.15 (Span: 50) + + resultingColor = Color.InterpolateBetween( + new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.6f), + new Color(Color.Blue.R, Color.Blue.G, Color.Blue.B, 0.2f), + (tempK - freezeStartK) * 0.02f); + break; + case < waterBoilK: + // Interpolate Transparent -> Yellow + // Range: 323.15 to 373.15 (Span: 50) + + resultingColor = Color.InterpolateBetween( + new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.2f), + new Color(Color.Yellow.R, Color.Yellow.G, Color.Yellow.B, 0.6f), + (tempK - heatStartK) * 0.02f); + break; + case < superHeatK: + // Interpolate Yellow -> Red + // Range: 373.15 to 573.15 (Span: 200) + resultingColor = Color.InterpolateBetween( + Color.Yellow, + Color.Red, + (tempK - waterBoilK) * 0.005f); + resultingColor.A = 0.6f; + break; + default: + resultingColor = Color.DarkRed; + resultingColor.A = 0.7f; + break; + } + + return resultingColor; + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (args.MapId == MapId.Nullspace) + return false; + + _gasTileOverlay ??= _entManager.System(); + if (_gasTileOverlay == null) + return false; + + var target = args.Viewport.RenderTarget; + + var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); + if (res.TemperatureTarget is null || res.TemperatureTarget.Texture.Size != target.Size) + { + res.TemperatureTarget?.Dispose(); + res.TemperatureTarget = _clyde.CreateRenderTarget( + target.Size, + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), + name: nameof(GasTileDangerousTemperatureOverlay)); + } + + var drawHandle = args.WorldHandle; + var worldBounds = args.WorldBounds; + var worldAABB = args.WorldAABB; + var mapId = args.MapId; + var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix(); + + drawHandle.RenderInRenderTarget(res.TemperatureTarget, + () => + { + _grids.Clear(); + _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids); + + foreach (var grid in _grids) + { + if (!_overlayQuery.TryGetComponent(grid.Owner, out var comp)) + continue; + + var gridTileSizeVec = grid.Comp.TileSizeVector; + var gridTileCenterVec = grid.Comp.TileSizeHalfVector; + var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner); + var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal; + + drawHandle.SetTransform(gridEntToViewportLocal); + + var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner); + var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize); + + var localBounds = new Box2i( + (int)MathF.Floor(floatBounds.Left), + (int)MathF.Floor(floatBounds.Bottom), + (int)MathF.Ceiling(floatBounds.Right), + (int)MathF.Ceiling(floatBounds.Top)); + + foreach (var chunk in comp.Chunks.Values) + { + var enumerator = new GasChunkEnumerator(chunk); + while (enumerator.MoveNext(out var tileGas)) + { + var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(tilePosition)) + continue; + + var gasColor = _colorCache[tileGas.ByteGasTemperature.Value]; + + if (gasColor.A <= 0f) + continue; + + drawHandle.DrawRect( + Box2.CenteredAround(tilePosition + gridTileCenterVec, gridTileSizeVec), + gasColor + ); + } + } + } + }, + new Color(0, 0, 0, 0)); + + drawHandle.SetTransform(Matrix3x2.Identity); + + return true; + } + + protected override void Draw(in OverlayDrawArgs args) + { + var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); + + if (res.TemperatureTarget != null) + args.WorldHandle.DrawTextureRect(res.TemperatureTarget.Texture, args.WorldBounds); + args.WorldHandle.SetTransform(Matrix3x2.Identity); + } + + protected override void DisposeBehavior() + { + _resources.Dispose(); + base.DisposeBehavior(); + } + + private sealed class CachedResources : IDisposable + { + public IRenderTexture? TemperatureTarget; + + public void Dispose() + { + TemperatureTarget?.Dispose(); + } + } +} diff --git a/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs b/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs new file mode 100644 index 00000000000..310bac86692 --- /dev/null +++ b/Content.Client/Atmos/Overlays/GasTileFireOverlay.cs @@ -0,0 +1,172 @@ +using Content.Client.Atmos.EntitySystems; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Species; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Enums; +using Robust.Shared.Graphics.RSI; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using System.Numerics; + +namespace Content.Client.Atmos.Overlays; + +/// +/// Overlay responsible for rendering atmos fire animation. +/// +public sealed class GasTileFireOverlay : Overlay +{ + [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld; + private static readonly ProtoId UnshadedShader = "unshaded"; + + private readonly SharedTransformSystem _xformSys; + private readonly SharedMapSystem _mapSystem = default!; + private readonly ShaderInstance _shader; + + private readonly float[] _timer; + private readonly float[][] _frameDelays; + private readonly int[] _frameCounter; + + // TODO combine textures into a single texture atlas. + private readonly Texture[][] _frames; + + private const int FireStates = 3; + private const string FireRsiPath = "/Textures/Effects/fire.rsi"; + + public const int GasOverlayZIndex = (int)Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else + + public GasTileFireOverlay() + { + IoCManager.InjectDependencies(this); + _xformSys = _entManager.System(); + _mapSystem = _entManager.System(); + _shader = _protoMan.Index(UnshadedShader).Instance(); + ZIndex = GasOverlayZIndex; + + _timer = new float[FireStates]; + _frameDelays = new float[FireStates][]; + _frameCounter = new int[FireStates]; + _frames = new Texture[FireStates][]; + + var fire = _resourceCache.GetResource(FireRsiPath).RSI; + + for (var i = 0; i < FireStates; i++) + { + if (!fire.TryGetState((i + 1).ToString(), out var state)) + throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!"); + + _frames[i] = state.GetFrames(RsiDirection.South); + _frameDelays[i] = state.GetDelays(); + _frameCounter[i] = 0; + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + for (var i = 0; i < FireStates; i++) + { + var delays = _frameDelays[i]; + if (delays.Length == 0) + continue; + + var frameCount = _frameCounter[i]; + _timer[i] += args.DeltaSeconds; + var time = delays[frameCount]; + + if (_timer[i] < time) continue; + _timer[i] -= time; + _frameCounter[i] = (frameCount + 1) % _frames[i].Length; + } + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (args.MapId == MapId.Nullspace) + return; + + var drawHandle = args.WorldHandle; + var xformQuery = _entManager.GetEntityQuery(); + var overlayQuery = _entManager.GetEntityQuery(); + var gridState = (args.WorldBounds, + args.WorldHandle, + _frames, + _frameCounter, + _shader, + overlayQuery, + xformQuery, + _xformSys); + + var mapUid = _mapSystem.GetMapOrInvalid(args.MapId); + + if (args.Space != OverlaySpace.WorldSpaceEntities) + return; + + // TODO: WorldBounds callback. + _mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState, + static (EntityUid uid, MapGridComponent grid, + ref (Box2Rotated WorldBounds, + DrawingHandleWorld drawHandle, + Texture[][] frames, + int[] frameCounter, + ShaderInstance shader, + EntityQuery overlayQuery, + EntityQuery xformQuery, + SharedTransformSystem xformSys) state) => + { + if (!state.overlayQuery.TryGetComponent(uid, out var comp) || + !state.xformQuery.TryGetComponent(uid, out var gridXform)) + { + return true; + } + + var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform); + state.drawHandle.SetTransform(worldMatrix); + var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.TileSize); + var localBounds = new Box2i( + (int)MathF.Floor(floatBounds.Left), + (int)MathF.Floor(floatBounds.Bottom), + (int)MathF.Ceiling(floatBounds.Right), + (int)MathF.Ceiling(floatBounds.Top)); + + // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are + // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls + // by chunk, even though its currently slower. + + state.drawHandle.UseShader(state.shader); + foreach (var chunk in comp.Chunks.Values) + { + var enumerator = new GasChunkEnumerator(chunk); + + while (enumerator.MoveNext(out var gas)) + { + if (gas.FireState == 0) + continue; + + var index = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(index)) + continue; + + var fireState = gas.FireState - 1; + var texture = state.frames[fireState][state.frameCounter[fireState]]; + state.drawHandle.DrawTexture(texture, index); + } + } + + return true; + }); + + drawHandle.UseShader(null); + drawHandle.SetTransform(Matrix3x2.Identity); + } +} diff --git a/Content.Client/Atmos/Overlays/GasTileOverlay.cs b/Content.Client/Atmos/Overlays/GasTileOverlay.cs deleted file mode 100644 index eeb10b54d03..00000000000 --- a/Content.Client/Atmos/Overlays/GasTileOverlay.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System.Numerics; -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; -using Robust.Client.ResourceManagement; -using Robust.Shared.Enums; -using Robust.Shared.Graphics.RSI; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; -using Robust.Shared.Utility; - -namespace Content.Client.Atmos.Overlays -{ - public sealed class GasTileOverlay : Overlay - { - private static readonly ProtoId UnshadedShader = "unshaded"; - - private readonly IEntityManager _entManager; - private readonly IMapManager _mapManager; - private readonly SharedAtmosphereSystem _atmosphereSystem; - private readonly SharedMapSystem _mapSystem; - private readonly SharedTransformSystem _xformSys; - - public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld; - private readonly ShaderInstance _shader; - - // Gas overlays - private readonly float[] _timer; - private readonly float[][] _frameDelays; - private readonly int[] _frameCounter; - - // TODO combine textures into a single texture atlas. - private readonly Texture[][] _frames; - - // Fire overlays - private const int FireStates = 3; - private const string FireRsiPath = "/Textures/Effects/fire.rsi"; - - private readonly float[] _fireTimer = new float[FireStates]; - private readonly float[][] _fireFrameDelays = new float[FireStates][]; - private readonly int[] _fireFrameCounter = new int[FireStates]; - private readonly Texture[][] _fireFrames = new Texture[FireStates][]; - - private int _gasCount; - - public const int GasOverlayZIndex = (int) Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else - - public GasTileOverlay(GasTileOverlaySystem system, IEntityManager entManager, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys, SharedTransformSystem xformSys) - { - _entManager = entManager; - _mapManager = IoCManager.Resolve(); - _atmosphereSystem = entManager.System(); - _mapSystem = entManager.System(); - _xformSys = xformSys; - _shader = protoMan.Index(UnshadedShader).Instance(); - ZIndex = GasOverlayZIndex; - - _gasCount = system.VisibleGasId.Length; - _timer = new float[_gasCount]; - _frameDelays = new float[_gasCount][]; - _frameCounter = new int[_gasCount]; - _frames = new Texture[_gasCount][]; - - for (var i = 0; i < _gasCount; i++) - { - var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]); - - SpriteSpecifier overlay; - - if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && !string.IsNullOrEmpty(gasPrototype.GasOverlayState)) - overlay = new SpriteSpecifier.Rsi(new (gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState); - else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture)) - overlay = new SpriteSpecifier.Texture(new (gasPrototype.GasOverlayTexture)); - else - continue; - - switch (overlay) - { - case SpriteSpecifier.Rsi animated: - var rsi = resourceCache.GetResource(animated.RsiPath).RSI; - var stateId = animated.RsiState; - - if (!rsi.TryGetState(stateId, out var state)) - continue; - - _frames[i] = state.GetFrames(RsiDirection.South); - _frameDelays[i] = state.GetDelays(); - _frameCounter[i] = 0; - break; - case SpriteSpecifier.Texture texture: - _frames[i] = new[] { spriteSys.Frame0(texture) }; - _frameDelays[i] = Array.Empty(); - break; - } - } - - var fire = resourceCache.GetResource(FireRsiPath).RSI; - - for (var i = 0; i < FireStates; i++) - { - if (!fire.TryGetState((i + 1).ToString(), out var state)) - throw new ArgumentOutOfRangeException($"Fire RSI doesn't have state \"{i}\"!"); - - _fireFrames[i] = state.GetFrames(RsiDirection.South); - _fireFrameDelays[i] = state.GetDelays(); - _fireFrameCounter[i] = 0; - } - } - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - - for (var i = 0; i < _gasCount; i++) - { - var delays = _frameDelays[i]; - if (delays.Length == 0) - continue; - - var frameCount = _frameCounter[i]; - _timer[i] += args.DeltaSeconds; - var time = delays[frameCount]; - - if (_timer[i] < time) - continue; - - _timer[i] -= time; - _frameCounter[i] = (frameCount + 1) % _frames[i].Length; - } - - for (var i = 0; i < FireStates; i++) - { - var delays = _fireFrameDelays[i]; - if (delays.Length == 0) - continue; - - var frameCount = _fireFrameCounter[i]; - _fireTimer[i] += args.DeltaSeconds; - var time = delays[frameCount]; - - if (_fireTimer[i] < time) continue; - _fireTimer[i] -= time; - _fireFrameCounter[i] = (frameCount + 1) % _fireFrames[i].Length; - } - } - - protected override void Draw(in OverlayDrawArgs args) - { - if (args.MapId == MapId.Nullspace) - return; - - var drawHandle = args.WorldHandle; - var xformQuery = _entManager.GetEntityQuery(); - var overlayQuery = _entManager.GetEntityQuery(); - var gridState = (args.WorldBounds, - args.WorldHandle, - _gasCount, - _frames, - _frameCounter, - _fireFrames, - _fireFrameCounter, - _shader, - overlayQuery, - xformQuery, - _xformSys); - - var mapUid = _mapSystem.GetMapOrInvalid(args.MapId); - - if (_entManager.TryGetComponent(mapUid, out var atmos)) - DrawMapOverlay(drawHandle, args, mapUid, atmos); - - if (args.Space != OverlaySpace.WorldSpaceEntities) - return; - - // TODO: WorldBounds callback. - _mapManager.FindGridsIntersecting(args.MapId, args.WorldAABB, ref gridState, - static (EntityUid uid, MapGridComponent grid, - ref (Box2Rotated WorldBounds, - DrawingHandleWorld drawHandle, - int gasCount, - Texture[][] frames, - int[] frameCounter, - Texture[][] fireFrames, - int[] fireFrameCounter, - ShaderInstance shader, - EntityQuery overlayQuery, - EntityQuery xformQuery, - SharedTransformSystem xformSys) state) => - { - if (!state.overlayQuery.TryGetComponent(uid, out var comp) || - !state.xformQuery.TryGetComponent(uid, out var gridXform)) - { - return true; - } - - var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform); - state.drawHandle.SetTransform(worldMatrix); - var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.TileSize); - var localBounds = new Box2i( - (int) MathF.Floor(floatBounds.Left), - (int) MathF.Floor(floatBounds.Bottom), - (int) MathF.Ceiling(floatBounds.Right), - (int) MathF.Ceiling(floatBounds.Top)); - - // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are - // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls - // by chunk, even though its currently slower. - - state.drawHandle.UseShader(null); - foreach (var chunk in comp.Chunks.Values) - { - var enumerator = new GasChunkEnumerator(chunk); - - while (enumerator.MoveNext(out var gas)) - { - if (gas.Opacity == null!) - continue; - - var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); - if (!localBounds.Contains(tilePosition)) - continue; - - for (var i = 0; i < state.gasCount; i++) - { - var opacity = gas.Opacity[i]; - if (opacity > 0) - state.drawHandle.DrawTexture(state.frames[i][state.frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity)); - } - } - } - - // And again for fire, with the unshaded shader - state.drawHandle.UseShader(state.shader); - foreach (var chunk in comp.Chunks.Values) - { - var enumerator = new GasChunkEnumerator(chunk); - - while (enumerator.MoveNext(out var gas)) - { - if (gas.FireState == 0) - continue; - - var index = chunk.Origin + (enumerator.X, enumerator.Y); - if (!localBounds.Contains(index)) - continue; - - var fireState = gas.FireState - 1; - var texture = state.fireFrames[fireState][state.fireFrameCounter[fireState]]; - state.drawHandle.DrawTexture(texture, index); - } - } - - return true; - }); - - drawHandle.UseShader(null); - drawHandle.SetTransform(Matrix3x2.Identity); - } - - private void DrawMapOverlay( - DrawingHandleWorld handle, - OverlayDrawArgs args, - EntityUid map, - MapAtmosphereComponent atmos) - { - var mapGrid = _entManager.HasComponent(map); - - // map-grid atmospheres get drawn above grids - if (mapGrid && args.Space != OverlaySpace.WorldSpaceEntities) - return; - - // Normal map atmospheres get drawn below grids - if (!mapGrid && args.Space != OverlaySpace.WorldSpaceBelowWorld) - return; - - var bottomLeft = args.WorldAABB.BottomLeft.Floored(); - var topRight = args.WorldAABB.TopRight.Ceiled(); - - for (var x = bottomLeft.X; x <= topRight.X; x++) - { - for (var y = bottomLeft.Y; y <= topRight.Y; y++) - { - var tilePosition = new Vector2(x, y); - - for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++) - { - var opacity = atmos.OverlayData.Opacity[i]; - - if (opacity > 0) - handle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity)); - } - } - } - } - } -} diff --git a/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs new file mode 100644 index 00000000000..37298b95fd8 --- /dev/null +++ b/Content.Client/Atmos/Overlays/GasTileVisibleGasOverlay.cs @@ -0,0 +1,258 @@ +using Content.Client.Atmos.Components; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Enums; +using Robust.Shared.Graphics.RSI; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Numerics; +using DrawDepth = Content.Shared.DrawDepth.DrawDepth; + +namespace Content.Client.Atmos.Overlays; + +/// +/// Overlay responsible for rendering visible atmos gasses (like plasma for example) usin. +/// +public sealed class GasTileVisibleGasOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + + private static readonly ProtoId UnshadedShader = "unshaded"; + + private readonly SharedAtmosphereSystem _atmosphereSystem; + private readonly SharedMapSystem _mapSystem; + private readonly SharedTransformSystem _xformSys; + private readonly SharedGasTileOverlaySystem _gasTileOverlaySystem; + private readonly SpriteSystem _spriteSystem; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld; + private readonly ShaderInstance _shader; + + // Gas overlays + private readonly float[] _timer; + private readonly float[][] _frameDelays; + private readonly int[] _frameCounter; + + // TODO combine textures into a single texture atlas. + private readonly Texture[][] _frames; + + private readonly int _gasCount; + + public const int GasOverlayZIndex = (int)DrawDepth.Gasses; // Under ghosts and fire, above mostly everything else + + public GasTileVisibleGasOverlay() + { + IoCManager.InjectDependencies(this); + _atmosphereSystem = _entManager.System(); + _mapSystem = _entManager.System(); + _xformSys = _entManager.System(); + _gasTileOverlaySystem = _entManager.System(); + _spriteSystem = _entManager.System(); + + _shader = _protoManager.Index(UnshadedShader).Instance(); + ZIndex = GasOverlayZIndex; + + _gasCount = _gasTileOverlaySystem.VisibleGasId.Length; + _timer = new float[_gasCount]; + _frameDelays = new float[_gasCount][]; + _frameCounter = new int[_gasCount]; + _frames = new Texture[_gasCount][]; + + for (var i = 0; i < _gasCount; i++) + { + var gasPrototype = _atmosphereSystem.GetGas(_gasTileOverlaySystem.VisibleGasId[i]); + + SpriteSpecifier overlay; + + if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) && + !string.IsNullOrEmpty(gasPrototype.GasOverlayState)) + overlay = new SpriteSpecifier.Rsi(new(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState); + else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture)) + overlay = new SpriteSpecifier.Texture(new(gasPrototype.GasOverlayTexture)); + else + continue; + + switch (overlay) + { + case SpriteSpecifier.Rsi animated: + var rsi = _resourceCache.GetResource(animated.RsiPath).RSI; + var stateId = animated.RsiState; + + if (!rsi.TryGetState(stateId, out var state)) + continue; + + _frames[i] = state.GetFrames(RsiDirection.South); + _frameDelays[i] = state.GetDelays(); + _frameCounter[i] = 0; + break; + case SpriteSpecifier.Texture texture: + _frames[i] = new[] { _spriteSystem.Frame0(texture) }; + _frameDelays[i] = Array.Empty(); + break; + } + } + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + for (var i = 0; i < _gasCount; i++) + { + var delays = _frameDelays[i]; + if (delays.Length == 0) + continue; + + var frameCount = _frameCounter[i]; + _timer[i] += args.DeltaSeconds; + var time = delays[frameCount]; + + if (_timer[i] < time) + continue; + + _timer[i] -= time; + _frameCounter[i] = (frameCount + 1) % _frames[i].Length; + } + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (args.MapId == MapId.Nullspace) + return; + + var drawHandle = args.WorldHandle; + var xformQuery = _entManager.GetEntityQuery(); + var overlayQuery = _entManager.GetEntityQuery(); + var gridState = (args.WorldBounds, + args.WorldHandle, + _gasCount, + _frames, + _frameCounter, + _shader, + overlayQuery, + xformQuery, + _xformSys); + + var mapUid = _mapSystem.GetMapOrInvalid(args.MapId); + + if (_entManager.TryGetComponent(mapUid, out var atmos)) + DrawMapOverlay(drawHandle, args, mapUid, atmos); + + if (args.Space != OverlaySpace.WorldSpaceEntities) + return; + + // TODO: WorldBounds callback. + _mapManager.FindGridsIntersecting(args.MapId, + args.WorldAABB, + ref gridState, + static (EntityUid uid, + MapGridComponent grid, + ref (Box2Rotated WorldBounds, + DrawingHandleWorld drawHandle, + int gasCount, + Texture[][] frames, + int[] frameCounter, + ShaderInstance shader, + EntityQuery overlayQuery, + EntityQuery xformQuery, + SharedTransformSystem xformSys) state) => + { + if (!state.overlayQuery.TryGetComponent(uid, out var comp) || + !state.xformQuery.TryGetComponent(uid, out var gridXform)) + { + return true; + } + + var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform); + state.drawHandle.SetTransform(worldMatrix); + var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.TileSize); + var localBounds = new Box2i( + (int)MathF.Floor(floatBounds.Left), + (int)MathF.Floor(floatBounds.Bottom), + (int)MathF.Ceiling(floatBounds.Right), + (int)MathF.Ceiling(floatBounds.Top)); + + // Currently it would be faster to group drawing by gas rather than by chunk, but if the textures are + // ever moved to a single atlas, that should no longer be the case. So this is just grouping draw calls + // by chunk, even though its currently slower. + + state.drawHandle.UseShader(null); + foreach (var chunk in comp.Chunks.Values) + { + var enumerator = new GasChunkEnumerator(chunk); + + while (enumerator.MoveNext(out var gas)) + { + if (gas.Opacity == null!) + continue; + + var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); + if (!localBounds.Contains(tilePosition)) + continue; + + for (var i = 0; i < state.gasCount; i++) + { + var opacity = gas.Opacity[i]; + if (opacity > 0) + { + state.drawHandle.DrawTexture(state.frames[i][state.frameCounter[i]], + tilePosition, + Color.White.WithAlpha(opacity)); + } + } + } + } + + return true; + }); + + drawHandle.UseShader(null); + drawHandle.SetTransform(Matrix3x2.Identity); + } + + private void DrawMapOverlay( + DrawingHandleWorld handle, + OverlayDrawArgs args, + EntityUid map, + MapAtmosphereComponent atmos) + { + var mapGrid = _entManager.HasComponent(map); + + // map-grid atmospheres get drawn above grids + if (mapGrid && args.Space != OverlaySpace.WorldSpaceEntities) + return; + + // Normal map atmospheres get drawn below grids + if (!mapGrid && args.Space != OverlaySpace.WorldSpaceBelowWorld) + return; + + var bottomLeft = args.WorldAABB.BottomLeft.Floored(); + var topRight = args.WorldAABB.TopRight.Ceiled(); + + for (var x = bottomLeft.X; x <= topRight.X; x++) + { + for (var y = bottomLeft.Y; y <= topRight.Y; y++) + { + var tilePosition = new Vector2(x, y); + + for (var i = 0; i < atmos.OverlayData.Opacity.Length; i++) + { + var opacity = atmos.OverlayData.Opacity[i]; + + if (opacity > 0) + handle.DrawTexture(_frames[i][_frameCounter[i]], tilePosition, Color.White.WithAlpha(opacity)); + } + } + } + } +} diff --git a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs index ecaaca30055..b23c606285e 100644 --- a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs @@ -37,10 +37,9 @@ protected override void Open() _window.SelectGasPressed += OnSelectGasPressed; } - private void OnToggleStatusButtonPressed() + private void OnToggleStatusButtonPressed(bool status) { - if (_window is null) return; - SendMessage(new GasFilterToggleStatusMessage(_window.FilterStatus)); + SendMessage(new GasFilterToggleStatusMessage(status)); } private void OnFilterTransferRatePressed(string value) diff --git a/Content.Client/Atmos/UI/GasFilterWindow.xaml b/Content.Client/Atmos/UI/GasFilterWindow.xaml index 861d4473089..5077fddc619 100644 --- a/Content.Client/Atmos/UI/GasFilterWindow.xaml +++ b/Content.Client/Atmos/UI/GasFilterWindow.xaml @@ -1,11 +1,12 @@ - - + + + diff --git a/Content.Client/Humanoid/LayerMarkingItem.xaml.cs b/Content.Client/Humanoid/LayerMarkingItem.xaml.cs new file mode 100644 index 00000000000..65458eb7d10 --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingItem.xaml.cs @@ -0,0 +1,222 @@ +using System.Linq; +using Content.Client.Guidebook.Controls; +using Content.Shared.Body; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Input; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using Content.Corvax.Interfaces.Shared; // Corvax-Sponsors + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class LayerMarkingItem : BoxContainer, ISearchableControl +{ + [Dependency] private readonly IEntityManager _entity = default!; + private ISharedSponsorsManager? _sponsorsManager; // Corvax-Sponsors + + private readonly SpriteSystem _sprite; + + private readonly MarkingsViewModel _markingsModel; + private readonly MarkingPrototype _markingPrototype; + private readonly ProtoId _organ; + private readonly HumanoidVisualLayers _layer; + private bool _interactive; + + private List? _colorSliders; + + public event Action? Pressed; + public event Action? Unpressed; + public ProtoId MarkingId => _markingPrototype.ID; + + public LayerMarkingItem(MarkingsViewModel model, ProtoId organ, HumanoidVisualLayers layer, MarkingPrototype prototype, bool interactive) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + IoCManager.Instance!.TryResolveType(out _sponsorsManager); // Corvax-Sponsors + + _sprite = _entity.System(); + + _markingsModel = model; + _markingPrototype = prototype; + _organ = organ; + _layer = layer; + _interactive = interactive; + + UpdateData(); + UpdateSelection(); + + SelectButton.OnPressed += SelectButtonPressed; + ColorsButton.OnPressed += ColorsButtonPressed; + + OnKeyBindDown += OnPressed; + OnKeyBindUp += OnUnpressed; + + if (!interactive) + { + SelectButton.MouseFilter = Control.MouseFilterMode.Ignore; + } + } + + protected override void EnteredTree() + { + base.EnteredTree(); + + _markingsModel.MarkingsReset += UpdateSelection; + _markingsModel.MarkingsChanged += MarkingsChanged; + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _markingsModel.MarkingsReset -= UpdateSelection; + _markingsModel.MarkingsChanged -= MarkingsChanged; + } + + private void MarkingsChanged(ProtoId organ, HumanoidVisualLayers layer) + { + if (_organ != organ || _layer != layer) + return; + + UpdateSelection(); + } + + private void UpdateData() + { + MarkingTexture.Textures = _markingPrototype.Sprites.Select(layer => _sprite.Frame0(layer)).ToList(); + SelectButton.Text = Loc.GetString($"marking-{_markingPrototype.ID}"); + + // Corvax-Sponsors-Start + if (_markingPrototype.SponsorOnly && _sponsorsManager != null && _interactive) + { + SelectButton.Disabled = !_sponsorsManager.GetClientPrototypes().Contains(_markingPrototype.ID); + } + // Corvax-Sponsors-End + } + + private void UpdateSelection() + { + var selected = _markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID); + SelectButton.Pressed = selected && _interactive; + ColorsButton.Visible = selected && _interactive && _markingsModel.IsMarkingColorCustomizable(_organ, _layer, _markingPrototype.ID); + + if (!selected || !_interactive) + { + ColorsButton.Pressed = false; + ColorsContainer.Visible = false; + } + + if (_markingsModel.GetMarking(_organ, _layer, _markingPrototype.ID) is { } marking && + _colorSliders is { } sliders) + { + for (var i = 0; i < _markingPrototype.Sprites.Count; i++) + { + sliders[i].Color = marking.MarkingColors[i]; + } + } + } + + private void SelectButtonPressed(BaseButton.ButtonEventArgs args) + { + if (!_interactive) + { + SelectButton.Pressed = false; + return; + } + + if (_markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID)) + { + if (!_markingsModel.TryDeselectMarking(_organ, _layer, _markingPrototype.ID)) + { + SelectButton.Pressed = true; + } + } + else + { + if (!_markingsModel.TrySelectMarking(_organ, _layer, _markingPrototype.ID)) + { + SelectButton.Pressed = false; + } + } + } + + private void ColorsButtonPressed(BaseButton.ButtonEventArgs args) + { + ColorsContainer.Visible = ColorsButton.Pressed; + + if (_colorSliders is not null) + return; + + if (_markingsModel.GetMarking(_organ, _layer, _markingPrototype.ID) is not { } marking) + return; + + _colorSliders = new(); + + for (var i = 0; i < _markingPrototype.Sprites.Count; i++) + { + var container = new BoxContainer() + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + }; + + ColorsContainer.AddChild(container); + + var selector = new ColorSelectorSliders(); + selector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; + + var label = _markingPrototype.Sprites[i] switch + { + SpriteSpecifier.Rsi rsi => Loc.GetString($"marking-{_markingPrototype.ID}-{rsi.RsiState}"), + SpriteSpecifier.Texture texture => Loc.GetString($"marking-{_markingPrototype.ID}-{texture.TexturePath.Filename}"), + _ => throw new InvalidOperationException("SpriteSpecifier not of known type"), + }; + + container.AddChild(new Label { Text = label }); + container.AddChild(selector); + + selector.Color = marking.MarkingColors[i]; + + _colorSliders.Add(selector); + + var colorIndex = i; + selector.OnColorChanged += _ => + { + _markingsModel.TrySetMarkingColor(_organ, _layer, _markingPrototype.ID, colorIndex, selector.Color); + }; + } + } + + public bool CheckMatchesSearch(string query) + { + return Loc.GetString($"marking-{_markingPrototype.ID}").Contains(query, StringComparison.OrdinalIgnoreCase); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } + + private void OnPressed(GUIBoundKeyEventArgs args) + { + if (args.Function != EngineKeyFunctions.UIClick) + return; + + Pressed?.Invoke(args, this); + } + + private void OnUnpressed(GUIBoundKeyEventArgs args) + { + if (args.Function != EngineKeyFunctions.UIClick) + return; + + Unpressed?.Invoke(args, this); + } +} diff --git a/Content.Client/Humanoid/LayerMarkingOrderer.xaml b/Content.Client/Humanoid/LayerMarkingOrderer.xaml new file mode 100644 index 00000000000..c25a29296eb --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingOrderer.xaml @@ -0,0 +1,3 @@ + + + diff --git a/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs b/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs new file mode 100644 index 00000000000..c3275ca885d --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs @@ -0,0 +1,192 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Interaction; +using Content.Client.Stylesheets; +using Content.Shared.Body; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class LayerMarkingOrderer : BoxContainer +{ + private readonly ProtoId _organ; + private readonly HumanoidVisualLayers _layer; + private readonly MarkingsViewModel _markingsModel; + private readonly DragDropHelper _dragDropHelper; + private readonly List _beacons = new(); + private LayerDragDropBeacon? _dragTarget; + + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public LayerMarkingOrderer(MarkingsViewModel markingsModel, ProtoId organ, HumanoidVisualLayers layer) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _markingsModel = markingsModel; + _organ = organ; + _layer = layer; + _dragDropHelper = new(OnBeginDrag, OnContinueDrag, OnEndDrag); + + UpdateItems(); + } + + protected override void EnteredTree() + { + base.EnteredTree(); + + _markingsModel.MarkingsReset += UpdateItems; + _markingsModel.MarkingsChanged += MarkingsChanged; + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _markingsModel.MarkingsReset -= UpdateItems; + _markingsModel.MarkingsChanged -= MarkingsChanged; + } + + private void MarkingsChanged(ProtoId organ, HumanoidVisualLayers layer) + { + if (_organ != organ || _layer != layer) + return; + + UpdateItems(); + } + + private void UpdateItems() + { + Items.RemoveAllChildren(); + _beacons.Clear(); + + if (_markingsModel.SelectedMarkings(_organ, _layer) is not { } markings) + return; + + for (var idx = 0; idx < markings.Count; idx++) + { + var marking = markings[idx]; + + var container = new LayerMarkingItemContainer(); + container.Margin = new(4); + + var item = new LayerMarkingItem(_markingsModel, _organ, _layer, _prototype.Index(marking.MarkingId), false); + item.DefaultCursorShape = CursorShape.Hand; + item.Pressed += (args, control) => OnItemPressed(args, control, container); + item.Unpressed += OnItemUnpressed; + + container.AddChild(item); + + var before = new LayerDragDropBeacon(CandidatePosition.Before, idx); + var after = new LayerDragDropBeacon(CandidatePosition.After, idx); + _beacons.Add(before); + _beacons.Add(after); + + Items.AddChild(before); + Items.AddChild(container); + Items.AddChild(after); + } + } + + private void OnItemPressed(GUIBoundKeyEventArgs args, LayerMarkingItem control, LayerMarkingItemContainer container) + { + _dragDropHelper.MouseDown(new(control, container)); + } + + private void OnItemUnpressed(GUIBoundKeyEventArgs args, LayerMarkingItem control) + { + _dragDropHelper.EndDrag(); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + _dragDropHelper.Update(args.DeltaSeconds); + } + + private bool OnBeginDrag() + { + var (item, container) = _dragDropHelper.Dragged; + + container.Visible = false; + item.Orphan(); + item.DefaultCursorShape = CursorShape.Move; + UserInterfaceManager.PopupRoot.AddChild(item); + LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32)); + return true; + } + + private bool OnContinueDrag(float frameTime) + { + var (item, container) = _dragDropHelper.Dragged; + + LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32)); + + var closestBeacon = + _beacons.MinBy(beacon => + (UserInterfaceManager.MousePositionScaled.Position - beacon.GlobalPosition).LengthSquared()); + + if (closestBeacon != _dragTarget) + { + _dragTarget?.UnbecomeTarget(); + _dragTarget = closestBeacon; + _dragTarget?.BecomeTarget(); + } + + return true; + } + + private void OnEndDrag() + { + var (item, container) = _dragDropHelper.Dragged; + + container.Visible = true; + item.Orphan(); + container.AddChild(item); + _dragTarget?.UnbecomeTarget(); + + if (_dragTarget != null) + { + _markingsModel.ChangeMarkingOrder(_organ, _layer, item.MarkingId, _dragTarget.CandidatePosition, _dragTarget.Index); + } + } +} + +internal readonly record struct LayerMarkingDragged(LayerMarkingItem Item, LayerMarkingItemContainer Container); + +internal sealed class LayerMarkingItemContainer : PanelContainer +{ + public LayerMarkingItemContainer() + { + SetHeight = 64; + HorizontalExpand = true; + } +} + +internal sealed class LayerDragDropBeacon(CandidatePosition position, int index) : PanelContainer +{ + public readonly CandidatePosition CandidatePosition = position; + public readonly int Index = index; + + public void BecomeTarget() + { + SetHeight = 64; + HorizontalExpand = true; + SetOnlyStyleClass(StyleClass.PanelDropTarget); + } + + public void UnbecomeTarget() + { + SetHeight = float.NaN; + RemoveStyleClass(StyleClass.PanelDropTarget); + } +} diff --git a/Content.Client/Humanoid/LayerMarkingPicker.xaml b/Content.Client/Humanoid/LayerMarkingPicker.xaml new file mode 100644 index 00000000000..c5804584ac2 --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingPicker.xaml @@ -0,0 +1,12 @@ + + + + + + + + + + @@ -18,9 +29,13 @@ diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs index 64d1f78d3ce..66530eba917 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using Content.Client.Decals.UI; using Content.Client.Stylesheets; using Content.Shared.Decals; using Robust.Client.AutoGenerated; @@ -8,6 +8,8 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; +using System.Linq; +using System.Numerics; namespace Content.Client.SprayPainter.UI; @@ -21,6 +23,9 @@ public sealed partial class SprayPainterDecals : Control public Action? OnColorChanged; public Action? OnAngleChanged; public Action? OnSnapChanged; + public Action? OnColorPickerToggled; + + private PaletteColorPicker? _palette; private SpriteSystem? _sprite; private string _selectedDecal = string.Empty; @@ -30,14 +35,17 @@ public SprayPainterDecals() { RobustXamlLoader.Load(this); - AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90; - SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90; + AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value + 90) % 360; + SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value - 90) % 360; SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0; AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value); UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed; SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed; ColorSelector.OnColorChanged += OnColorSelected; + + ColorPalette.OnPressed += ColorPaletteOnPressed; + ColorPicker.OnPressed += args => OnColorPickerToggled?.Invoke(args.Button.Pressed); } private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _) @@ -147,6 +155,7 @@ private void DecalButtonOnPressed(BaseButton.ButtonEventArgs obj) public void SetSelectedDecal(string name) { _selectedDecal = name; + SelectedDecalName.Text = name; if (_sprite is null) return; @@ -171,4 +180,35 @@ public void SetSnap(bool snap) { SnapToTileCheckBox.Pressed = snap; } + + private void ColorPaletteOnPressed(BaseButton.ButtonEventArgs _) + { + // Code copied from other implementations of `PaletteColorPicker`. + if (_palette is null) + { + _palette = new PaletteColorPicker(); + _palette.OpenCenteredLeft(); + _palette.PaletteList.OnItemSelected += args => + { + var color = (args.ItemList.GetSelected().First().Metadata as Color?)!.Value; + ColorSelector.Color = color; + OnColorSelected(color); + }; + return; + } + + if (_palette.IsOpen) + { + _palette.Close(); + } + else + { + _palette.Open(); + } + } + + public void SetColorPicker(bool enabled) + { + ColorPicker.Pressed = enabled; + } } diff --git a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs index eb1218ad678..2f727960438 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs @@ -30,6 +30,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow public event Action? OnDecalColorChanged; public event Action? OnDecalAngleChanged; public event Action? OnDecalSnapChanged; + public event Action? OnDecalColorPickerToggled; // Pipe color data private ItemList _colorList = default!; @@ -195,6 +196,7 @@ public void PopulateCategories(Dictionary _sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color); _sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle); _sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap); + _sprayPainterDecals.OnColorPickerToggled += toggle => OnDecalColorPickerToggled?.Invoke(toggle); Tabs.AddChild(_sprayPainterDecals); TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals")); @@ -298,7 +300,12 @@ public void SetDecalSnap(bool snap) if (_sprayPainterDecals != null) _sprayPainterDecals.SetSnap(snap); } - # endregion + + public void SetDecalColorPicker(bool colorPickerEnabled) + { + _sprayPainterDecals?.SetColorPicker(colorPickerEnabled); + } + #endregion } public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData; diff --git a/Content.Client/StatusIcon/StatusIconOverlay.cs b/Content.Client/StatusIcon/StatusIconOverlay.cs index 4d3be5439cf..7064be5bab2 100644 --- a/Content.Client/StatusIcon/StatusIconOverlay.cs +++ b/Content.Client/StatusIcon/StatusIconOverlay.cs @@ -96,7 +96,7 @@ protected override void Draw(in OverlayDrawArgs args) countL++; } yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetL - proto.Offset) / EyeManager.PixelsPerMeter; - xOffset = -(bounds.Width + sprite.Offset.X) / 2f; + xOffset = -(bounds.Width + sprite.Offset.X) / 2f + (float)proto.OffsetHorizontal / EyeManager.PixelsPerMeter; } else @@ -109,7 +109,7 @@ protected override void Draw(in OverlayDrawArgs args) countR++; } yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetR - proto.Offset) / EyeManager.PixelsPerMeter; - xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float)texture.Width / EyeManager.PixelsPerMeter; + xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float)(texture.Width - proto.OffsetHorizontal) / EyeManager.PixelsPerMeter; } diff --git a/Content.Client/Stylesheets/CommonStylesheet.cs b/Content.Client/Stylesheets/CommonStylesheet.cs index f8eae88b38e..72b58e56483 100644 --- a/Content.Client/Stylesheets/CommonStylesheet.cs +++ b/Content.Client/Stylesheets/CommonStylesheet.cs @@ -6,7 +6,7 @@ namespace Content.Client.Stylesheets; public abstract class CommonStylesheet : PalettedStylesheet, IButtonConfig, IWindowConfig, IIconConfig, ITabContainerConfig, ISliderConfig, IRadialMenuConfig, IPlaceholderConfig, ITooltipConfig, IPanelConfig, INanoHeadingConfig, - ILineEditConfig, IStripebackConfig, ICheckboxConfig + ILineEditConfig, IStripebackConfig, ICheckboxConfig, ISwitchButtonConfig { /// /// This constructor will not access any virtual or abstract properties, so you can set them from your config. @@ -73,4 +73,11 @@ protected CommonStylesheet(object config) : base(config) { } ColorPalette IButtonConfig.ButtonPalette => PrimaryPalette with { PressedElement = PositivePalette.PressedElement }; ColorPalette IButtonConfig.PositiveButtonPalette => PositivePalette; ColorPalette IButtonConfig.NegativeButtonPalette => NegativePalette; + + ResPath ISwitchButtonConfig.SwitchButtonTrackFillPath => new("switchbutton_track_fill.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonTrackOutlinePath => new("switchbutton_track_outline.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonThumbFillPath => new("switchbutton_thumb_fill.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonThumbOutlinePath => new("switchbutton_thumb_outline.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonSymbolOffPath => new("switchbutton_symbol_off.svg.96dpi.png"); + ResPath ISwitchButtonConfig.SwitchButtonSymbolOnPath => new("switchbutton_symbol_on.svg.96dpi.png"); } diff --git a/Content.Client/Stylesheets/SheetletConfigs/ISwitchButtonConfig.cs b/Content.Client/Stylesheets/SheetletConfigs/ISwitchButtonConfig.cs new file mode 100644 index 00000000000..0697bc96263 --- /dev/null +++ b/Content.Client/Stylesheets/SheetletConfigs/ISwitchButtonConfig.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Stylesheets.SheetletConfigs; + +public interface ISwitchButtonConfig +{ + public ResPath SwitchButtonTrackFillPath { get; } + public ResPath SwitchButtonTrackOutlinePath { get; } + public ResPath SwitchButtonThumbFillPath { get; } + public ResPath SwitchButtonThumbOutlinePath { get; } + public ResPath SwitchButtonSymbolOffPath { get; } + public ResPath SwitchButtonSymbolOnPath { get; } +} + diff --git a/Content.Client/Stylesheets/Sheetlets/ButtonSheetlet.cs b/Content.Client/Stylesheets/Sheetlets/ButtonSheetlet.cs index 51ba2063928..8b92e1410aa 100644 --- a/Content.Client/Stylesheets/Sheetlets/ButtonSheetlet.cs +++ b/Content.Client/Stylesheets/Sheetlets/ButtonSheetlet.cs @@ -19,6 +19,7 @@ public override StyleRule[] GetRules(T sheet, object config) var crossTex = sheet.GetTextureOr(iconCfg.CrossIconPath, NanotrasenStylesheet.TextureRoot); var refreshTex = sheet.GetTextureOr(iconCfg.RefreshIconPath, NanotrasenStylesheet.TextureRoot); + var helpTex = sheet.GetTextureOr(iconCfg.HelpIconPath, NanotrasenStylesheet.TextureRoot); var rules = new List { @@ -56,6 +57,11 @@ public override StyleRule[] GetRules(T sheet, object config) .Class(StyleClass.RefreshButton) .Prop(TextureButton.StylePropertyTexture, refreshTex), + // Help button + E() + .Class(StyleClass.HelpButton) + .Prop(TextureButton.StylePropertyTexture, helpTex), + // Ensure labels in buttons are aligned. E