mirror of
https://github.com/corvax-team/ss14-wl.git
synced 2026-06-09 10:06:46 +02:00
Merge remote-tracking branch 'corvax/master' into upstream
This commit is contained in:
@@ -45,13 +45,23 @@ jobs:
|
||||
run: dotnet build --configuration DebugOpt --no-restore /m
|
||||
|
||||
- name: Run Content.Tests
|
||||
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
|
||||
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: Run 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: Build & Test Debug
|
||||
needs:
|
||||
|
||||
@@ -3,19 +3,20 @@ name: Close PRs on master
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, ready_for_review ]
|
||||
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{github.head_ref == 'master' || github.head_ref == 'main' || github.head_ref == 'develop'}}
|
||||
|
||||
steps:
|
||||
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: |
|
||||
@@ -23,5 +24,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 <branch_name` and resetting the master branch. \n\n This pr won't be automatically closed. However, a maintainer may close it for this reason."
|
||||
# })
|
||||
# body: "Thank you for your contribution! It appears you created a pull request from the master branch or another main development branch. This is [something you should avoid doing](https://jmeridth.com/posts/do-not-issue-pull-requests-from-your-master-branch/)\n\nYou can move your current work to another branch by following [these commands](https://ohshitgit.com/#accidental-commit-master). Then, you may recreate your pull request using the new branch. \n\n This pull request won't be automatically closed. However, a maintainer may close it for this reason."})
|
||||
|
||||
@@ -62,10 +62,11 @@ jobs:
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Corvax-Secrets-End
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
# ubuntu-latest has .NET 10
|
||||
# - name: Setup .NET Core
|
||||
# uses: actions/setup-dotnet@v4.1.0
|
||||
# with:
|
||||
# dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
@@ -74,7 +75,13 @@ jobs:
|
||||
run: dotnet build Content.Packaging --configuration Release --no-restore /m
|
||||
|
||||
- name: Package server
|
||||
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
|
||||
run: dotnet run --project Content.Packaging server --log-build --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
|
||||
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
run: dotnet run --project Content.Packaging client --log-build --no-wipe-release
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binlogs
|
||||
path: release/*.binlog
|
||||
retention-days: 7
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
name: Update Wiki Images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master, jsondump ]
|
||||
paths:
|
||||
- '.github/workflows/update-wiki-images.yml'
|
||||
- 'Resources/Prototypes/**'
|
||||
- 'Resources/Textures/**'
|
||||
- 'RobustToolbox/**'
|
||||
|
||||
jobs:
|
||||
update-wiki-images:
|
||||
name: Generate and Upload Entity Images
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Pull Engine Updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
|
||||
- name: Update Engine Submodules
|
||||
run: |
|
||||
cd RobustToolbox/
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Linux graphics dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xvfb libgl1-mesa-dri libglu1-mesa mesa-utils libopenal1
|
||||
|
||||
- name: Install Dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build Project
|
||||
run: dotnet build --configuration Release --no-restore /p:WarningsAsErrors=nullable /m
|
||||
|
||||
- name: Generate entity images
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
env:
|
||||
LIBGL_ALWAYS_SOFTWARE: "1"
|
||||
MESA_GL_VERSION_OVERRIDE: "3.3"
|
||||
MESA_GLSL_VERSION_OVERRIDE: "330"
|
||||
SDL_AUDIODRIVER: "dummy"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xvfb-run -a dotnet ./bin/Content.Client/Content.Client.dll --self-contained \
|
||||
--cvar autogen.entity_screenshot.enabled=true \
|
||||
--cvar audio.interface_volume=0 \
|
||||
--cvar ambience.lobby_music_enabled=false \
|
||||
--cvar audio.admin_sounds_enabled=false \
|
||||
--cvar audio.bwoink_sound_enabled=false \
|
||||
--cvar interface.click_sound='' \
|
||||
--cvar interface.hover_sound=''
|
||||
|
||||
- name: Build animated PNGs
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
python -m pip install --disable-pip-version-check pillow
|
||||
|
||||
python <<'PY'
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from PIL import Image
|
||||
|
||||
base = Path("./bin/Content.Client/user_data/Textures/Entities")
|
||||
animated_root = base / "_animated"
|
||||
|
||||
if not animated_root.is_dir():
|
||||
print(f"No animated frame directory found at {animated_root}")
|
||||
raise SystemExit(0)
|
||||
|
||||
for entity_dir in sorted(path for path in animated_root.iterdir() if path.is_dir()):
|
||||
metadata = entity_dir / "frames.txt"
|
||||
if not metadata.is_file():
|
||||
print(f"Skipping {entity_dir.name}: no frames.txt")
|
||||
continue
|
||||
|
||||
frames = []
|
||||
durations = []
|
||||
|
||||
with metadata.open("r", encoding="utf-8") as handle:
|
||||
for raw_line in handle:
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
filename, delay_ms = line.split("\t", 1)
|
||||
frame_path = entity_dir / filename
|
||||
if not frame_path.is_file():
|
||||
raise FileNotFoundError(f"Missing frame {frame_path}")
|
||||
|
||||
frames.append(frame_path)
|
||||
durations.append(max(1, int(delay_ms)))
|
||||
|
||||
if not frames:
|
||||
print(f"Skipping {entity_dir.name}: no frames listed")
|
||||
continue
|
||||
|
||||
output = base / f"{entity_dir.name}.png"
|
||||
|
||||
if len(frames) == 1:
|
||||
shutil.copyfile(frames[0], output)
|
||||
print(f"Copied single-frame animation to {output.name}")
|
||||
continue
|
||||
|
||||
images = []
|
||||
try:
|
||||
with Image.open(frames[0]) as first_src:
|
||||
canvas_size = first_src.convert("RGBA").size
|
||||
|
||||
for frame_path in frames:
|
||||
with Image.open(frame_path) as src:
|
||||
img = src.convert("RGBA")
|
||||
if img.size == canvas_size:
|
||||
images.append(img.copy())
|
||||
else:
|
||||
canvas = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
|
||||
canvas.paste(img, (0, 0))
|
||||
images.append(canvas)
|
||||
|
||||
first, *rest = images
|
||||
first.save(
|
||||
output,
|
||||
format="PNG",
|
||||
save_all=True,
|
||||
append_images=rest,
|
||||
duration=durations,
|
||||
loop=0,
|
||||
disposal=1,
|
||||
blend=0,
|
||||
)
|
||||
print(f"Built animated PNG {output.name} from {len(frames)} frames")
|
||||
finally:
|
||||
for image in images:
|
||||
image.close()
|
||||
PY
|
||||
|
||||
- name: Upload entity images to wiki
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BASE="./bin/Content.Client/user_data/Textures/Entities"
|
||||
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' | sed 's/[[:space:]]*$//')"
|
||||
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
|
||||
|
||||
if [[ ! -d "$BASE" ]]; then
|
||||
echo "Entity image directory not found: $BASE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
normalize_filename() {
|
||||
awk 'BEGIN { print toupper(substr(ARGV[1],1,1)) substr(ARGV[1],2) }' "$1"
|
||||
}
|
||||
|
||||
files=()
|
||||
while IFS= read -r -d '' file; do
|
||||
files+=("$file")
|
||||
done < <(find "$BASE" -maxdepth 1 -type f -name '*.png' -print0 | sort -z)
|
||||
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
echo "No PNG files found in $BASE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cookiejar="$(mktemp)"
|
||||
trap 'rm -f "$cookiejar"' EXIT
|
||||
|
||||
login_token=$(curl -sS -c "$cookiejar" \
|
||||
--data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken')
|
||||
|
||||
curl -sS -c "$cookiejar" -b "$cookiejar" \
|
||||
--data-urlencode "action=login" \
|
||||
--data-urlencode "lgname=$USER" \
|
||||
--data-urlencode "lgpassword=$PASS" \
|
||||
--data-urlencode "lgtoken=$login_token" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" > /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
|
||||
@@ -18,83 +18,412 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
- name: Detect changed paths
|
||||
id: changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
general:
|
||||
- '.github/workflows/update-wiki.yml'
|
||||
- 'Content.Shared/**'
|
||||
- 'Content.Server/**'
|
||||
- 'Content.Client/**'
|
||||
- 'Resources/**'
|
||||
- 'RobustToolbox/**'
|
||||
|
||||
- name: Pull Engine Updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
prototypes:
|
||||
- '.github/workflows/update-wiki.yml'
|
||||
- 'Resources/Prototypes/**'
|
||||
|
||||
- name: Update Engine Submodules
|
||||
run: |
|
||||
cd RobustToolbox/
|
||||
git submodule update --init --recursive
|
||||
- name: Setup Submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
- name: Pull Engine Updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
|
||||
- name: Install Dependencies
|
||||
run: dotnet restore
|
||||
- name: Update Engine Submodules
|
||||
run: |
|
||||
cd RobustToolbox/
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Build Project
|
||||
run: dotnet build --configuration Release --no-restore /p:WarningsAsErrors=nullable /m
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Generate JSON blobs for prototypes
|
||||
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
|
||||
continue-on-error: true
|
||||
- name: Install Dependencies
|
||||
run: dotnet restore
|
||||
|
||||
# Проходит по всем JSON-файлам в директории BASE и загружает каждый файл как страницу в MediaWiki.
|
||||
# Имя страницы формируется из относительного пути к файлу.
|
||||
- name: Upload JSON files to wiki
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
- name: Build Project
|
||||
run: dotnet build --configuration Release --no-restore /p:WarningsAsErrors=nullable /m
|
||||
|
||||
BASE="./bin/Content.Server/data"
|
||||
ROOT="${{ secrets.WIKI_PAGE_ROOT }}"
|
||||
API="${{ secrets.WIKI_ROOT_URL }}/api.php"
|
||||
USER="${{ secrets.WIKI_BOT_USER }}"
|
||||
PASS="${{ secrets.WIKI_BOT_PASS }}"
|
||||
- name: Generate JSON blobs for prototypes
|
||||
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
|
||||
continue-on-error: true
|
||||
|
||||
API="$(printf "%s" "$API" | tr -d '\r\n' | sed 's/[[:space:]]*$//')"
|
||||
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:]]*$//')"
|
||||
# Проходит по всем JSON-файлам в директории BASE и загружает каждый файл как страницу в MediaWiki.
|
||||
# Имя страницы формируется из относительного пути к файлу.
|
||||
- name: Upload JSON files to wiki
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || steps.changes.outputs.general == 'true' }}
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cookiejar="$(mktemp)"
|
||||
trap 'rm -f "$cookiejar"' EXIT
|
||||
BASE="./bin/Content.Server/data"
|
||||
ROOT="${{ secrets.WIKI_PAGE_ROOT }}"
|
||||
NAMESPACE="${{ secrets.WIKI_BOT_NAMESPACE }}"
|
||||
API="${{ secrets.WIKI_ROOT_URL }}/api.php"
|
||||
USER="${{ secrets.WIKI_BOT_USER }}"
|
||||
PASS="${{ secrets.WIKI_BOT_PASS }}"
|
||||
|
||||
login_token=$(curl -sS -c "$cookiejar" --data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken')
|
||||
curl -sS -c "$cookiejar" -b "$cookiejar" \
|
||||
--data-urlencode "action=login" \
|
||||
--data-urlencode "lgname=$USER" \
|
||||
--data-urlencode "lgpassword=$PASS" \
|
||||
--data-urlencode "lgtoken=$login_token" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" > /dev/null
|
||||
API="$(printf "%s" "$API" | tr -d '\r\n' | sed 's/[[:space:]]*$//')"
|
||||
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')"
|
||||
|
||||
find "$BASE" -type f -name '*.json' | while IFS= read -r file; do
|
||||
rel="${file#$BASE/}"
|
||||
rel="$(printf "%s" "$rel" | tr -d '\r\n' | sed 's/:/_/g')"
|
||||
page="$ROOT/$rel"
|
||||
echo "Uploading $rel → $page"
|
||||
if [[ -n "$NAMESPACE" ]]; then
|
||||
MODULE_ROOT="Модуль:IanComradeBot/$NAMESPACE"
|
||||
else
|
||||
MODULE_ROOT="Модуль:IanComradeBot"
|
||||
fi
|
||||
|
||||
token=$(curl -sS -b "$cookiejar" --data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken')
|
||||
cookiejar="$(mktemp)"
|
||||
trap 'rm -f "$cookiejar"' EXIT
|
||||
|
||||
curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=edit" \
|
||||
--data-urlencode "title=$page" \
|
||||
--data-urlencode "summary=Update $rel via GitHub Actions" \
|
||||
--data-urlencode "text@${file}" \
|
||||
--data-urlencode "token=$token" \
|
||||
--data-urlencode "bot=true" \
|
||||
--data-urlencode "minor=true" \
|
||||
--data-urlencode "assert=bot" \
|
||||
login_token=$(curl -sS -c "$cookiejar" \
|
||||
--data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken')
|
||||
|
||||
curl -sS -c "$cookiejar" -b "$cookiejar" \
|
||||
--data-urlencode "action=login" \
|
||||
--data-urlencode "lgname=$USER" \
|
||||
--data-urlencode "lgpassword=$PASS" \
|
||||
--data-urlencode "lgtoken=$login_token" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" | jq -r '.'
|
||||
done
|
||||
"$API" > /dev/null
|
||||
|
||||
token=$(curl -sS -b "$cookiejar" \
|
||||
--data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken')
|
||||
|
||||
edit_page_file() {
|
||||
local title="$1"
|
||||
local summary="$2"
|
||||
local textfile="$3"
|
||||
local token="$4"
|
||||
|
||||
curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=edit" \
|
||||
--data-urlencode "title=$title" \
|
||||
--data-urlencode "summary=$summary" \
|
||||
--data-urlencode "text@${textfile}" \
|
||||
--data-urlencode "token=$token" \
|
||||
--data-urlencode "bot=true" \
|
||||
--data-urlencode "minor=true" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" | jq -r '.edit.result // "null"'
|
||||
}
|
||||
|
||||
edit_page() {
|
||||
local title="$1"
|
||||
local summary="$2"
|
||||
local text="$3"
|
||||
local token="$4"
|
||||
|
||||
curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=edit" \
|
||||
--data-urlencode "title=$title" \
|
||||
--data-urlencode "summary=$summary" \
|
||||
--data-urlencode "text=$text" \
|
||||
--data-urlencode "token=$token" \
|
||||
--data-urlencode "bot=true" \
|
||||
--data-urlencode "minor=true" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" | jq -r '.edit.result // "null"'
|
||||
}
|
||||
|
||||
query_page_linecounts() {
|
||||
local -n titles_ref=$1
|
||||
local -n linecounts_ref=$2
|
||||
linecounts_ref=()
|
||||
|
||||
(( ${#titles_ref[@]} == 0 )) && return 0
|
||||
|
||||
local titles
|
||||
titles=$(printf '%s|' "${titles_ref[@]}")
|
||||
titles="${titles%|}"
|
||||
|
||||
local resp
|
||||
resp=$(curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=query" \
|
||||
--data-urlencode "format=json" \
|
||||
--data-urlencode "formatversion=2" \
|
||||
--data-urlencode "prop=revisions" \
|
||||
--data-urlencode "rvprop=content" \
|
||||
--data-urlencode "rvslots=main" \
|
||||
--data-urlencode "titles=$titles" \
|
||||
"$API")
|
||||
|
||||
while IFS=$'\t' read -r title linecount; do
|
||||
[[ -z "${title:-}" ]] && continue
|
||||
[[ -z "${linecount:-}" ]] && continue
|
||||
linecounts_ref["$title"]="$linecount"
|
||||
done < <(
|
||||
jq -r '
|
||||
.query.pages[]
|
||||
| [.title, ((.revisions[0].slots.main.content? // "")| split("\n")| length)]
|
||||
| @tsv
|
||||
' <<< "$resp"
|
||||
)
|
||||
}
|
||||
|
||||
count_lines() {
|
||||
awk 'END { print NR }'
|
||||
}
|
||||
|
||||
files=()
|
||||
while IFS= read -r -d '' file; do
|
||||
files+=("$file")
|
||||
done < <(find "$BASE" -type f -name '*.json' -print0)
|
||||
|
||||
batch_size=50
|
||||
|
||||
for ((i=0; i<${#files[@]}; i+=batch_size)); do
|
||||
batch_files=("${files[@]:i:batch_size}")
|
||||
|
||||
declare -a json_titles=()
|
||||
declare -a module_titles=()
|
||||
|
||||
for file in "${batch_files[@]}"; do
|
||||
rel="${file#$BASE/}"
|
||||
rel="$(printf "%s" "$rel" | tr -d '\r\n' | sed 's/:/_/g')"
|
||||
json_titles+=("$ROOT/$rel")
|
||||
module_titles+=("$MODULE_ROOT/$rel/data")
|
||||
done
|
||||
|
||||
declare -A json_lines=()
|
||||
declare -A module_lines=()
|
||||
|
||||
query_page_linecounts json_titles json_lines
|
||||
query_page_linecounts module_titles module_lines
|
||||
|
||||
for file in "${batch_files[@]}"; do
|
||||
rel="${file#$BASE/}"
|
||||
rel="$(printf "%s" "$rel" | tr -d '\r\n' | sed 's/:/_/g')"
|
||||
|
||||
json_title="$ROOT/$rel"
|
||||
module_title="$MODULE_ROOT/$rel/data"
|
||||
|
||||
echo "Processing $rel → $json_title"
|
||||
|
||||
local_lines=$(awk 'END { print NR }' "$file")
|
||||
remote_lines="${json_lines[$json_title]:-}"
|
||||
|
||||
if [[ -n "$remote_lines" && "$remote_lines" == "$local_lines" ]]; then
|
||||
echo "Skipping unchanged page: $json_title"
|
||||
else
|
||||
echo "Uploading $rel → $json_title"
|
||||
edit_page_file "$json_title" "Update $rel via GitHub Actions" "$file" "$token"
|
||||
fi
|
||||
|
||||
lua_content=$'local loader = require("Module:JsonLoader")\nreturn loader.getFromTitle("'"$json_title"'")'
|
||||
|
||||
local_lines=$(printf '%s' "$lua_content" | awk 'END { print NR }')
|
||||
remote_lines="${module_lines[$module_title]:-}"
|
||||
|
||||
if [[ -n "$remote_lines" && "$remote_lines" == "$local_lines" ]]; then
|
||||
echo "Skipping unchanged module: $module_title"
|
||||
else
|
||||
echo "Creating/updating module → $module_title"
|
||||
edit_page "$module_title" "Auto-create loader module for $rel" "$lua_content" "$token"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Создает страницы по каждому id подходящей сущности.
|
||||
# Имя страницы формируется из имени сущности в entity_name.json.
|
||||
- name: Create entity pages
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || steps.changes.outputs.prototypes == 'true' }}
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
FILE="./bin/Content.Server/data/entity_name.json"
|
||||
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
|
||||
|
||||
login_token=$(curl -sS -c "$cookiejar" \
|
||||
--data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken')
|
||||
|
||||
curl -sS -c "$cookiejar" -b "$cookiejar" \
|
||||
--data-urlencode "action=login" \
|
||||
--data-urlencode "lgname=$USER" \
|
||||
--data-urlencode "lgpassword=$PASS" \
|
||||
--data-urlencode "lgtoken=$login_token" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" > /dev/null
|
||||
|
||||
token=$(curl -sS -b "$cookiejar" \
|
||||
--data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken')
|
||||
|
||||
mapfile -t pages < <(
|
||||
jq -r '
|
||||
to_entries[]
|
||||
| "\(.key)|\(.value | if type == "array" then .[0] else . end)"
|
||||
' "$FILE" | grep -v '|$'
|
||||
)
|
||||
|
||||
batch_size=50
|
||||
|
||||
for ((i=0; i<${#pages[@]}; i+=batch_size)); do
|
||||
batch=("${pages[@]:i:batch_size}")
|
||||
|
||||
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" \
|
||||
--data-urlencode "action=query" \
|
||||
--data-urlencode "format=json" \
|
||||
--data-urlencode "titles=$titles" \
|
||||
"$API" | jq -r '
|
||||
.query.pages[]
|
||||
| select(has("missing") | not)
|
||||
| .title
|
||||
'
|
||||
)
|
||||
|
||||
while IFS='|' read -r name id; do
|
||||
[[ -z "${name:-}" ]] && continue
|
||||
[[ -z "${id:-}" ]] && continue
|
||||
|
||||
if grep -Fxq "$NAMESPACE$name" <<< "$existing_titles"; then
|
||||
echo "Skipping existing page: $NAMESPACE$name"
|
||||
continue
|
||||
fi
|
||||
|
||||
content="{{сущность|$id}}"
|
||||
echo "Creating page: $NAMESPACE$name → $id"
|
||||
|
||||
curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=edit" \
|
||||
--data-urlencode "createonly=true" \
|
||||
--data-urlencode "title=$NAMESPACE$name" \
|
||||
--data-urlencode "text=$content" \
|
||||
--data-urlencode "summary=Create $id via GitHub Actions" \
|
||||
--data-urlencode "token=$token" \
|
||||
--data-urlencode "bot=true" \
|
||||
--data-urlencode "minor=true" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" | jq -r '.edit.result // "null"' || true
|
||||
done < <(printf '%s\n' "${batch[@]}")
|
||||
done
|
||||
|
||||
# Добавляет предупреждение дубликатам страниц сущностей.
|
||||
- name: Duplicates entity pages
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || steps.changes.outputs.prototypes == 'true' }}
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
FILE="./bin/Content.Server/data/entity_name_wiki.json"
|
||||
API="${{ secrets.WIKI_ROOT_URL }}/api.php"
|
||||
USER="${{ secrets.WIKI_BOT_USER }}"
|
||||
PASS="${{ secrets.WIKI_BOT_PASS }}"
|
||||
|
||||
API="$(printf "%s" "$API" | tr -d '\r\n')"
|
||||
USER="$(printf "%s" "$USER" | tr -d '\r\n')"
|
||||
PASS="$(printf "%s" "$PASS" | tr -d '\r\n')"
|
||||
|
||||
cookiejar="$(mktemp)"
|
||||
trap 'rm -f "$cookiejar"' EXIT
|
||||
warning="{{сущность/infobox|тип=дубликат}}"
|
||||
|
||||
login_token=$(curl -sS -c "$cookiejar" \
|
||||
--data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken')
|
||||
|
||||
curl -sS -c "$cookiejar" -b "$cookiejar" \
|
||||
--data-urlencode "action=login" \
|
||||
--data-urlencode "lgname=$USER" \
|
||||
--data-urlencode "lgpassword=$PASS" \
|
||||
--data-urlencode "lgtoken=$login_token" \
|
||||
--data-urlencode "format=json" \
|
||||
"$API" > /dev/null
|
||||
|
||||
token=$(curl -sS -b "$cookiejar" \
|
||||
--data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken')
|
||||
|
||||
jq -r '.[]' "$FILE" |
|
||||
xargs -r -d '\n' -P 1 -I{} bash -c '
|
||||
set -uo pipefail
|
||||
title="$1"
|
||||
api="$2"
|
||||
token="$3"
|
||||
cookiejar="$4"
|
||||
warning="$5"
|
||||
|
||||
if [[ -z "$title" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
page_json=$(curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=query" \
|
||||
--data-urlencode "prop=revisions" \
|
||||
--data-urlencode "rvprop=content" \
|
||||
--data-urlencode "rvslots=main" \
|
||||
--data-urlencode "format=json" \
|
||||
--data-urlencode "titles=$title" \
|
||||
"$api")
|
||||
|
||||
content=$(printf "%s" "$page_json" | jq -r \
|
||||
".query.pages | to_entries[0].value.revisions[0].slots.main[\"*\"] // \"\"")
|
||||
|
||||
if printf "%s" "$content" | grep -Fq "$warning"; then
|
||||
echo "Skipping (already has warning): $title"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Prepending Infobox to: $title"
|
||||
|
||||
curl -sS -b "$cookiejar" \
|
||||
--data-urlencode "action=edit" \
|
||||
--data-urlencode "title=$title" \
|
||||
--data-urlencode "prependtext=${warning}" \
|
||||
--data-urlencode "summary=Add Warning via GitHub Actions" \
|
||||
--data-urlencode "token=$token" \
|
||||
--data-urlencode "bot=true" \
|
||||
--data-urlencode "minor=true" \
|
||||
--data-urlencode "format=json" \
|
||||
"$api" | jq -r ".edit.result // \"null\"" || true
|
||||
' _ "{}" "$API" "$token" "$cookiejar" "$warning"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
|
||||
<Nullable>disable</Nullable>
|
||||
<DefineConstants>$(DefineConstants);ALLOW_BAD_PRACTICES</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<Import Project="../MSBuild/Content.props" />
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SecurityIconPrototype> CrewBorderIcon = "CrewBorderIcon";
|
||||
private static readonly ProtoId<SecurityIconPrototype> CrewUncertainBorderIcon = "CrewUncertainBorderIcon";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<JobStatusComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
|
||||
}
|
||||
|
||||
// show the status icons if the player has the correponding HUDs
|
||||
private void OnGetStatusIconsEvent(Entity<JobStatusComponent> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SharedIdCardConsoleSystem>();
|
||||
|
||||
_maxNameLength =_cfgManager.GetCVar(CCVars.MaxNameLength);
|
||||
_maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength);
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
@@ -77,12 +67,6 @@ namespace Content.Client.Access.UI
|
||||
|
||||
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, ProtoId<JobPrototype> newJobPrototype)
|
||||
{
|
||||
if (newFullName.Length > _maxNameLength)
|
||||
newFullName = newFullName[.._maxNameLength];
|
||||
|
||||
if (newJobTitle.Length > _maxIdJobLength)
|
||||
newJobTitle = newJobTitle[.._maxIdJobLength];
|
||||
|
||||
SendMessage(new WriteToTargetIdMessage(
|
||||
newFullName,
|
||||
newJobTitle,
|
||||
|
||||
@@ -23,10 +23,6 @@ namespace Content.Client.Access.UI
|
||||
|
||||
private readonly IdCardConsoleBoundUserInterface _owner;
|
||||
|
||||
// CCVar.
|
||||
private int _maxNameLength;
|
||||
private int _maxIdJobLength;
|
||||
|
||||
private AccessLevelControl _accessButtons = new();
|
||||
private readonly List<string> _jobPrototypeIds = new();
|
||||
|
||||
@@ -46,11 +42,8 @@ namespace Content.Client.Access.UI
|
||||
|
||||
_owner = owner;
|
||||
|
||||
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
|
||||
_maxIdJobLength = _cfgManager.GetCVar(CCVars.MaxIdJobLength);
|
||||
|
||||
FullNameLineEdit.OnTextEntered += _ => SubmitData();
|
||||
FullNameLineEdit.IsValid = s => s.Length <= _maxNameLength;
|
||||
FullNameLineEdit.IsValid = s => s.Length <= _cfgManager.GetCVar(CCVars.MaxNameLength);
|
||||
FullNameLineEdit.OnTextChanged += _ =>
|
||||
{
|
||||
FullNameSaveButton.Disabled = FullNameSaveButton.Text == _lastFullName;
|
||||
@@ -58,7 +51,7 @@ namespace Content.Client.Access.UI
|
||||
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;
|
||||
|
||||
@@ -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 @@ public sealed class BanListEui : BaseEui
|
||||
return date.ToString("MM/dd/yyyy h:mm tt");
|
||||
}
|
||||
|
||||
public static void SetData<T>(IBanListLine<T> line, SharedServerBan ban) where T : SharedServerBan
|
||||
public static void SetData<T>(IBanListLine<T> line, SharedBan ban) where T : SharedBan
|
||||
{
|
||||
line.Reason.Text = ban.Reason;
|
||||
line.BanTime.Text = FormatDate(ban.BanTime);
|
||||
@@ -94,20 +95,20 @@ public sealed class BanListEui : BaseEui
|
||||
line.BanningAdmin.Text = ban.BanningAdminName;
|
||||
}
|
||||
|
||||
private void OnLineIdsClicked<T>(IBanListLine<T> line) where T : SharedServerBan
|
||||
private void OnLineIdsClicked<T>(IBanListLine<T> 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);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed partial class BanListControl : Control
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetBans(List<SharedServerBan> bans)
|
||||
public void SetBans(List<SharedBan> bans)
|
||||
{
|
||||
for (var i = Bans.ChildCount - 1; i >= 1; i--)
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
namespace Content.Client.Administration.UI.BanList.Bans;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedServerBan>
|
||||
public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedBan>
|
||||
{
|
||||
public SharedServerBan Ban { get; }
|
||||
public SharedBan Ban { get; }
|
||||
|
||||
public event Action<BanListLine>? IdsClicked;
|
||||
|
||||
public BanListLine(SharedServerBan ban)
|
||||
public BanListLine(SharedBan ban)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Administration.UI.BanList;
|
||||
|
||||
public interface IBanListLine<T> where T : SharedServerBan
|
||||
public interface IBanListLine<T> where T : SharedBan
|
||||
{
|
||||
T Ban { get; }
|
||||
Label Reason { get; }
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed partial class RoleBanListControl : Control
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetRoleBans(List<SharedServerRoleBan> bans)
|
||||
public void SetRoleBans(List<SharedBan> bans)
|
||||
{
|
||||
for (var i = RoleBans.ChildCount - 1; i >= 1; i--)
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
namespace Content.Client.Administration.UI.BanList.RoleBans;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedServerRoleBan>
|
||||
public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedBan>
|
||||
{
|
||||
public SharedServerRoleBan Ban { get; }
|
||||
public SharedBan Ban { get; }
|
||||
|
||||
public event Action<RoleBanListLine>? IdsClicked;
|
||||
|
||||
public RoleBanListLine(SharedServerRoleBan ban)
|
||||
public RoleBanListLine(SharedBan ban)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedS
|
||||
IdsHidden.OnPressed += IdsPressed;
|
||||
|
||||
BanListEui.SetData(this, ban);
|
||||
Role.Text = ban.Role;
|
||||
Role.Text = string.Join(", ", ban.Roles ?? []);
|
||||
}
|
||||
|
||||
private void IdsPressed(ButtonEventArgs buttonEventArgs)
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed partial class AdminNotesLine : BoxContainer
|
||||
|
||||
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 @@ 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 rolesText = string.Join(
|
||||
", ",
|
||||
// Explicit cast here to avoid sandbox violation.
|
||||
(IEnumerable<BanRoleDef>?)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);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ public sealed partial class AdminNotesLinePopup : Popup
|
||||
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));
|
||||
|
||||
@@ -25,8 +25,8 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
|
||||
var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap);
|
||||
|
||||
if (TryComp<HumanoidAppearanceComponent>(ent, out var humanoidAppearance) &&
|
||||
ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite))
|
||||
if (TryComp<HumanoidProfileComponent>(ent, out var humanoid) &&
|
||||
ent.Comp.SpeciesSprites.TryGetValue(humanoid.Species, out var speciesSprite))
|
||||
{
|
||||
_sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'anomaly-generator-ui-title'}"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</BoxContainer>
|
||||
<!--Sprite View-->
|
||||
<PanelContainer Margin="12 0 0 0"
|
||||
StyleClasses="Inset"
|
||||
StyleClasses="BackgroundPanelDark"
|
||||
VerticalAlignment="Center">
|
||||
<SpriteView Name="EntityView"
|
||||
SetSize="96 96"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
|
||||
namespace Content.Client.Atmos.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class MapAtmosphereComponent : SharedMapAtmosphereComponent
|
||||
{
|
||||
|
||||
}
|
||||
@@ -10,7 +10,9 @@ namespace Content.Client.Atmos.EntitySystems
|
||||
[UsedImplicitly]
|
||||
internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem
|
||||
{
|
||||
public readonly Dictionary<EntityUid, AtmosDebugOverlayMessage> TileData = new();
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
|
||||
public readonly Dictionary<EntityUid, AtmosDebugOverlayMessage> TileData = [];
|
||||
|
||||
// Configuration set by debug commands and used by AtmosDebugOverlay {
|
||||
/// <summary>Value source for display</summary>
|
||||
@@ -25,6 +27,8 @@ namespace Content.Client.Atmos.EntitySystems
|
||||
public bool CfgCBM = false;
|
||||
// }
|
||||
|
||||
private AtmosDebugOverlay? _overlay;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -34,10 +38,6 @@ namespace Content.Client.Atmos.EntitySystems
|
||||
SubscribeNetworkEvent<AtmosDebugOverlayDisableMessage>(HandleAtmosDebugOverlayDisableMessage);
|
||||
|
||||
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
|
||||
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
if(!overlayManager.HasOverlay<AtmosDebugOverlay>())
|
||||
overlayManager.AddOverlay(new AtmosDebugOverlay(this));
|
||||
}
|
||||
|
||||
private void OnGridRemoved(GridRemovalEvent ev)
|
||||
@@ -51,19 +51,25 @@ namespace Content.Client.Atmos.EntitySystems
|
||||
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<IOverlayManager>();
|
||||
if (overlayManager.HasOverlay<AtmosDebugOverlay>())
|
||||
overlayManager.RemoveOverlay<AtmosDebugOverlay>();
|
||||
|
||||
RemoveOverlay();
|
||||
}
|
||||
|
||||
public void Reset(RoundRestartCleanupEvent ev)
|
||||
@@ -75,6 +81,15 @@ namespace Content.Client.Atmos.EntitySystems
|
||||
{
|
||||
return TileData.ContainsKey(gridId);
|
||||
}
|
||||
|
||||
private void RemoveOverlay()
|
||||
{
|
||||
if (_overlay is null)
|
||||
return;
|
||||
|
||||
_overlayManager.RemoveOverlay(_overlay);
|
||||
_overlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum AtmosDebugOverlayMode : byte
|
||||
|
||||
@@ -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 @@ public sealed partial class AtmosphereSystem
|
||||
implementation.
|
||||
*/
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>No-op on client as reactions aren't entirely in shared.
|
||||
/// Don't call it. Smile.</remarks>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Client.Atmos.Overlays;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// System responsible for rendering atmos fire animations using <see cref="GasTileFireOverlay"/>.
|
||||
/// </summary>
|
||||
[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<GasTileFireOverlay>();
|
||||
}
|
||||
}
|
||||
@@ -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<GasOverlayUpdateEvent>(HandleGasOverlayUpdate);
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private GasTileOverlay _overlay = default!;
|
||||
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
|
||||
{
|
||||
Dictionary<Vector2i, GasOverlayChunk> modifiedChunks;
|
||||
|
||||
public override void Initialize()
|
||||
switch (args.Current)
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<GasOverlayUpdateEvent>(HandleGasOverlayUpdate);
|
||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentHandleState>(OnHandleState);
|
||||
|
||||
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlayMan.RemoveOverlay<GasTileOverlay>();
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
|
||||
{
|
||||
Dictionary<Vector2i, GasOverlayChunk> modifiedChunks;
|
||||
|
||||
switch (args.Current)
|
||||
// is this a delta or full state?
|
||||
case GasTileOverlayDeltaState delta:
|
||||
{
|
||||
// is this a delta or full state?
|
||||
case GasTileOverlayDeltaState delta:
|
||||
modifiedChunks = delta.ModifiedChunks;
|
||||
foreach (var index in comp.Chunks.Keys)
|
||||
{
|
||||
modifiedChunks = delta.ModifiedChunks;
|
||||
foreach (var index in comp.Chunks.Keys)
|
||||
{
|
||||
if (!delta.AllChunks.Contains(index))
|
||||
comp.Chunks.Remove(index);
|
||||
}
|
||||
|
||||
break;
|
||||
if (!delta.AllChunks.Contains(index))
|
||||
comp.Chunks.Remove(index);
|
||||
}
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var (index, data) in modifiedChunks)
|
||||
case GasTileOverlayState state:
|
||||
{
|
||||
comp.Chunks[index] = data;
|
||||
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)
|
||||
{
|
||||
comp.Chunks[index] = data;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGasOverlayUpdate(GasOverlayUpdateEvent ev)
|
||||
{
|
||||
foreach (var (nent, removedIndicies) in ev.RemovedChunks)
|
||||
{
|
||||
var grid = GetEntity(nent);
|
||||
|
||||
if (!TryComp(grid, out GasTileOverlayComponent? comp))
|
||||
continue;
|
||||
|
||||
foreach (var index in removedIndicies)
|
||||
{
|
||||
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);
|
||||
|
||||
if (!TryComp(grid, out GasTileOverlayComponent? comp))
|
||||
continue;
|
||||
|
||||
foreach (var chunkData in gridData)
|
||||
{
|
||||
var grid = GetEntity(nent);
|
||||
|
||||
if (!TryComp(grid, out GasTileOverlayComponent? comp))
|
||||
continue;
|
||||
|
||||
foreach (var index in removedIndicies)
|
||||
{
|
||||
comp.Chunks.Remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (nent, gridData) in ev.UpdatedChunks)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Client.Atmos.Overlays;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// System responsible for rendering visible atmos gasses (like plasma for example) using <see cref="GasTileVisibleGasOverlay"/>.
|
||||
/// </summary>
|
||||
[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<GasTileVisibleGasOverlay>();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">
|
||||
<!-- Left column (view of entity) -->
|
||||
<PanelContainer Margin="2 0 6 0" StyleClasses="Inset" VerticalAlignment="Center" VerticalExpand="True">
|
||||
<PanelContainer Margin="2 0 6 0" StyleClasses="BackgroundPanelDark" VerticalAlignment="Center" VerticalExpand="True">
|
||||
<SpriteView Name="EntityView" OverrideDirection="South" Scale="2 2" SetSize="64 64"/>
|
||||
</PanelContainer>
|
||||
<!-- Center column (pressure, temperature, alarm state) -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a thermal heatmap overlay for gas tiles, used for equipment like thermal glasses.
|
||||
/// /// </summary>
|
||||
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<GasTileOverlayComponent> _overlayQuery;
|
||||
|
||||
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
||||
private List<Entity<MapGridComponent>> _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<SharedTransformSystem>();
|
||||
|
||||
_overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used for Calculating onscreen color from ThermalByte core value
|
||||
/// /// </summary>
|
||||
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<GasTileOverlaySystem>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Overlay responsible for rendering atmos fire animation.
|
||||
/// </summary>
|
||||
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<ShaderPrototype> 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<SharedTransformSystem>();
|
||||
_mapSystem = _entManager.System<SharedMapSystem>();
|
||||
_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<RSIResource>(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<TransformComponent>();
|
||||
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
|
||||
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<GasTileOverlayComponent> overlayQuery,
|
||||
EntityQuery<TransformComponent> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<ShaderPrototype> 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<IMapManager>();
|
||||
_atmosphereSystem = entManager.System<SharedAtmosphereSystem>();
|
||||
_mapSystem = entManager.System<SharedMapSystem>();
|
||||
_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<RSIResource>(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<float>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var fire = resourceCache.GetResource<RSIResource>(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<TransformComponent>();
|
||||
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
|
||||
var gridState = (args.WorldBounds,
|
||||
args.WorldHandle,
|
||||
_gasCount,
|
||||
_frames,
|
||||
_frameCounter,
|
||||
_fireFrames,
|
||||
_fireFrameCounter,
|
||||
_shader,
|
||||
overlayQuery,
|
||||
xformQuery,
|
||||
_xformSys);
|
||||
|
||||
var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
|
||||
|
||||
if (_entManager.TryGetComponent<MapAtmosphereComponent>(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<GasTileOverlayComponent> overlayQuery,
|
||||
EntityQuery<TransformComponent> 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<MapGridComponent>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Overlay responsible for rendering visible atmos gasses (like plasma for example) usin.
|
||||
/// </summary>
|
||||
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<ShaderPrototype> 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<SharedAtmosphereSystem>();
|
||||
_mapSystem = _entManager.System<SharedMapSystem>();
|
||||
_xformSys = _entManager.System<SharedTransformSystem>();
|
||||
_gasTileOverlaySystem = _entManager.System<SharedGasTileOverlaySystem>();
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
|
||||
_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<RSIResource>(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<float>();
|
||||
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<TransformComponent>();
|
||||
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
|
||||
var gridState = (args.WorldBounds,
|
||||
args.WorldHandle,
|
||||
_gasCount,
|
||||
_frames,
|
||||
_frameCounter,
|
||||
_shader,
|
||||
overlayQuery,
|
||||
xformQuery,
|
||||
_xformSys);
|
||||
|
||||
var mapUid = _mapSystem.GetMapOrInvalid(args.MapId);
|
||||
|
||||
if (_entManager.TryGetComponent<MapAtmosphereComponent>(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<GasTileOverlayComponent> overlayQuery,
|
||||
EntityQuery<TransformComponent> 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<MapGridComponent>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,10 +37,9 @@ namespace Content.Client.Atmos.UI
|
||||
_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)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="480 400" Title="Filter">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-filter-ui-filter-status}"/>
|
||||
<Button Name="ToggleStatusButton"/>
|
||||
</BoxContainer>
|
||||
<controls:SwitchButton
|
||||
Name="ToggleStatusButton"
|
||||
HorizontalAlignment="Left"
|
||||
Pressed="True" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-filter-ui-filter-transfer-rate}"/>
|
||||
|
||||
@@ -18,11 +18,10 @@ namespace Content.Client.Atmos.UI
|
||||
{
|
||||
private readonly ButtonGroup _buttonGroup = new();
|
||||
|
||||
public bool FilterStatus = true;
|
||||
public string? SelectedGas;
|
||||
public string? CurrentGasId;
|
||||
|
||||
public event Action? ToggleStatusButtonPressed;
|
||||
public event Action<bool>? ToggleStatusButtonPressed;
|
||||
public event Action<string>? FilterTransferRateChanged;
|
||||
public event Action? SelectGasPressed;
|
||||
|
||||
@@ -30,8 +29,7 @@ namespace Content.Client.Atmos.UI
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ToggleStatusButton.OnPressed += _ => SetFilterStatus(!FilterStatus);
|
||||
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
|
||||
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
|
||||
|
||||
FilterTransferRateInput.OnTextChanged += _ => SetFilterRate.Disabled = false;
|
||||
SetFilterRate.OnPressed += _ =>
|
||||
@@ -53,15 +51,7 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
public void SetFilterStatus(bool enabled)
|
||||
{
|
||||
FilterStatus = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-filter-ui-status-enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-filter-ui-status-disabled");
|
||||
}
|
||||
ToggleStatusButton.Pressed = enabled;
|
||||
}
|
||||
|
||||
public void SetGasFiltered(string? id, string name)
|
||||
|
||||
@@ -33,10 +33,9 @@ namespace Content.Client.Atmos.UI
|
||||
_window.MixerNodePercentageChanged += OnMixerSetPercentagePressed;
|
||||
}
|
||||
|
||||
private void OnToggleStatusButtonPressed()
|
||||
private void OnToggleStatusButtonPressed(bool status)
|
||||
{
|
||||
if (_window is null) return;
|
||||
SendMessage(new GasMixerToggleStatusMessage(_window.MixerStatus));
|
||||
SendMessage(new GasMixerToggleStatusMessage(status));
|
||||
}
|
||||
|
||||
private void OnMixerOutputPressurePressed(string value)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="200 200" Title="Gas Mixer">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-mixer-ui-mixer-status}"/>
|
||||
<Control MinSize="5 0" />
|
||||
<Button Name="ToggleStatusButton"/>
|
||||
</BoxContainer>
|
||||
<controls:SwitchButton
|
||||
Name="ToggleStatusButton"
|
||||
HorizontalAlignment="Left"
|
||||
Pressed="True" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-mixer-ui-mixer-output-pressure}"/>
|
||||
<Control MinSize="5 0" />
|
||||
|
||||
@@ -21,9 +21,7 @@ namespace Content.Client.Atmos.UI
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GasMixerWindow : DefaultWindow
|
||||
{
|
||||
public bool MixerStatus = true;
|
||||
|
||||
public event Action? ToggleStatusButtonPressed;
|
||||
public event Action<bool>? ToggleStatusButtonPressed;
|
||||
public event Action<string>? MixerOutputPressureChanged;
|
||||
public event Action<string>? MixerNodePercentageChanged;
|
||||
|
||||
@@ -33,8 +31,7 @@ namespace Content.Client.Atmos.UI
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ToggleStatusButton.OnPressed += _ => SetMixerStatus(!MixerStatus);
|
||||
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
|
||||
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
|
||||
|
||||
MixerPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
|
||||
SetOutputPressureButton.OnPressed += _ =>
|
||||
@@ -83,15 +80,7 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
public void SetMixerStatus(bool enabled)
|
||||
{
|
||||
MixerStatus = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-mixer-ui-status-enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-mixer-ui-status-disabled");
|
||||
}
|
||||
ToggleStatusButton.Pressed = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,12 +42,9 @@ public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKe
|
||||
_window.SetOutputPressure(pump.TargetPressure);
|
||||
}
|
||||
|
||||
private void OnToggleStatusButtonPressed()
|
||||
private void OnToggleStatusButtonPressed(bool status)
|
||||
{
|
||||
if (_window is null)
|
||||
return;
|
||||
|
||||
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
|
||||
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(status));
|
||||
}
|
||||
|
||||
private void OnPumpOutputPressurePressed(float value)
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
SetSize="340 110" MinSize="340 110" Title="Pressure Pump">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-pump-ui-pump-status}" Margin="0 0 5 0"/>
|
||||
<Button Name="ToggleStatusButton"/>
|
||||
<controls:SwitchButton
|
||||
Name="ToggleStatusButton"
|
||||
HorizontalAlignment="Left"
|
||||
Pressed="True" />
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Button HorizontalAlignment="Right" Name="SetOutputPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" Disabled="True" Margin="0 0 5 0"/>
|
||||
<Button Name="SetMaxPressureButton" Text="{Loc comp-gas-pump-ui-pump-set-max}" />
|
||||
|
||||
@@ -11,9 +11,7 @@ namespace Content.Client.Atmos.UI
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GasPressurePumpWindow : FancyWindow
|
||||
{
|
||||
public bool PumpStatus = true;
|
||||
|
||||
public event Action? ToggleStatusButtonPressed;
|
||||
public event Action<bool>? ToggleStatusButtonPressed;
|
||||
public event Action<float>? PumpOutputPressureChanged;
|
||||
|
||||
public float MaxPressure
|
||||
@@ -33,8 +31,7 @@ namespace Content.Client.Atmos.UI
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
|
||||
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
|
||||
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
|
||||
|
||||
PumpPressureOutputInput.OnValueChanged += _ => SetOutputPressureButton.Disabled = false;
|
||||
|
||||
@@ -59,15 +56,7 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
public void SetPumpStatus(bool enabled)
|
||||
{
|
||||
PumpStatus = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-disabled");
|
||||
}
|
||||
ToggleStatusButton.Pressed = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
_window = this.CreateWindow<GasThermomachineWindow>();
|
||||
|
||||
_window.ToggleStatusButton.OnPressed += _ => OnToggleStatusButtonPressed();
|
||||
_window.ToggleStatusButton.OnToggled += _ => OnToggleStatusButtonPressed();
|
||||
_window.TemperatureSpinbox.OnValueChanged += _ => OnTemperatureChanged(_window.TemperatureSpinbox.Value);
|
||||
_window.Entity = Owner;
|
||||
Update();
|
||||
@@ -45,9 +45,6 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
private void OnToggleStatusButtonPressed()
|
||||
{
|
||||
if (_window is null) return;
|
||||
|
||||
_window.SetActive(!_window.Active);
|
||||
SendPredictedMessage(new GasThermomachineToggleMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="300 120" Title="{Loc comp-gas-thermomachine-ui-title-freezer}">
|
||||
<BoxContainer Name="VboxContainer" Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-thermomachine-ui-toggle}"/>
|
||||
<Control MinSize="5 0" />
|
||||
<Button Access="Public" Name="ToggleStatusButton"/>
|
||||
</BoxContainer>
|
||||
<controls:SwitchButton
|
||||
Name="ToggleStatusButton"
|
||||
HorizontalAlignment="Left"
|
||||
Pressed="True"
|
||||
Access="Public" />
|
||||
<BoxContainer Name="SpinboxHBox" Orientation="Horizontal">
|
||||
<Label Text="{Loc comp-gas-thermomachine-ui-temperature}"/>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -12,8 +12,6 @@ public sealed partial class GasThermomachineWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public bool Active = true;
|
||||
|
||||
public FloatSpinBox TemperatureSpinbox;
|
||||
|
||||
public EntityUid Entity;
|
||||
@@ -30,15 +28,7 @@ public sealed partial class GasThermomachineWindow : FancyWindow
|
||||
|
||||
public void SetActive(bool active)
|
||||
{
|
||||
Active = active;
|
||||
if (active)
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-thermomachine-ui-status-enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-thermomachine-ui-status-disabled");
|
||||
}
|
||||
ToggleStatusButton.Pressed = active;
|
||||
}
|
||||
|
||||
public void SetTemperature(float temperature)
|
||||
|
||||
@@ -38,11 +38,9 @@ namespace Content.Client.Atmos.UI
|
||||
Update();
|
||||
}
|
||||
|
||||
private void OnToggleStatusButtonPressed()
|
||||
private void OnToggleStatusButtonPressed(bool status)
|
||||
{
|
||||
if (_window is null) return;
|
||||
|
||||
SendPredictedMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
|
||||
SendPredictedMessage(new GasVolumePumpToggleStatusMessage(status));
|
||||
}
|
||||
|
||||
private void OnPumpTransferRatePressed(string value)
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="200 120" Title="Volume Pump">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-pump-ui-pump-status}"/>
|
||||
<Control MinSize="5 0" />
|
||||
<Button Name="ToggleStatusButton"/>
|
||||
</BoxContainer>
|
||||
<controls:SwitchButton
|
||||
Name="ToggleStatusButton"
|
||||
HorizontalAlignment="Left"
|
||||
Pressed="True" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Text="{Loc comp-gas-pump-ui-pump-transfer-rate}"/>
|
||||
|
||||
@@ -19,17 +19,14 @@ namespace Content.Client.Atmos.UI
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GasVolumePumpWindow : FancyWindow
|
||||
{
|
||||
public bool PumpStatus = true;
|
||||
|
||||
public event Action? ToggleStatusButtonPressed;
|
||||
public event Action<bool>? ToggleStatusButtonPressed;
|
||||
public event Action<string>? PumpTransferRateChanged;
|
||||
|
||||
public GasVolumePumpWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ToggleStatusButton.OnPressed += _ => SetPumpStatus(!PumpStatus);
|
||||
ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
|
||||
ToggleStatusButton.OnToggled += _ => ToggleStatusButtonPressed?.Invoke(ToggleStatusButton.Pressed);
|
||||
|
||||
PumpTransferRateInput.OnTextChanged += _ => SetTransferRateButton.Disabled = false;
|
||||
SetTransferRateButton.OnPressed += _ =>
|
||||
@@ -52,15 +49,7 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
public void SetPumpStatus(bool enabled)
|
||||
{
|
||||
PumpStatus = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-gas-pump-ui-status-disabled");
|
||||
}
|
||||
ToggleStatusButton.Pressed = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class SpaceHeaterBoundUserInterface : BoundUserInterface
|
||||
|
||||
_window = this.CreateWindow<SpaceHeaterWindow>();
|
||||
|
||||
_window.ToggleStatusButton.OnPressed += _ => OnToggleStatusButtonPressed();
|
||||
_window.ToggleStatusButton.OnToggled += _ => OnToggleStatusButtonPressed();
|
||||
_window.IncreaseTempRange.OnPressed += _ => OnTemperatureRangeChanged(_window.TemperatureChangeDelta);
|
||||
_window.DecreaseTempRange.OnPressed += _ => OnTemperatureRangeChanged(-_window.TemperatureChangeDelta);
|
||||
_window.ModeSelector.OnItemSelected += OnModeChanged;
|
||||
@@ -34,7 +34,6 @@ public sealed class SpaceHeaterBoundUserInterface : BoundUserInterface
|
||||
|
||||
private void OnToggleStatusButtonPressed()
|
||||
{
|
||||
_window?.SetActive(!_window.Active);
|
||||
SendMessage(new SpaceHeaterToggleMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="280 160" Title="{Loc comp-space-heater-ui-title}">
|
||||
|
||||
<BoxContainer Name="VboxContainer" Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Button Text="{Loc comp-space-heater-ui-status-disabled}" Access="Public" Name="ToggleStatusButton"/>
|
||||
</BoxContainer>
|
||||
<controls:SwitchButton
|
||||
Name="ToggleStatusButton"
|
||||
HorizontalExpand="True"
|
||||
Access="Public" />
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="5">
|
||||
<Label Text="{Loc comp-space-heater-ui-mode}"/>
|
||||
<OptionButton Access="Public" Name="ModeSelector"/>
|
||||
|
||||
@@ -15,7 +15,6 @@ public sealed partial class SpaceHeaterWindow : DefaultWindow
|
||||
{
|
||||
// To account for a minimum delta temperature for atmos equalization to trigger we use a fixed step for target temperature increment/decrement
|
||||
public int TemperatureChangeDelta = 5;
|
||||
public bool Active;
|
||||
|
||||
// Temperatures range bounds in Kelvin (K)
|
||||
public float MinTemp;
|
||||
@@ -49,17 +48,7 @@ public sealed partial class SpaceHeaterWindow : DefaultWindow
|
||||
|
||||
public void SetActive(bool active)
|
||||
{
|
||||
Active = active;
|
||||
ToggleStatusButton.Pressed = active;
|
||||
|
||||
if (active)
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-space-heater-ui-status-enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
ToggleStatusButton.Text = Loc.GetString("comp-space-heater-ui-status-disabled");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTemperature(float targetTemperature)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.BarSign;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.BarSign.Ui;
|
||||
@@ -16,13 +17,12 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
|
||||
{
|
||||
base.Open();
|
||||
|
||||
var sign = EntMan.GetComponentOrNull<BarSignComponent>(Owner)?.Current is { } current
|
||||
? _prototype.Index(current)
|
||||
: null;
|
||||
var allSigns = BarSignSystem.GetAllBarSigns(_prototype)
|
||||
.OrderBy(p => Loc.GetString(p.Name))
|
||||
.ToList();
|
||||
_menu = new(sign, allSigns);
|
||||
|
||||
_menu = this.CreateWindow<BarSignMenu>();
|
||||
_menu.LoadSigns(allSigns);
|
||||
|
||||
_menu.OnSignSelected += id =>
|
||||
{
|
||||
@@ -30,16 +30,17 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
|
||||
};
|
||||
|
||||
_menu.OnClose += Close;
|
||||
_menu.OpenCentered();
|
||||
_menu.OpenToLeft();
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
if (!EntMan.TryGetComponent<BarSignComponent>(Owner, out var signComp))
|
||||
if (!EntMan.TryGetComponent<BarSignComponent>(Owner, out var signComp)
|
||||
|| !_prototype.Resolve(signComp.Current, out var signPrototype))
|
||||
return;
|
||||
|
||||
if (_prototype.Resolve(signComp.Current, out var signPrototype))
|
||||
_menu?.UpdateState(signPrototype);
|
||||
_menu?.UpdateState(signPrototype);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -8,23 +8,13 @@ namespace Content.Client.BarSign.Ui;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BarSignMenu : FancyWindow
|
||||
{
|
||||
private string? _currentId;
|
||||
|
||||
private readonly List<BarSignPrototype> _cachedPrototypes = new();
|
||||
private List<BarSignPrototype> _cachedPrototypes = new();
|
||||
|
||||
public event Action<string>? OnSignSelected;
|
||||
|
||||
public BarSignMenu(BarSignPrototype? currentSign, List<BarSignPrototype> signs)
|
||||
public BarSignMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_currentId = currentSign?.ID;
|
||||
|
||||
_cachedPrototypes.Clear();
|
||||
_cachedPrototypes = signs;
|
||||
foreach (var proto in _cachedPrototypes)
|
||||
{
|
||||
SignOptions.AddItem(Loc.GetString(proto.Name));
|
||||
}
|
||||
|
||||
SignOptions.OnItemSelected += idx =>
|
||||
{
|
||||
@@ -32,18 +22,21 @@ public sealed partial class BarSignMenu : FancyWindow
|
||||
SignOptions.SelectId(idx.Id);
|
||||
};
|
||||
|
||||
if (currentSign != null)
|
||||
}
|
||||
|
||||
public void LoadSigns(List<BarSignPrototype> signs)
|
||||
{
|
||||
_cachedPrototypes.Clear();
|
||||
_cachedPrototypes = signs;
|
||||
|
||||
foreach (var proto in _cachedPrototypes)
|
||||
{
|
||||
var idx = _cachedPrototypes.IndexOf(currentSign);
|
||||
SignOptions.TrySelectId(idx);
|
||||
SignOptions.AddItem(Loc.GetString(proto.Name));
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(BarSignPrototype newSign)
|
||||
{
|
||||
if (_currentId != null && newSign.ID == _currentId)
|
||||
return;
|
||||
_currentId = newSign.ID;
|
||||
var idx = _cachedPrototypes.IndexOf(newSign);
|
||||
SignOptions.TrySelectId(idx);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Body.Systems;
|
||||
|
||||
namespace Content.Client.Body.Systems;
|
||||
|
||||
public sealed class BodySystem : SharedBodySystem
|
||||
{
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
using Content.Shared.Body.Systems;
|
||||
|
||||
namespace Content.Client.Body.Systems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class MetabolizerSystem : SharedMetabolizerSystem;
|
||||
@@ -0,0 +1,264 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Body;
|
||||
|
||||
public sealed class VisualBodySystem : SharedVisualBodySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly MarkingManager _marking = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<VisualOrganComponent, OrganGotInsertedEvent>(OnOrganGotInserted);
|
||||
SubscribeLocalEvent<VisualOrganComponent, OrganGotRemovedEvent>(OnOrganGotRemoved);
|
||||
SubscribeLocalEvent<VisualOrganComponent, AfterAutoHandleStateEvent>(OnOrganState);
|
||||
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotInsertedEvent>(OnMarkingsGotInserted);
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotRemovedEvent>(OnMarkingsGotRemoved);
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, AfterAutoHandleStateEvent>(OnMarkingsState);
|
||||
|
||||
SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent>>(OnMarkingsChangedVisibility);
|
||||
|
||||
Subs.CVar(_cfg, CCVars.AccessibilityClientCensorNudity, OnCensorshipChanged, true);
|
||||
Subs.CVar(_cfg, CCVars.AccessibilityServerCensorNudity, OnCensorshipChanged, true);
|
||||
}
|
||||
|
||||
private void OnCensorshipChanged(bool value)
|
||||
{
|
||||
var query = AllEntityQuery<OrganComponent, VisualOrganMarkingsComponent>();
|
||||
while (query.MoveNext(out var ent, out var organComp, out var markingsComp))
|
||||
{
|
||||
if (organComp.Body is not { } body)
|
||||
continue;
|
||||
|
||||
RemoveMarkings((ent, markingsComp), body);
|
||||
ApplyMarkings((ent, markingsComp), body);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOrganGotInserted(Entity<VisualOrganComponent> ent, ref OrganGotInsertedEvent args)
|
||||
{
|
||||
ApplyVisual(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnOrganGotRemoved(Entity<VisualOrganComponent> ent, ref OrganGotRemovedEvent args)
|
||||
{
|
||||
RemoveVisual(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnOrganState(Entity<VisualOrganComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
ApplyVisual(ent, body);
|
||||
}
|
||||
|
||||
private void ApplyVisual(Entity<VisualOrganComponent> ent, EntityUid target)
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true))
|
||||
return;
|
||||
|
||||
_sprite.LayerSetData(target, index, ent.Comp.Data);
|
||||
}
|
||||
|
||||
private void RemoveVisual(Entity<VisualOrganComponent> ent, EntityUid target)
|
||||
{
|
||||
if (!_sprite.LayerMapTryGet(target, ent.Comp.Layer, out var index, true))
|
||||
return;
|
||||
|
||||
_sprite.LayerSetRsiState(target, index, RSI.StateId.Invalid);
|
||||
}
|
||||
|
||||
private void OnMarkingsGotInserted(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotInsertedEvent args)
|
||||
{
|
||||
ApplyMarkings(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnMarkingsGotRemoved(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotRemovedEvent args)
|
||||
{
|
||||
RemoveMarkings(ent, args.Target);
|
||||
}
|
||||
|
||||
private void OnMarkingsState(Entity<VisualOrganMarkingsComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
RemoveMarkings(ent, body);
|
||||
ApplyMarkings(ent, body);
|
||||
}
|
||||
|
||||
protected override void SetOrganColor(Entity<VisualOrganComponent> ent, Color color)
|
||||
{
|
||||
base.SetOrganColor(ent, color);
|
||||
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
ApplyVisual(ent, body);
|
||||
}
|
||||
|
||||
protected override void SetOrganMarkings(Entity<VisualOrganMarkingsComponent> ent, Dictionary<HumanoidVisualLayers, List<Marking>> markings)
|
||||
{
|
||||
base.SetOrganMarkings(ent, markings);
|
||||
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
RemoveMarkings(ent, body);
|
||||
ApplyMarkings(ent, body);
|
||||
}
|
||||
|
||||
protected override void SetOrganAppearance(Entity<VisualOrganComponent> ent, PrototypeLayerData data)
|
||||
{
|
||||
base.SetOrganAppearance(ent, data);
|
||||
|
||||
if (Comp<OrganComponent>(ent).Body is not { } body)
|
||||
return;
|
||||
|
||||
ApplyVisual(ent, body);
|
||||
}
|
||||
|
||||
private IEnumerable<Marking> AllMarkings(Entity<VisualOrganMarkingsComponent> ent)
|
||||
{
|
||||
foreach (var markings in ent.Comp.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markings)
|
||||
{
|
||||
yield return marking;
|
||||
}
|
||||
}
|
||||
|
||||
var censorNudity = _cfg.GetCVar(CCVars.AccessibilityClientCensorNudity) || _cfg.GetCVar(CCVars.AccessibilityServerCensorNudity);
|
||||
if (!censorNudity)
|
||||
yield break;
|
||||
|
||||
var group = _prototype.Index(ent.Comp.MarkingData.Group);
|
||||
foreach (var layer in ent.Comp.MarkingData.Layers)
|
||||
{
|
||||
if (!group.Limits.TryGetValue(layer, out var layerLimits))
|
||||
continue;
|
||||
|
||||
if (layerLimits.NudityDefault.Count < 1)
|
||||
continue;
|
||||
|
||||
var markings = ent.Comp.Markings.GetValueOrDefault(layer) ?? [];
|
||||
if (markings.Any(marking => _marking.TryGetMarking(marking, out var proto) && proto.BodyPart == layer))
|
||||
continue;
|
||||
|
||||
foreach (var marking in layerLimits.NudityDefault)
|
||||
{
|
||||
yield return new(marking, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
|
||||
{
|
||||
var applied = new List<Marking>();
|
||||
foreach (var marking in AllMarkings(ent))
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
if (!_sprite.LayerMapTryGet(target, proto.BodyPart, out var index, true))
|
||||
continue;
|
||||
|
||||
for (var i = 0; i < proto.Sprites.Count; i++)
|
||||
{
|
||||
var sprite = proto.Sprites[i];
|
||||
|
||||
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{proto.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet(target, layerId, out _, false))
|
||||
{
|
||||
var layer = _sprite.AddLayer(target, sprite, index + i + 1);
|
||||
_sprite.LayerMapSet(target, layerId, layer);
|
||||
_sprite.LayerSetSprite(target, layerId, rsi);
|
||||
}
|
||||
|
||||
if (marking.MarkingColors is not null && i < marking.MarkingColors.Count)
|
||||
_sprite.LayerSetColor(target, layerId, marking.MarkingColors[i]);
|
||||
else
|
||||
_sprite.LayerSetColor(target, layerId, Color.White);
|
||||
}
|
||||
|
||||
applied.Add(marking);
|
||||
}
|
||||
ent.Comp.AppliedMarkings = applied;
|
||||
}
|
||||
|
||||
private void RemoveMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
|
||||
{
|
||||
foreach (var marking in ent.Comp.AppliedMarkings)
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
foreach (var sprite in proto.Sprites)
|
||||
{
|
||||
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{proto.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet(target, layerId, out var index, false))
|
||||
continue;
|
||||
|
||||
_sprite.LayerMapRemove(target, layerId);
|
||||
_sprite.RemoveLayer(target, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMarkingsChangedVisibility(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent> args)
|
||||
{
|
||||
if (!ent.Comp.HideableLayers.Contains(args.Args.Layer))
|
||||
return;
|
||||
|
||||
foreach (var markings in ent.Comp.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markings)
|
||||
{
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
if (proto.BodyPart != args.Args.Layer && !(ent.Comp.DependentHidingLayers.TryGetValue(args.Args.Layer, out var dependent) && dependent.Contains(proto.BodyPart)))
|
||||
continue;
|
||||
|
||||
foreach (var sprite in proto.Sprites)
|
||||
{
|
||||
DebugTools.Assert(sprite is SpriteSpecifier.Rsi);
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{proto.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet(args.Body.Owner, layerId, out var index, true))
|
||||
continue;
|
||||
|
||||
_sprite.LayerSetVisible(args.Body.Owner, index, args.Args.Visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,9 +70,9 @@ namespace Content.Client.Cargo.BUI
|
||||
|
||||
_menu.OnClose += Close;
|
||||
|
||||
_menu.OnItemSelected += (args) =>
|
||||
_menu.OnItemSelected += (row) =>
|
||||
{
|
||||
if (args.Button.Parent is not CargoProductRow row)
|
||||
if (row == null)
|
||||
return;
|
||||
|
||||
description.Clear();
|
||||
@@ -175,23 +175,23 @@ namespace Content.Client.Cargo.BUI
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RemoveOrder(ButtonEventArgs args)
|
||||
private void RemoveOrder(CargoOrderData? order)
|
||||
{
|
||||
if (args.Button.Parent?.Parent is not CargoOrderRow row || row.Order == null)
|
||||
if (order == null)
|
||||
return;
|
||||
|
||||
SendMessage(new CargoConsoleRemoveOrderMessage(row.Order.OrderId));
|
||||
SendMessage(new CargoConsoleRemoveOrderMessage(order.OrderId));
|
||||
}
|
||||
|
||||
private void ApproveOrder(ButtonEventArgs args)
|
||||
private void ApproveOrder(CargoOrderData? order)
|
||||
{
|
||||
if (args.Button.Parent?.Parent is not CargoOrderRow row || row.Order == null)
|
||||
if (order == null)
|
||||
return;
|
||||
|
||||
if (OrderCount >= OrderCapacity)
|
||||
return;
|
||||
|
||||
SendMessage(new CargoConsoleApproveOrderMessage(row.Order.OrderId));
|
||||
SendMessage(new CargoConsoleApproveOrderMessage(order.OrderId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,226 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
SetSize="600 600"
|
||||
MinSize="600 600">
|
||||
<BoxContainer Orientation="Vertical" Margin="15 5 15 10">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'cargo-console-menu-account-name-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<RichTextLabel Name="AccountNameLabel"
|
||||
Text="{Loc 'cargo-console-menu-account-name-none-text'}" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'cargo-console-menu-points-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<RichTextLabel Name="PointsLabel"
|
||||
Text="$0" />
|
||||
</BoxContainer>
|
||||
<Control MinHeight="10"/>
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="540 390"
|
||||
SetSize="995 600">
|
||||
|
||||
<!-- Main Container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
|
||||
<TabContainer Name="TabContainer" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<OptionButton Name="Categories"
|
||||
Prefix="{Loc 'cargo-console-menu-categories-label'}"
|
||||
HorizontalExpand="True" />
|
||||
<LineEdit Name="SearchBar"
|
||||
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<ScrollContainer HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="2">
|
||||
<BoxContainer Name="Products"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<!-- Products get added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<Control MinHeight="5" Name="OrdersSpacer"/>
|
||||
<PanelContainer VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="1"
|
||||
Name="Orders">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" Margin="5">
|
||||
<Label Text="{Loc 'cargo-console-menu-requests-label'}" />
|
||||
<BoxContainer Name="Requests"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
<!-- Requests are added here by code -->
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<!-- Funds tab -->
|
||||
<BoxContainer Orientation="Vertical" Margin="15">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
|
||||
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
|
||||
<OptionButton Name="ActionOptions"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
|
||||
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="15"/>
|
||||
<BoxContainer HorizontalAlignment="Center">
|
||||
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
<Control VerticalExpand="True"/>
|
||||
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
|
||||
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Sub-Main Container -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalExpand="True"
|
||||
Margin="8 4 8 6">
|
||||
|
||||
<!-- Left Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="4"
|
||||
Margin="0 0 8 0"
|
||||
HorizontalExpand="True">
|
||||
|
||||
<!-- Info -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<GridContainer Columns="3">
|
||||
|
||||
<!-- Account -->
|
||||
<Label Text="{Loc 'cargo-console-menu-account-name-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -2"/>
|
||||
|
||||
<RichTextLabel Name="AccountNameLabel"
|
||||
Text="{Loc 'cargo-console-menu-account-name-none-text'}"
|
||||
Margin="4 0"/>
|
||||
|
||||
<!-- Balance -->
|
||||
<Label Text="{Loc 'cargo-console-menu-points-label'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -2"/>
|
||||
|
||||
<RichTextLabel Name="PointsLabel"
|
||||
Text="$0"
|
||||
Margin="4 0" />
|
||||
|
||||
<!-- Orders Count/Capacity -->
|
||||
<Label Text="{Loc 'cargo-console-menu-order-capacity-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -2 0 -1"/>
|
||||
|
||||
<Label Name="ShuttleCapacityLabel"
|
||||
Text="0/20"
|
||||
Margin="4 0"/>
|
||||
</GridContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 4.5 -8 0"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Search -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="0 2 0 0">
|
||||
|
||||
<LineEdit Name="SearchBar"
|
||||
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
|
||||
HorizontalExpand="True" />
|
||||
|
||||
<OptionButton Name="Categories"
|
||||
Prefix="{Loc 'cargo-console-menu-categories-label'}"
|
||||
StyleClasses="OpenLeft"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Product list -->
|
||||
<ScrollContainer
|
||||
HorizontalExpand="False"
|
||||
VerticalExpand="True"
|
||||
HScrollEnabled="False">
|
||||
|
||||
<BoxContainer Name="Products"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
|
||||
<!-- Products get added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -8"/>
|
||||
|
||||
<!-- Right Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SizeFlagsStretchRatio="0.8"
|
||||
HorizontalExpand="True"
|
||||
Name="RightPart">
|
||||
|
||||
<!-- Requests Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="2">
|
||||
|
||||
<!-- Title -->
|
||||
<controls:StripeBack>
|
||||
<Label Text="{Loc 'cargo-console-menu-requests-label'}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="4"/>
|
||||
</controls:StripeBack>
|
||||
|
||||
<PanelContainer VerticalExpand="True"
|
||||
Margin="0 -4 0 0">
|
||||
|
||||
<!-- Background -->
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#040404" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Name="Requests"
|
||||
Orientation="Vertical"
|
||||
StyleClasses="transparentItemList"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="8"
|
||||
Margin="8">
|
||||
|
||||
<!-- Requests are added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Orders Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
|
||||
<!-- Title -->
|
||||
<controls:StripeBack>
|
||||
<Label Text="{Loc 'cargo-console-menu-orders-label'}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="4"/>
|
||||
</controls:StripeBack>
|
||||
|
||||
<PanelContainer VerticalExpand="True"
|
||||
Margin="0 -4 0 0">
|
||||
|
||||
<!-- Background -->
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#040404" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
StyleClasses="transparentItemList"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="6">
|
||||
|
||||
<!-- Orders are added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Funds tab -->
|
||||
<BoxContainer Orientation="Vertical" Margin="15">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
|
||||
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
|
||||
<OptionButton Name="ActionOptions"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
|
||||
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="15"/>
|
||||
<BoxContainer HorizontalAlignment="Center">
|
||||
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
<Control VerticalExpand="True"/>
|
||||
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
|
||||
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</TabContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- TODO: Create customControls element -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="12 0 6 2"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
<!-- Footer title -->
|
||||
<Label Text="{Loc 'cargo-console-menu-flavor-left'}"
|
||||
StyleClasses="WindowFooterText" />
|
||||
|
||||
<!-- Version -->
|
||||
<Label Text="{Loc 'cargo-console-menu-flavor-right'}"
|
||||
StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 4 0" />
|
||||
|
||||
<TextureRect StyleClasses="NTLogoDark"
|
||||
Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -29,9 +30,9 @@ namespace Content.Client.Cargo.UI
|
||||
private readonly EntityQuery<CargoOrderConsoleComponent> _orderConsoleQuery;
|
||||
private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
|
||||
|
||||
public event Action<ButtonEventArgs>? OnItemSelected;
|
||||
public event Action<ButtonEventArgs>? OnOrderApproved;
|
||||
public event Action<ButtonEventArgs>? OnOrderCanceled;
|
||||
public event Action<CargoProductRow?>? OnItemSelected;
|
||||
public event Action<CargoOrderData?>? OnOrderApproved;
|
||||
public event Action<CargoOrderData?>? OnOrderCanceled;
|
||||
|
||||
public event Action<ProtoId<CargoAccountPrototype>?, int>? OnAccountAction;
|
||||
|
||||
@@ -164,7 +165,7 @@ namespace Content.Client.Cargo.UI
|
||||
};
|
||||
button.MainButton.OnPressed += args =>
|
||||
{
|
||||
OnItemSelected?.Invoke(args);
|
||||
OnItemSelected?.Invoke(button);
|
||||
};
|
||||
Products.AddChild(button);
|
||||
}
|
||||
@@ -210,38 +211,66 @@ namespace Content.Client.Cargo.UI
|
||||
|
||||
foreach (var order in orders)
|
||||
{
|
||||
if (order.Approved)
|
||||
if (order.Approved || !_protoManager.Resolve(order.Product, out var productProto))
|
||||
continue;
|
||||
|
||||
var product = _protoManager.Index<EntityPrototype>(order.ProductId);
|
||||
var productName = product.Name;
|
||||
var product = _protoManager.Index<EntityPrototype>(productProto.Product);
|
||||
var productName = productProto.Name;
|
||||
var requester = !string.IsNullOrEmpty(order.Requester) ?
|
||||
order.Requester : Loc.GetString("cargo-console-menu-order-row-alerts-requester-unknown");
|
||||
var account = _protoManager.Index(order.Account);
|
||||
|
||||
var row = new CargoOrderRow
|
||||
{
|
||||
Order = order,
|
||||
|
||||
Title =
|
||||
{
|
||||
Text = Loc.GetString(
|
||||
"cargo-console-menu-order-row-title",
|
||||
("productName", productName),
|
||||
("orderAmount", order.OrderQuantity),
|
||||
("orderPrice", productProto.Cost)),
|
||||
},
|
||||
|
||||
Stride =
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = account.Color,
|
||||
ContentMarginBottomOverride = 2,
|
||||
},
|
||||
},
|
||||
|
||||
Icon = { Texture = _spriteSystem.Frame0(product) },
|
||||
|
||||
ProductName =
|
||||
{
|
||||
Text = Loc.GetString(
|
||||
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
|
||||
("productName", productName),
|
||||
("orderAmount", order.OrderQuantity),
|
||||
("orderRequester", order.Requester),
|
||||
("orderRequester", requester),
|
||||
("accountColor", account.Color),
|
||||
("account", Loc.GetString(account.Code)))
|
||||
},
|
||||
|
||||
Description =
|
||||
{
|
||||
Text = Loc.GetString("cargo-console-menu-order-reason-description",
|
||||
("reason", order.Reason))
|
||||
Text = !string.IsNullOrEmpty(order.Reason) ?
|
||||
Loc.GetString(
|
||||
"cargo-console-menu-order-row-product-description",
|
||||
("orderReason", order.Reason))
|
||||
:
|
||||
Loc.GetString(
|
||||
"cargo-console-menu-order-row-product-description",
|
||||
("orderReason", Loc.GetString("cargo-console-menu-order-row-alerts-reason-absent")))
|
||||
}
|
||||
};
|
||||
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
|
||||
|
||||
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(order); };
|
||||
|
||||
// TODO: Disable based on access.
|
||||
row.SetApproveVisible(orderConsole.Mode != CargoOrderConsoleMode.SendToPrimary);
|
||||
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
|
||||
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(order); };
|
||||
Requests.AddChild(row);
|
||||
}
|
||||
}
|
||||
@@ -294,8 +323,7 @@ namespace Content.Client.Cargo.UI
|
||||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
|
||||
_timing.CurTime < orderConsole.NextAccountActionTime;
|
||||
|
||||
OrdersSpacer.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
Orders.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
RightPart.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,53 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'cargo-console-order-menu-title'}">
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'cargo-console-order-menu-title'}"
|
||||
MinSize="460 261">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<GridContainer Columns="2">
|
||||
<Label Text="{Loc 'cargo-console-order-menu-product-label'}" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-product-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<Label Name="ProductName"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-description-label'}" />
|
||||
Access="Public" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-description-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<RichTextLabel Name="Description"
|
||||
Access="Public"
|
||||
VerticalExpand="True"
|
||||
SetWidth="350"/>
|
||||
<Label Text="{Loc 'cargo-console-order-menu-cost-label'}" />
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
MaxWidth="460" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-cost-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<Label Name="PointCost"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-requester-label'}" />
|
||||
Access="Public" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-requester-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<LineEdit Name="Requester"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-reason-label'}" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-reason-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<LineEdit Name="Reason"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-amount-label'}" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-amount-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<SpinBox Name="Amount"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
Value="1" />
|
||||
</GridContainer>
|
||||
<Control VerticalExpand="True"/>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 6 0 2"/>
|
||||
<Button Name="SubmitButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-order-menu-submit-button'}"
|
||||
TextAlign="Center" />
|
||||
VerticalAlignment="Bottom" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,33 +1,81 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 1">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
RectClipContent="True" />
|
||||
<Control MinWidth="5"/>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<RichTextLabel Name="ProductName"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText" />
|
||||
<Label Name="Description"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<PanelContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="BackgroundPanel">
|
||||
|
||||
<!-- Main Container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="6"
|
||||
Margin="-14 -2">
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Control>
|
||||
<PanelContainer StyleClasses="WindowHeadingBackground" />
|
||||
|
||||
<BoxContainer Margin="6">
|
||||
<Label Name="Title"
|
||||
Access="Public"
|
||||
MaxHeight="28"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
<PanelContainer Name="Stride"
|
||||
Access="Public"
|
||||
StyleClasses="LowDivider" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Info -->
|
||||
<BoxContainer>
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
Margin="4"
|
||||
Stretch="KeepAspectCentered"
|
||||
RectClipContent="True"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="4 0"/>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
|
||||
<RichTextLabel Name="ProductName"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
StyleClasses="LabelSubText" />
|
||||
|
||||
<Label Name="Description"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<!-- Buttons -->
|
||||
<!-- Btn's position hardcoded (args.Button.Parent?.Parent?.Parent type) in CargoConsoleBUI 158 & 166 line -->
|
||||
<BoxContainer Margin="6">
|
||||
<Button Name="Approve"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-order-row-button-approve'}"
|
||||
StyleClasses="OpenRight"
|
||||
HorizontalExpand="True"/>
|
||||
|
||||
<Button Name="Cancel"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-order-row-button-cancel'}"
|
||||
StyleClasses="OpenLeft"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<Button Name="Approve"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-cargo-order-row-approve-button'}"
|
||||
StyleClasses="OpenRight" />
|
||||
<Button Name="Cancel"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-cargo-order-row-cancel-button'}"
|
||||
StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True">
|
||||
<Button Name="MainButton"
|
||||
ToolTip=""
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
RectClipContent="True" />
|
||||
<Label Name="ProductName"
|
||||
Access="Public"
|
||||
HorizontalExpand="True" />
|
||||
<PanelContainer StyleClasses="BackgroundDark">
|
||||
<Label Name="PointCost"
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer HorizontalExpand="True">
|
||||
<!-- Btn position hardcoded (args.Button.Parent?.Parent type) in CargoConsoleBUI 71 line -->
|
||||
<Button Name="MainButton"
|
||||
ToolTip=""
|
||||
Access="Public"
|
||||
VerticalExpand="False"
|
||||
StyleClasses="OpenBoth" />
|
||||
|
||||
<!-- Icon & Name -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="4 0">
|
||||
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
RectClipContent="True" />
|
||||
|
||||
<Label Name="ProductName"
|
||||
Access="Public"
|
||||
MinSize="52 32"
|
||||
Align="Right"
|
||||
Margin="0 0 5 0"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
HorizontalExpand="True"
|
||||
ClipText="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<Label Name="PointCost"
|
||||
Access="Public"
|
||||
MinSize="56 32"
|
||||
Align="Right"
|
||||
Margin="0 0 5 0"
|
||||
HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -7,7 +7,7 @@ using Robust.Client.UserInterface.XAML;
|
||||
namespace Content.Client.Cargo.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CargoProductRow : PanelContainer
|
||||
public sealed partial class CargoProductRow : BoxContainer
|
||||
{
|
||||
public CargoProductPrototype? Product { get; set; }
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ namespace Content.Client.Cargo.UI
|
||||
|
||||
foreach (var order in orders)
|
||||
{
|
||||
var product = protoManager.Index<EntityPrototype>(order.ProductId);
|
||||
if (!protoManager.Resolve(order.Product, out var productProto))
|
||||
continue;
|
||||
|
||||
var product = protoManager.Index<EntityPrototype>(productProto.Product);
|
||||
var productName = product.Name;
|
||||
var account = protoManager.Index(order.Account);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
@@ -94,7 +94,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
|
||||
continue;
|
||||
|
||||
//these bloat the hell out of blood/fat
|
||||
if (entProto.HasComponent<BodyPartComponent>())
|
||||
if (entProto.HasComponent<OrganComponent>())
|
||||
continue;
|
||||
|
||||
//these feel obvious...
|
||||
@@ -116,7 +116,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
|
||||
}
|
||||
|
||||
|
||||
if (extractableComponent.GrindableSolution is { } grindableSolutionId &&
|
||||
if (extractableComponent.GrindableSolutionName is { } grindableSolutionId &&
|
||||
entProto.TryGetComponent<SolutionContainerManagerComponent>(out var manager, EntityManager.ComponentFactory) &&
|
||||
_solutionContainer.TryGetSolution(manager, grindableSolutionId, out var grindableSolution))
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="100 150">
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="Inset">
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="BackgroundPanelDark">
|
||||
<BoxContainer Name="GeneticScannerContents" Margin="5 5 5 5" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
<Label HorizontalAlignment="Center" Text="{Loc 'cloning-console-window-scanner-details-label'}" />
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
|
||||
@@ -35,7 +35,7 @@
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<Control MinSize="50 5" />
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="Inset">
|
||||
<PanelContainer VerticalExpand="True" StyleClasses="BackgroundPanelDark">
|
||||
<BoxContainer Name="CloningPodContents" Margin="5 5 5 5" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
<Label HorizontalAlignment="Center" Text="{Loc 'cloning-console-window-pod-details-label'}" />
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
|
||||
@@ -271,7 +271,7 @@ public sealed class ClientClothingSystem : ClothingSystem
|
||||
// Select displacement maps
|
||||
var displacementData = inventory.Displacements.GetValueOrDefault(slot); //Default unsexed map
|
||||
|
||||
var equipeeSex = CompOrNull<HumanoidAppearanceComponent>(equipee)?.Sex;
|
||||
var equipeeSex = CompOrNull<HumanoidProfileComponent>(equipee)?.Sex;
|
||||
if (equipeeSex != null)
|
||||
{
|
||||
switch (equipeeSex)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Client.Hands.Systems;
|
||||
using Content.Client.NPC.HTN;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.CombatMode;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -59,11 +58,6 @@ public sealed class CombatModeSystem : SharedCombatModeSystem
|
||||
UpdateHud(entity);
|
||||
}
|
||||
|
||||
protected override bool IsNpc(EntityUid uid)
|
||||
{
|
||||
return HasComp<HTNComponent>(uid);
|
||||
}
|
||||
|
||||
private void UpdateHud(EntityUid entity)
|
||||
{
|
||||
if (entity != _playerManager.LocalEntity || !Timing.IsFirstTimePredicted)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
using Content.Shared.Body.Organ;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Client.Commands;
|
||||
|
||||
public sealed class HideMechanismsCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
|
||||
|
||||
public override string Command => "hidemechanisms";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var query = EntityManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out _, out var sprite))
|
||||
{
|
||||
_spriteSystem.SetContainerOccluded((uid, sprite), false);
|
||||
|
||||
var tempParent = uid;
|
||||
while (_containerSystem.TryGetContainingContainer((tempParent, null, null), out var container))
|
||||
{
|
||||
if (!container.ShowContents)
|
||||
{
|
||||
_spriteSystem.SetContainerOccluded((uid, sprite), true);
|
||||
break;
|
||||
}
|
||||
|
||||
tempParent = container.Owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Content.Shared.Body.Organ;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Client.Commands;
|
||||
|
||||
public sealed class ShowMechanismsCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
|
||||
|
||||
public override string Command => "showmechanisms";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var query = EntityManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out _, out var sprite))
|
||||
{
|
||||
_spriteSystem.SetContainerOccluded((uid, sprite), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,6 @@ namespace Content.Client.Construction
|
||||
_ghosts.Add(comp.GhostId, ghost.Value);
|
||||
|
||||
var sprite = Comp<SpriteComponent>(ghost.Value);
|
||||
_sprite.SetColor((ghost.Value, sprite), new Color(48, 255, 48, 128));
|
||||
|
||||
if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory))
|
||||
{
|
||||
@@ -306,20 +305,11 @@ namespace Content.Client.Construction
|
||||
var targetSprite = EnsureComp<SpriteComponent>(dummy);
|
||||
EntityManager.System<AppearanceSystem>().OnChangeData(dummy, targetSprite);
|
||||
|
||||
for (var i = 0; i < targetSprite.AllLayers.Count(); i++)
|
||||
_sprite.CopySprite((dummy, targetSprite), (ghost.Value, sprite));
|
||||
|
||||
for (var i = 0; i < sprite.AllLayers.Count(); i++)
|
||||
{
|
||||
if (!targetSprite[i].Visible || !targetSprite[i].RsiState.IsValid)
|
||||
continue;
|
||||
|
||||
var rsi = targetSprite[i].Rsi ?? targetSprite.BaseRSI;
|
||||
if (rsi is null || !rsi.TryGetState(targetSprite[i].RsiState, out var state) ||
|
||||
state.StateId.Name is null)
|
||||
continue;
|
||||
|
||||
_sprite.AddBlankLayer((ghost.Value, sprite), i);
|
||||
_sprite.LayerSetSprite((ghost.Value, sprite), i, new SpriteSpecifier.Rsi(rsi.Path, state.StateId.Name));
|
||||
sprite.LayerSetShader(i, "unshaded");
|
||||
_sprite.LayerSetVisible((ghost.Value, sprite), i, true);
|
||||
}
|
||||
|
||||
Del(dummy);
|
||||
@@ -327,6 +317,8 @@ namespace Content.Client.Construction
|
||||
else
|
||||
return false;
|
||||
|
||||
_sprite.SetColor((ghost.Value, sprite), new Color(48, 255, 48, 128));
|
||||
|
||||
if (prototype.CanBuildInImpassable)
|
||||
EnsureComp<WallMountComponent>(ghost.Value).Arc = new(Math.Tau);
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Corvax.GuideGenerator;
|
||||
using Content.Client.Gameplay;
|
||||
using Robust.Client;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Corvax.ExportSprites;
|
||||
|
||||
public sealed class EntityScreenshotGenerator
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IBaseClient _baseClient = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly EntityScreenshotRenderService _renderService = default!;
|
||||
[Dependency] private readonly IGameController _gameController = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private bool _started;
|
||||
private bool _startupRequested;
|
||||
private bool _pendingStart;
|
||||
private const uint WarmupFrames = 3;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = _logManager.GetSawmill("entity-screenshot-generator");
|
||||
_renderService.Initialize();
|
||||
}
|
||||
|
||||
public bool PostInit()
|
||||
{
|
||||
if (!_cfg.GetCVar(CCVars.EntityScreenshotGeneratorEnabled))
|
||||
return false;
|
||||
|
||||
if (_baseClient.RunLevel == ClientRunLevel.SinglePlayerGame)
|
||||
{
|
||||
_pendingStart = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_startupRequested)
|
||||
return true;
|
||||
|
||||
_startupRequested = true;
|
||||
_baseClient.StartSinglePlayer();
|
||||
_stateManager.RequestStateChange<GameplayState>();
|
||||
_pendingStart = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (!_pendingStart || _started)
|
||||
return;
|
||||
|
||||
if (_baseClient.RunLevel != ClientRunLevel.SinglePlayerGame)
|
||||
return;
|
||||
|
||||
TryStart();
|
||||
}
|
||||
|
||||
public bool TryStart()
|
||||
{
|
||||
if (_started || !_cfg.GetCVar(CCVars.EntityScreenshotGeneratorEnabled))
|
||||
return _started;
|
||||
|
||||
try
|
||||
{
|
||||
_entitySystemManager.GetEntitySystem<SharedMapSystem>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = RunAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
_pendingStart = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task RunAsync()
|
||||
{
|
||||
var outputDir = new ResPath(_cfg.GetCVar(CCVars.EntityScreenshotOutputPath));
|
||||
var wasPaused = _gameTiming.Paused;
|
||||
|
||||
try
|
||||
{
|
||||
_gameTiming.Paused = true;
|
||||
|
||||
var failures = new List<string>();
|
||||
var exported = 0;
|
||||
var mapSystem = _entitySystemManager.GetEntitySystem<SharedMapSystem>();
|
||||
var allowedIds = EntityProjectHelper.GetProjectEntityIds();
|
||||
var prototypes = _prototypeManager.EnumeratePrototypes<EntityPrototype>()
|
||||
.Where(proto =>
|
||||
!proto.Abstract &&
|
||||
proto.Components.ContainsKey("Sprite") &&
|
||||
EntityProjectHelper.MatchesAllowedIds(proto.ID, allowedIds))
|
||||
.OrderBy(proto => proto.ID)
|
||||
.ToList();
|
||||
var previewMap = mapSystem.CreateMap(out var mapId);
|
||||
var previewGrid = _mapManager.CreateGridEntity(mapId);
|
||||
|
||||
if (!_resourceManager.UserData.IsDir(outputDir))
|
||||
_resourceManager.UserData.CreateDir(outputDir);
|
||||
|
||||
foreach (var proto in prototypes)
|
||||
{
|
||||
EntityUid entity = default;
|
||||
|
||||
try
|
||||
{
|
||||
entity = _entityManager.SpawnEntity(proto.ID, new EntityCoordinates(previewGrid.Owner, default));
|
||||
|
||||
await WaitForEntityAppearanceAsync(entity);
|
||||
ApplyPrototypeAppearance(entity, proto);
|
||||
await WaitForEntityAppearanceAsync(entity, 1);
|
||||
|
||||
await _renderService.Export(entity, Direction.South, outputDir / $"{proto.ID}.png");
|
||||
exported++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
failures.Add($"{proto.ID}: {e.Message}");
|
||||
_sawmill.Error($"Failed to export {proto.ID}: {e}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_entityManager.EntityExists(entity))
|
||||
_entityManager.DeleteEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.Count > 0)
|
||||
WriteFailures(outputDir, failures);
|
||||
|
||||
if (_entityManager.EntityExists(previewGrid))
|
||||
_entityManager.DeleteEntity(previewGrid);
|
||||
|
||||
if (_entityManager.EntityExists(previewMap))
|
||||
_entityManager.DeleteEntity(previewMap);
|
||||
|
||||
_gameController.Shutdown($"Entity screenshot generation complete. Exported {exported}/{prototypes.Count}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Entity screenshot generation crashed: {e}");
|
||||
WriteFailures(outputDir, new[] { e.ToString() });
|
||||
_gameController.Shutdown("Entity screenshot generation failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gameTiming.Paused = wasPaused;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteFailures(ResPath outputDir, IEnumerable<string> failures)
|
||||
{
|
||||
if (!_resourceManager.UserData.IsDir(outputDir))
|
||||
_resourceManager.UserData.CreateDir(outputDir);
|
||||
|
||||
using var writer = _resourceManager.UserData.OpenWriteText(outputDir / "failures.txt");
|
||||
foreach (var failure in failures)
|
||||
{
|
||||
writer.WriteLine(failure);
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
private async Task WaitForEntityAppearanceAsync(EntityUid entity)
|
||||
{
|
||||
await WaitForEntityAppearanceAsync(entity, WarmupFrames);
|
||||
}
|
||||
|
||||
private async Task WaitForEntityAppearanceAsync(EntityUid entity, uint frames)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent(entity, out MetaDataComponent? metadata))
|
||||
return;
|
||||
|
||||
if (!metadata.EntityInitialized)
|
||||
_entityManager.InitializeAndStartEntity((entity, metadata), doMapInit: true);
|
||||
|
||||
var targetFrame = _gameTiming.CurFrame + frames;
|
||||
|
||||
while (_entityManager.EntityExists(entity) && _gameTiming.CurFrame < targetFrame)
|
||||
{
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPrototypeAppearance(EntityUid entity, EntityPrototype prototype)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent(entity, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
if (!prototype.TryGetComponent<SolutionContainerManagerComponent>(out var manager, _entityManager.ComponentFactory) ||
|
||||
manager.Solutions == null ||
|
||||
manager.Solutions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var solutionEntry = manager.Solutions.FirstOrDefault(entry => entry.Value.Volume > 0);
|
||||
if (string.IsNullOrEmpty(solutionEntry.Key))
|
||||
solutionEntry = manager.Solutions.First();
|
||||
|
||||
var solution = solutionEntry.Value;
|
||||
var appearanceSystem = _entitySystemManager.GetEntitySystem<SharedAppearanceSystem>();
|
||||
|
||||
appearanceSystem.SetData(entity, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearance);
|
||||
appearanceSystem.SetData(entity, SolutionContainerVisuals.Color, solution.GetColor(_prototypeManager), appearance);
|
||||
appearanceSystem.SetData(entity, SolutionContainerVisuals.SolutionName, solutionEntry.Key, appearance);
|
||||
|
||||
if (solution.GetPrimaryReagentId() is { } reagent)
|
||||
appearanceSystem.SetData(entity, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = Robust.Shared.Maths.Color;
|
||||
|
||||
namespace Content.Client.Corvax.ExportSprites;
|
||||
|
||||
public sealed class EntityScreenshotRenderService
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
|
||||
private EntityScreenshotRenderControl? _control;
|
||||
private bool _initialized;
|
||||
private readonly Dictionary<(ResPath Path, string State), Image<Rgba32>> _rsiStateImageCache = new();
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = _logManager.GetSawmill("corvax.entity-sprite-export");
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
foreach (var image in _rsiStateImageCache.Values)
|
||||
{
|
||||
image.Dispose();
|
||||
}
|
||||
|
||||
_rsiStateImageCache.Clear();
|
||||
|
||||
if (_control == null)
|
||||
return;
|
||||
|
||||
foreach (var queued in _control.QueuedTextures)
|
||||
{
|
||||
queued.Tcs.SetCanceled();
|
||||
}
|
||||
|
||||
_control.QueuedTextures.Clear();
|
||||
_ui.RootControl.RemoveChild(_control);
|
||||
_control = null;
|
||||
}
|
||||
|
||||
public async Task Export(EntityUid entity,
|
||||
Direction direction,
|
||||
ResPath outputPath,
|
||||
CancellationToken cancelToken = default)
|
||||
{
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
if (!_entityManager.TryGetComponent<SpriteComponent>(entity, out var spriteComp))
|
||||
return;
|
||||
|
||||
var renderBounds = GetRenderBounds(spriteComp);
|
||||
|
||||
if (renderBounds.Size.Equals(Vector2i.Zero))
|
||||
return;
|
||||
|
||||
var animationLayers = GetAnimatedLayers(spriteComp);
|
||||
if (animationLayers.Count == 0)
|
||||
{
|
||||
DeleteIfExists(GetAnimationDirectory(outputPath));
|
||||
await ExportFrame(entity, direction, outputPath, renderBounds, cancelToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var animationFrames = BuildAnimationFrames(entity, spriteComp, animationLayers);
|
||||
if (animationFrames.Count <= 1)
|
||||
{
|
||||
DeleteIfExists(GetAnimationDirectory(outputPath));
|
||||
await ExportFrame(entity, direction, outputPath, renderBounds, cancelToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await ExportAnimation(entity, direction, outputPath, renderBounds, spriteComp, animationLayers, animationFrames, cancelToken);
|
||||
}
|
||||
|
||||
private void EnsureControlAttached()
|
||||
{
|
||||
if (!_initialized)
|
||||
Initialize();
|
||||
|
||||
if (_control != null)
|
||||
return;
|
||||
|
||||
_control = new EntityScreenshotRenderControl();
|
||||
_ui.RootControl.AddChild(_control);
|
||||
}
|
||||
|
||||
private async Task ExportAnimation(
|
||||
EntityUid entity,
|
||||
Direction direction,
|
||||
ResPath outputPath,
|
||||
SpriteRenderBounds renderBounds,
|
||||
SpriteComponent spriteComp,
|
||||
IReadOnlyList<AnimatedLayerInfo> animationLayers,
|
||||
IReadOnlyList<AnimationFrameInfo> animationFrames,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
var originalTimes = new float[animationLayers.Count];
|
||||
for (var i = 0; i < animationLayers.Count; i++)
|
||||
{
|
||||
originalTimes[i] = spriteComp[animationLayers[i].Index].AnimationTime;
|
||||
}
|
||||
|
||||
var animationDir = GetAnimationDirectory(outputPath);
|
||||
DeleteIfExists(outputPath);
|
||||
DeleteIfExists(animationDir);
|
||||
_resourceManager.UserData.CreateDir(animationDir);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var t in animationFrames)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
ApplyAnimationTime(entity, spriteComp, animationLayers, t.RenderTimeSeconds);
|
||||
var framePath = animationDir / t.FileName;
|
||||
await ExportFrame(entity, direction, framePath, renderBounds, cancelToken);
|
||||
}
|
||||
|
||||
WriteAnimationMetadata(animationDir, animationFrames);
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (var i = 0; i < animationLayers.Count; i++)
|
||||
{
|
||||
_entitySystemManager.GetEntitySystem<SpriteSystem>()
|
||||
.LayerSetAnimationTime((entity, spriteComp), animationLayers[i].Index, originalTimes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportFrame(
|
||||
EntityUid entity,
|
||||
Direction direction,
|
||||
ResPath outputPath,
|
||||
SpriteRenderBounds renderBounds,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
if (TryExportFrameDirect(entity, direction, outputPath, renderBounds))
|
||||
return;
|
||||
|
||||
EnsureControlAttached();
|
||||
|
||||
var texture = _clyde.CreateRenderTarget(
|
||||
renderBounds.Size,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: "corvax-entity-export");
|
||||
|
||||
var tcs = new TaskCompletionSource(cancelToken);
|
||||
_control!.QueuedTextures.Enqueue((texture, direction, entity, outputPath, tcs));
|
||||
await tcs.Task;
|
||||
}
|
||||
|
||||
private static SpriteRenderBounds GetRenderBounds(SpriteComponent spriteComp)
|
||||
{
|
||||
var hasVisibleLayers = false;
|
||||
var min = Vector2i.Zero;
|
||||
var max = Vector2i.Zero;
|
||||
|
||||
foreach (var layer in spriteComp.AllLayers)
|
||||
{
|
||||
if (layer is not SpriteComponent.Layer spriteLayer || !spriteLayer.Visible)
|
||||
continue;
|
||||
|
||||
var pixelOffset = ToPixelOffset(spriteComp.Offset + spriteLayer.Offset);
|
||||
var halfSize = spriteLayer.PixelSize / 2;
|
||||
var topLeft = pixelOffset - halfSize;
|
||||
var bottomRight = topLeft + spriteLayer.PixelSize;
|
||||
|
||||
if (!hasVisibleLayers)
|
||||
{
|
||||
min = topLeft;
|
||||
max = bottomRight;
|
||||
hasVisibleLayers = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
min = Vector2i.ComponentMin(min, topLeft);
|
||||
max = Vector2i.ComponentMax(max, bottomRight);
|
||||
}
|
||||
|
||||
return !hasVisibleLayers
|
||||
? new SpriteRenderBounds(Vector2i.Zero, Vector2i.Zero)
|
||||
: new SpriteRenderBounds(min, max - min);
|
||||
}
|
||||
|
||||
private static ResPath GetAnimationDirectory(ResPath outputPath)
|
||||
{
|
||||
return outputPath.Directory / "_animated" / outputPath.FilenameWithoutExtension;
|
||||
}
|
||||
|
||||
private void DeleteIfExists(ResPath path)
|
||||
{
|
||||
if (_resourceManager.UserData.Exists(path))
|
||||
_resourceManager.UserData.Delete(path);
|
||||
}
|
||||
|
||||
private static List<AnimatedLayerInfo> GetAnimatedLayers(SpriteComponent spriteComp)
|
||||
{
|
||||
var result = new List<AnimatedLayerInfo>();
|
||||
var index = 0;
|
||||
|
||||
foreach (var spriteLayer in spriteComp.AllLayers)
|
||||
{
|
||||
if (!spriteLayer.Visible ||
|
||||
!spriteLayer.AutoAnimated ||
|
||||
spriteLayer.ActualRsi == null ||
|
||||
!spriteLayer.ActualRsi.TryGetState(spriteLayer.RsiState, out var state) ||
|
||||
!state.IsAnimated ||
|
||||
state.TotalDelay <= 0f)
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new AnimatedLayerInfo(index, state.TotalDelay, state.GetDelays()));
|
||||
index++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<AnimationFrameInfo> BuildAnimationFrames(
|
||||
EntityUid entity,
|
||||
SpriteComponent spriteComp,
|
||||
IReadOnlyList<AnimatedLayerInfo> animationLayers)
|
||||
{
|
||||
const float epsilon = 0.0001f;
|
||||
const int maxFrames = 512;
|
||||
|
||||
var initialSignature = BuildAnimationSignatureAt(entity, spriteComp, animationLayers, 0f, epsilon);
|
||||
var frames = new List<AnimationFrameInfo>();
|
||||
var currentFrameStart = 0f;
|
||||
|
||||
for (var i = 0; i < maxFrames; i++)
|
||||
{
|
||||
var nextDelta = GetNextBoundaryDelta(currentFrameStart, animationLayers, epsilon);
|
||||
if (nextDelta <= epsilon)
|
||||
break;
|
||||
|
||||
var renderTime = currentFrameStart + MathF.Min(epsilon, nextDelta * 0.5f);
|
||||
frames.Add(new AnimationFrameInfo($"{i:D4}.png", renderTime, ToDelayMilliseconds(nextDelta)));
|
||||
|
||||
var nextFrameStart = currentFrameStart + nextDelta;
|
||||
if (BuildAnimationSignatureAt(entity, spriteComp, animationLayers, nextFrameStart, epsilon) == initialSignature)
|
||||
break;
|
||||
|
||||
currentFrameStart = nextFrameStart;
|
||||
}
|
||||
|
||||
ApplyAnimationTime(entity, spriteComp, animationLayers, 0f);
|
||||
return frames;
|
||||
}
|
||||
|
||||
private void ApplyAnimationTime(
|
||||
EntityUid entity,
|
||||
SpriteComponent spriteComp,
|
||||
IReadOnlyList<AnimatedLayerInfo> animationLayers,
|
||||
float timeSeconds)
|
||||
{
|
||||
var spriteSystem = _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
|
||||
foreach (var layer in animationLayers)
|
||||
{
|
||||
spriteSystem.LayerSetAnimationTime((entity, spriteComp), layer.Index, timeSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildAnimationSignature(
|
||||
SpriteComponent spriteComp,
|
||||
IReadOnlyList<AnimatedLayerInfo> animationLayers)
|
||||
{
|
||||
var parts = new string[animationLayers.Count];
|
||||
|
||||
for (var i = 0; i < animationLayers.Count; i++)
|
||||
{
|
||||
var layer = spriteComp[animationLayers[i].Index];
|
||||
parts[i] = $"{animationLayers[i].Index}:{layer.Visible}:{layer.RsiState.Name}:{layer.AnimationFrame}";
|
||||
}
|
||||
|
||||
return string.Join("|", parts);
|
||||
}
|
||||
|
||||
private string BuildAnimationSignatureAt(
|
||||
EntityUid entity,
|
||||
SpriteComponent spriteComp,
|
||||
IReadOnlyList<AnimatedLayerInfo> animationLayers,
|
||||
float frameStartTime,
|
||||
float epsilon)
|
||||
{
|
||||
var nextDelta = GetNextBoundaryDelta(frameStartTime, animationLayers, epsilon);
|
||||
var probeTime = nextDelta > epsilon
|
||||
? frameStartTime + MathF.Min(epsilon, nextDelta * 0.5f)
|
||||
: frameStartTime;
|
||||
|
||||
ApplyAnimationTime(entity, spriteComp, animationLayers, probeTime);
|
||||
return BuildAnimationSignature(spriteComp, animationLayers);
|
||||
}
|
||||
|
||||
private static float GetNextBoundaryDelta(
|
||||
float currentTime,
|
||||
IReadOnlyList<AnimatedLayerInfo> animationLayers,
|
||||
float epsilon)
|
||||
{
|
||||
var nextDelta = float.MaxValue;
|
||||
var foundDelta = false;
|
||||
|
||||
foreach (var layer in animationLayers)
|
||||
{
|
||||
if (layer.TotalDelay <= epsilon)
|
||||
continue;
|
||||
|
||||
var mod = currentTime % layer.TotalDelay;
|
||||
var cumulative = 0f;
|
||||
float? layerDelta = null;
|
||||
|
||||
foreach (var delay in layer.Delays)
|
||||
{
|
||||
cumulative += delay;
|
||||
if (cumulative > mod + epsilon)
|
||||
{
|
||||
layerDelta = cumulative - mod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
layerDelta ??= layer.TotalDelay - mod;
|
||||
|
||||
if (layerDelta.Value > epsilon && layerDelta.Value < nextDelta)
|
||||
{
|
||||
nextDelta = layerDelta.Value;
|
||||
foundDelta = true;
|
||||
}
|
||||
}
|
||||
|
||||
return foundDelta ? nextDelta : 0f;
|
||||
}
|
||||
|
||||
private static int ToDelayMilliseconds(float seconds)
|
||||
{
|
||||
return Math.Max(1, (int)MathF.Round(seconds * 1000f));
|
||||
}
|
||||
|
||||
private void WriteAnimationMetadata(ResPath animationDir, IReadOnlyList<AnimationFrameInfo> animationFrames)
|
||||
{
|
||||
using var writer = _resourceManager.UserData.OpenWriteText(animationDir / "frames.txt");
|
||||
foreach (var frame in animationFrames)
|
||||
{
|
||||
writer.WriteLine($"{frame.FileName}\t{frame.DelayMilliseconds}");
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
private readonly record struct AnimatedLayerInfo(int Index, float TotalDelay, float[] Delays);
|
||||
private readonly record struct AnimationFrameInfo(string FileName, float RenderTimeSeconds, int DelayMilliseconds);
|
||||
private readonly record struct SpriteRenderBounds(Vector2i Min, Vector2i Size);
|
||||
|
||||
private bool TryExportFrameDirect(
|
||||
EntityUid entity,
|
||||
Direction direction,
|
||||
ResPath outputPath,
|
||||
SpriteRenderBounds renderBounds)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent<SpriteComponent>(entity, out var spriteComp))
|
||||
return false;
|
||||
|
||||
// Keep the old render-target path for uncommon transformed sprites.
|
||||
if (spriteComp.Scale != Vector2.One || spriteComp.Rotation != Angle.Zero)
|
||||
return false;
|
||||
|
||||
var size = renderBounds.Size;
|
||||
if (size == Vector2i.Zero)
|
||||
return true;
|
||||
|
||||
using var image = new Image<Rgba32>(size.X, size.Y);
|
||||
var buffer = image.GetPixelSpan();
|
||||
|
||||
foreach (var baseLayer in spriteComp.AllLayers)
|
||||
{
|
||||
if (baseLayer is not SpriteComponent.Layer spriteLayer || !spriteLayer.Visible)
|
||||
continue;
|
||||
|
||||
if (spriteLayer.Scale != Vector2.One || spriteLayer.Rotation != Angle.Zero)
|
||||
return false;
|
||||
|
||||
if (!TryGetLayerImage(spriteLayer, direction, out var sourceImage, out var sourceRect))
|
||||
continue;
|
||||
|
||||
var drawColor = spriteComp.Color * spriteLayer.Color;
|
||||
var drawOffset = ToPixelOffset(spriteComp.Offset + spriteLayer.Offset) - renderBounds.Min;
|
||||
var topLeft = drawOffset - new Vector2i(sourceRect.Width, sourceRect.Height) / 2;
|
||||
BlitImage(sourceImage, sourceRect, drawColor, buffer, size, topLeft);
|
||||
}
|
||||
|
||||
if (!_resourceManager.UserData.IsDir(outputPath.Directory))
|
||||
_resourceManager.UserData.CreateDir(outputPath.Directory);
|
||||
|
||||
if (_resourceManager.UserData.Exists(outputPath))
|
||||
_resourceManager.UserData.Delete(outputPath);
|
||||
|
||||
using var file = _resourceManager.UserData.OpenWrite(outputPath);
|
||||
image.SaveAsPng(file);
|
||||
_sawmill.Info($"Saved screenshot to {outputPath} (direct)");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void BlitImage(
|
||||
Image<Rgba32> sourceImage,
|
||||
Rectangle sourceRect,
|
||||
Color modulation,
|
||||
Span<Rgba32> destination,
|
||||
Vector2i destinationSize,
|
||||
Vector2i topLeft)
|
||||
{
|
||||
var source = sourceImage.GetPixelSpan();
|
||||
var sourceWidth = sourceImage.Width;
|
||||
|
||||
for (var y = 0; y < sourceRect.Height; y++)
|
||||
{
|
||||
var dstY = topLeft.Y + y;
|
||||
if (dstY < 0 || dstY >= destinationSize.Y)
|
||||
continue;
|
||||
|
||||
var srcY = sourceRect.Top + y;
|
||||
for (var x = 0; x < sourceRect.Width; x++)
|
||||
{
|
||||
var dstX = topLeft.X + x;
|
||||
if (dstX < 0 || dstX >= destinationSize.X)
|
||||
continue;
|
||||
|
||||
var srcX = sourceRect.Left + x;
|
||||
var texel = source[srcY * sourceWidth + srcX];
|
||||
var src = Modulate(texel, modulation);
|
||||
if (src.A == 0)
|
||||
continue;
|
||||
|
||||
ref var dst = ref destination[dstY * destinationSize.X + dstX];
|
||||
BlendPixel(ref dst, src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Rgba32 Modulate(Rgba32 texel, Color modulation)
|
||||
{
|
||||
return new Rgba32(
|
||||
(byte) (texel.R * modulation.RByte / byte.MaxValue),
|
||||
(byte) (texel.G * modulation.GByte / byte.MaxValue),
|
||||
(byte) (texel.B * modulation.BByte / byte.MaxValue),
|
||||
(byte) (texel.A * modulation.AByte / byte.MaxValue));
|
||||
}
|
||||
|
||||
private static void BlendPixel(ref Rgba32 destination, Rgba32 source)
|
||||
{
|
||||
if (source.A == byte.MaxValue)
|
||||
{
|
||||
destination = source;
|
||||
return;
|
||||
}
|
||||
|
||||
var srcAlpha = source.A / 255f;
|
||||
var dstAlpha = destination.A / 255f;
|
||||
var outAlpha = srcAlpha + dstAlpha * (1f - srcAlpha);
|
||||
|
||||
if (outAlpha <= 0f)
|
||||
{
|
||||
destination = default;
|
||||
return;
|
||||
}
|
||||
|
||||
var srcR = source.R / 255f;
|
||||
var srcG = source.G / 255f;
|
||||
var srcB = source.B / 255f;
|
||||
var dstR = destination.R / 255f;
|
||||
var dstG = destination.G / 255f;
|
||||
var dstB = destination.B / 255f;
|
||||
|
||||
var outR = (srcR * srcAlpha + dstR * dstAlpha * (1f - srcAlpha)) / outAlpha;
|
||||
var outG = (srcG * srcAlpha + dstG * dstAlpha * (1f - srcAlpha)) / outAlpha;
|
||||
var outB = (srcB * srcAlpha + dstB * dstAlpha * (1f - srcAlpha)) / outAlpha;
|
||||
|
||||
destination = new Rgba32(
|
||||
(byte) Math.Clamp((int) MathF.Round(outR * 255f), 0, 255),
|
||||
(byte) Math.Clamp((int) MathF.Round(outG * 255f), 0, 255),
|
||||
(byte) Math.Clamp((int) MathF.Round(outB * 255f), 0, 255),
|
||||
(byte) Math.Clamp((int) MathF.Round(outAlpha * 255f), 0, 255));
|
||||
}
|
||||
|
||||
private static Vector2i ToPixelOffset(Vector2 offset)
|
||||
{
|
||||
return new Vector2i(
|
||||
(int) MathF.Round(offset.X * EyeManager.PixelsPerMeter),
|
||||
(int) MathF.Round(offset.Y * EyeManager.PixelsPerMeter));
|
||||
}
|
||||
|
||||
private bool TryGetLayerImage(
|
||||
SpriteComponent.Layer layer,
|
||||
Direction direction,
|
||||
out Image<Rgba32> image,
|
||||
out Rectangle sourceRect)
|
||||
{
|
||||
image = default!;
|
||||
sourceRect = default;
|
||||
|
||||
// Raw texture layers need a separate cache path. Use render target fallback for them.
|
||||
if (layer.Texture != null)
|
||||
return false;
|
||||
|
||||
var rsi = layer.ActualRsi;
|
||||
var stateId = ((ISpriteLayer) layer).RsiState;
|
||||
if (rsi == null ||
|
||||
!stateId.IsValid ||
|
||||
!rsi.TryGetState(stateId, out var state))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rsiPath = rsi.Path;
|
||||
var stateName = stateId.Name!;
|
||||
|
||||
if (!_rsiStateImageCache.TryGetValue((rsiPath, stateName), out image!))
|
||||
{
|
||||
using var stream = _resourceManager.ContentFileRead(rsiPath / (stateName + ".png"));
|
||||
image = Image.Load<Rgba32>(stream);
|
||||
_rsiStateImageCache[(rsiPath, stateName)] = image;
|
||||
}
|
||||
|
||||
var frameWidth = rsi.Size.X;
|
||||
var frameHeight = rsi.Size.Y;
|
||||
var statesX = image.Width / frameWidth;
|
||||
var statesY = image.Height / frameHeight;
|
||||
var totalFrames = statesX * statesY;
|
||||
var dirCount = state.RsiDirections switch
|
||||
{
|
||||
Robust.Shared.Graphics.RSI.RsiDirectionType.Dir1 => 1,
|
||||
Robust.Shared.Graphics.RSI.RsiDirectionType.Dir4 => 4,
|
||||
Robust.Shared.Graphics.RSI.RsiDirectionType.Dir8 => 8,
|
||||
_ => 1
|
||||
};
|
||||
|
||||
if (totalFrames == 0 || totalFrames % dirCount != 0)
|
||||
return false;
|
||||
|
||||
var framesPerDirection = totalFrames / dirCount;
|
||||
var frame = Math.Clamp(layer.AnimationFrame, 0, framesPerDirection - 1);
|
||||
var rsiDirection = direction.Convert(state.RsiDirections).OffsetRsiDir(layer.DirOffset);
|
||||
var target = (int) rsiDirection * framesPerDirection + frame;
|
||||
var targetY = target / statesX;
|
||||
var targetX = target % statesX;
|
||||
sourceRect = new Rectangle(targetX * frameWidth, targetY * frameHeight, frameWidth, frameHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed class EntityScreenshotRenderControl : Control
|
||||
{
|
||||
private static readonly Color ExportBackgroundColor = new(128, 128, 128, 0);
|
||||
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
|
||||
internal readonly Queue<(
|
||||
IRenderTexture Texture,
|
||||
Direction Direction,
|
||||
EntityUid Entity,
|
||||
ResPath OutputPath,
|
||||
TaskCompletionSource Tcs)> QueuedTextures = new();
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public EntityScreenshotRenderControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = _logManager.GetSawmill("corvax.entity-sprite-export");
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
while (QueuedTextures.TryDequeue(out var queued))
|
||||
{
|
||||
if (queued.Tcs.Task.IsCanceled)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
if (!_entityManager.EntityExists(queued.Entity))
|
||||
{
|
||||
queued.Texture.Dispose();
|
||||
queued.Tcs.SetResult();
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = queued;
|
||||
handle.RenderInRenderTarget(queued.Texture,
|
||||
() =>
|
||||
{
|
||||
handle.DrawEntity(result.Entity,
|
||||
result.Texture.Size / 2,
|
||||
Vector2.One,
|
||||
Angle.Zero,
|
||||
overrideDirection: result.Direction);
|
||||
},
|
||||
ExportBackgroundColor);
|
||||
|
||||
if (!_resourceManager.UserData.IsDir(queued.OutputPath.Directory))
|
||||
_resourceManager.UserData.CreateDir(queued.OutputPath.Directory);
|
||||
|
||||
var result1 = queued;
|
||||
queued.Texture.CopyPixelsToMemory<Rgba32>(image =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_resourceManager.UserData.Exists(result.OutputPath))
|
||||
{
|
||||
_sawmill.Info($"Found existing file {result.OutputPath} to replace.");
|
||||
_resourceManager.UserData.Delete(result.OutputPath);
|
||||
}
|
||||
|
||||
using var file = _resourceManager.UserData.OpenWrite(result.OutputPath);
|
||||
image.SaveAsPng(file);
|
||||
_sawmill.Info($"Saved screenshot to {result.OutputPath}");
|
||||
result1.Tcs.SetResult();
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(exc.StackTrace))
|
||||
_sawmill.Fatal(exc.StackTrace);
|
||||
|
||||
result1.Tcs.SetException(exc);
|
||||
}
|
||||
finally
|
||||
{
|
||||
image.Dispose();
|
||||
result1.Texture.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
queued.Texture.Dispose();
|
||||
|
||||
if (!string.IsNullOrEmpty(exc.StackTrace))
|
||||
_sawmill.Fatal(exc.StackTrace);
|
||||
|
||||
queued.Tcs.SetException(exc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Corvax.Ipc;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Corvax.Ipc;
|
||||
public sealed class IpcSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly SharedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
private static readonly TimeSpan AlertUpdateDelay = TimeSpan.FromSeconds(0.5f);
|
||||
private TimeSpan _nextAlertUpdate = TimeSpan.Zero;
|
||||
private EntityQuery<IpcComponent> _ipcQuery;
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ipcQuery = GetEntityQuery<IpcComponent>();
|
||||
SubscribeLocalEvent<IpcComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<IpcComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
SubscribeLocalEvent<IpcComponent, PowerCellChangedEvent>(OnPowerCellChanged);
|
||||
SubscribeLocalEvent<IpcComponent, PowerCellSlotEmptyEvent>(OnPowerCellEmpty);
|
||||
}
|
||||
private void OnPowerCellChanged(EntityUid uid, IpcComponent component, ref PowerCellChangedEvent args)
|
||||
{
|
||||
if (_player.LocalEntity != uid)
|
||||
return;
|
||||
UpdateBatteryAlert((uid, component));
|
||||
}
|
||||
private void OnPowerCellEmpty(EntityUid uid, IpcComponent component, ref PowerCellSlotEmptyEvent args)
|
||||
{
|
||||
if (_player.LocalEntity != uid)
|
||||
return;
|
||||
UpdateBatteryAlert((uid, component));
|
||||
}
|
||||
private void OnPlayerAttached(Entity<IpcComponent> ent, ref LocalPlayerAttachedEvent args)
|
||||
{
|
||||
UpdateBatteryAlert(ent);
|
||||
}
|
||||
private void OnPlayerDetached(Entity<IpcComponent> ent, ref LocalPlayerDetachedEvent args)
|
||||
{
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
|
||||
}
|
||||
private void UpdateBatteryAlert(Entity<IpcComponent> ent)
|
||||
{
|
||||
if (!_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery)
|
||||
|| battery.Value.Comp.MaxCharge <= 0
|
||||
|| _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge < 0.01f)
|
||||
{
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
|
||||
_alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
|
||||
return;
|
||||
}
|
||||
var chargePercent = (short)MathF.Round(
|
||||
_battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge * 10f);
|
||||
if (chargePercent == 0 && _powerCell.HasDrawCharge(ent.Owner))
|
||||
chargePercent = 1;
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
|
||||
_alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
|
||||
}
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
if (_player.LocalEntity is not { } localPlayer)
|
||||
return;
|
||||
var curTime = _timing.CurTime;
|
||||
if (curTime < _nextAlertUpdate)
|
||||
return;
|
||||
_nextAlertUpdate = curTime + AlertUpdateDelay;
|
||||
if (!_ipcQuery.TryComp(localPlayer, out var ipc))
|
||||
return;
|
||||
UpdateBatteryAlert((localPlayer, ipc));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Content.Client.Corvax.TTS;
|
||||
using Content.Shared.Corvax.CCCVars;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Lobby.UI;
|
||||
|
||||
public sealed partial class HumanoidProfileEditor
|
||||
{
|
||||
private TTSTab? _ttsTab;
|
||||
|
||||
private void RefreshVoiceTab()
|
||||
{
|
||||
if (!_cfgManager.GetCVar(CCCVars.TTSEnabled))
|
||||
return;
|
||||
|
||||
_ttsTab = new TTSTab();
|
||||
var children = new List<Control>();
|
||||
foreach (var child in TabContainer.Children)
|
||||
children.Add(child);
|
||||
|
||||
TabContainer.RemoveAllChildren();
|
||||
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
{
|
||||
if (i == 1) // Set the tab to the 2nd place.
|
||||
{
|
||||
TabContainer.AddChild(_ttsTab);
|
||||
}
|
||||
TabContainer.AddChild(children[i]);
|
||||
}
|
||||
|
||||
TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-voice-tab"));
|
||||
|
||||
_ttsTab.OnVoiceSelected += voiceId =>
|
||||
{
|
||||
SetVoice(voiceId);
|
||||
_ttsTab.SetSelectedVoice(voiceId);
|
||||
};
|
||||
|
||||
_ttsTab.OnPreviewRequested += voiceId =>
|
||||
{
|
||||
_entManager.System<TTSSystem>().RequestPreviewTTS(voiceId);
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateTTSVoicesControls()
|
||||
{
|
||||
if (Profile is null || _ttsTab is null)
|
||||
return;
|
||||
|
||||
_ttsTab.UpdateControls(Profile, Profile.Sex);
|
||||
_ttsTab.SetSelectedVoice(Profile.Voice);
|
||||
}
|
||||
private void SetVoice(string newVoice)
|
||||
{
|
||||
Profile = Profile?.WithVoice(newVoice);
|
||||
IsDirty = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center"
|
||||
SeparationOverride="10">
|
||||
|
||||
<BoxContainer Name="CrewContainer" Orientation="Vertical" HorizontalExpand="True">
|
||||
<Label Name="MemberNameLabel" HorizontalExpand="True" />
|
||||
<BoxContainer Name="JobContaiber" Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Label Name="JobTitleLabel" Margin="0 0 5 0" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<OptionButton Name="SquadDropdown" StyleClasses="SecApartmentOptionButton"
|
||||
MinWidth="50"
|
||||
Margin="0 0 8 0" />
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</PanelContainer>
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Corvax.SecApartment.Stylesheets;
|
||||
using Content.Shared.SecApartment;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Corvax.SecApartment;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CrewMemberEntry : PanelContainer
|
||||
{
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public CrewMemberInfo CrewMember { get; }
|
||||
public List<Squad> Squads { get; }
|
||||
|
||||
private readonly Dictionary<int, string> _squadIdMap = new();
|
||||
|
||||
public Action<string>? OnAssignPressed;
|
||||
|
||||
public CrewMemberEntry(CrewMemberInfo crewMember, List<Squad> squads, SpriteSpecifier? jobIcon, SpriteSystem sprite)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
CrewMember = crewMember;
|
||||
Squads = squads;
|
||||
|
||||
_sprite = sprite;
|
||||
|
||||
SetupStyles();
|
||||
SetupUI(jobIcon);
|
||||
}
|
||||
|
||||
private void SetupUI(SpriteSpecifier? jobIcon)
|
||||
{
|
||||
MemberNameLabel.Text = CrewMember.Name;
|
||||
JobTitleLabel.Text = CrewMember.JobTitle;
|
||||
|
||||
if (jobIcon != null)
|
||||
{
|
||||
var icon = new TextureRect()
|
||||
{
|
||||
TextureScale = new Vector2(2, 2),
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
Texture = _sprite.Frame0(jobIcon),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
};
|
||||
|
||||
JobContaiber.AddChild(icon);
|
||||
}
|
||||
|
||||
UpdateSquadDropdown();
|
||||
|
||||
SquadDropdown.OnItemSelected += args =>
|
||||
{
|
||||
SquadDropdown.SelectId(args.Id);
|
||||
if (SquadDropdown.SelectedId >= 0 && _squadIdMap.TryGetValue(SquadDropdown.SelectedId, out var squadId))
|
||||
OnAssignPressed?.Invoke(squadId);
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupStyles()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#441111"),
|
||||
BorderColor = SecApartmentStyles.TabActiveColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
ContentMarginBottomOverride = 4,
|
||||
ContentMarginLeftOverride = 8,
|
||||
ContentMarginRightOverride = 8,
|
||||
ContentMarginTopOverride = 4
|
||||
};
|
||||
|
||||
SquadDropdown.AddStyleClass(SecApartmentStyles.StyleClassOptionButton);
|
||||
SquadDropdown.OptionStyleClasses.Add(SecApartmentStyles.StyleClassButtonRed);
|
||||
|
||||
MemberNameLabel.FontColorOverride = SecApartmentStyles.HeadingColor;
|
||||
JobTitleLabel.FontColorOverride = SecApartmentStyles.SubTextColor;
|
||||
}
|
||||
|
||||
private void UpdateSquadDropdown()
|
||||
{
|
||||
SquadDropdown.Clear();
|
||||
_squadIdMap.Clear();
|
||||
|
||||
if (!Squads.Any())
|
||||
{
|
||||
SquadDropdown.AddItem(Loc.GetString("sec-apartment-assign-no-squads"));
|
||||
SquadDropdown.Disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
SquadDropdown.Disabled = false;
|
||||
SquadDropdown.AddItem(Loc.GetString("sec-apartment-assign-select-squad"));
|
||||
|
||||
foreach (var squad in Squads)
|
||||
{
|
||||
var index = SquadDropdown.ItemCount;
|
||||
SquadDropdown.AddItem($"{squad.Name} ({squad.Members.Count})");
|
||||
_squadIdMap[index] = squad.SquadId;
|
||||
}
|
||||
|
||||
SquadDropdown.Select(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Content.Shared.Overlays;
|
||||
using Content.Shared.SecApartment;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
public sealed class ShowSquadIconsSystem : EquipmentHudSystem<ShowSquadIconsComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SquadMemberComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
|
||||
}
|
||||
|
||||
private void OnGetStatusIconsEvent(EntityUid uid, SquadMemberComponent component, ref GetStatusIconsEvent ev)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
if (_prototype.Resolve(component.StatusIcon, out var iconPrototype))
|
||||
ev.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Content.Shared.SecApartment;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Corvax.SecApartment;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class SecApartmentBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private SecApartmentWindow? _window;
|
||||
|
||||
public SecApartmentBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<SecApartmentWindow>();
|
||||
|
||||
_window.OnCreateSquad += squadName =>
|
||||
SendMessage(new CreateSquadMessage(squadName));
|
||||
|
||||
_window.OnChangeSquadIcon += (squadId, iconId) =>
|
||||
SendMessage(new ChangeSquadIconMessage(squadId, iconId));
|
||||
|
||||
_window.OnRenameSquad += (squadId, newName) =>
|
||||
SendMessage(new RenameSquadMessage(squadId, newName));
|
||||
|
||||
_window.OnDeleteSquad += squadId =>
|
||||
SendMessage(new DeleteSquadMessage(squadId));
|
||||
|
||||
_window.OnUpdateSquadDescription += (squadId, description) =>
|
||||
SendMessage(new UpdateSquadDescriptionMessage(squadId, description));
|
||||
|
||||
_window.OnChangeSquadStatus += (squadId, status) =>
|
||||
SendMessage(new ChangeSquadStatusMessage(squadId, status));
|
||||
|
||||
_window.OnAddMemberToSquad += (squadId, memberId) =>
|
||||
SendMessage(new AddMemberToSquadMessage(squadId, memberId));
|
||||
|
||||
_window.OnRemoveMemberFromSquad += (squadId, memberId) =>
|
||||
SendMessage(new RemoveMemberFromSquadMessage(squadId, memberId));
|
||||
|
||||
_window.OnRemoveTimer += timerUid =>
|
||||
SendMessage(new RemoveTimerMessage(timerUid));
|
||||
|
||||
_window.OnClose += Close;
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case SecApartmentUpdateState updateState:
|
||||
_window.UpdateState(updateState);
|
||||
break;
|
||||
|
||||
case SensorStatusUpdateState sensorState:
|
||||
_window.UpdateSensorStatuses(sensorState);
|
||||
break;
|
||||
|
||||
case TimerUpdateState timerState:
|
||||
_window.UpdateTimerState(timerState);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<ui:SecApartmentWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.Corvax.SecApartment"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
SetSize="1000 650" MouseFilter="Stop">
|
||||
|
||||
<PanelContainer Name="Background" StyleClasses="PdaBackgroundRect" ModulateSelfOverride="#121212"/>
|
||||
<PanelContainer Name="Border" StyleClasses="PdaBorderRect" Modulate="#303030"/>
|
||||
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
|
||||
<BoxContainer SetHeight="40" Margin="20 10 10 5" Orientation="Horizontal" HorizontalExpand="True">
|
||||
<TextureRect TexturePath="/Textures/Interface/Nano/ntlogo.svg.png" SetSize="32 32" Margin="0 0 10 0"/>
|
||||
<Label Text="{Loc 'sec-apartment-ui-sec-apartment'}" StyleClasses="ConsoleHeadingBig"
|
||||
HorizontalExpand="True"/>
|
||||
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"
|
||||
Modulate="#ff4444" VerticalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
|
||||
<Control Margin="15 0" RectClipContent="True" VerticalExpand="true"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer Name="ContentBorder" StyleClasses="PdaBackground" Modulate="#440000"/>
|
||||
<Control Name="ContentsContainer" Margin="3 3" Modulate="#FFFFFF">
|
||||
|
||||
<PanelContainer Name="ContentBackground" StyleClasses="PdaContentBackground" Modulate="#BB2222"/>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 5 10 0">
|
||||
<Label Text="{Loc 'sec-apartment-ui-station'}" StyleClasses="ConsoleSubHeading" FontColorOverride="#ff8888"/>
|
||||
<Label Name="StationLabel" StyleClasses="ConsoleHeading" FontColorOverride="#ff4444"
|
||||
HorizontalExpand="True" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" HorizontalExpand="True" Margin="10 5 10 5" SetHeight="2">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#ff4444" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
|
||||
<TabContainer Name="Tabs" Margin="10 0 10 0" VerticalExpand="True" HorizontalExpand="True"
|
||||
TabFontColorOverride="#ff4444" TabFontColorInactiveOverride="#ff8888">
|
||||
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True" Margin="10">
|
||||
|
||||
<BoxContainer Orientation="Vertical" MinWidth="400" Margin="0 0 15 0">
|
||||
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<Label Text="{Loc 'sec-apartment-ui-no-squads-crew'}" StyleClasses="ConsoleSubHeading" FontColorOverride="#ff8888"/>
|
||||
<Label Name="UnassignedCountLabel" StyleClasses="ConsoleText"
|
||||
FontColorOverride="#ff4444" HorizontalExpand="True" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="AngleRect" VerticalExpand="True" HorizontalExpand="True" Modulate="#FFFFFF">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#330000" BorderColor="#ff4444" BorderThickness="2" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<BoxContainer Name="UnassignedContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5"/>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||
|
||||
<PanelContainer StyleClasses="AngleRect" Margin="0 0 0 10">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#552222" BorderColor="#ff4444" BorderThickness="2" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" Margin="8">
|
||||
<LineEdit Name="NewSquadName"
|
||||
PlaceHolder="{Loc 'sec-apartment-ui-squads-placeholder'}"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="ConsoleLineEdit"
|
||||
Margin="0 0 10 0"/>
|
||||
<Button Name="CreateSquadButton" Text="{Loc 'sec-apartment-ui-squads-create'}"
|
||||
MinWidth="50" StyleClasses="ButtonRed"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<Label Text="{Loc 'sec-apartment-ui-squads'}" StyleClasses="ConsoleSubHeading" FontColorOverride="#ff8888"/>
|
||||
<Label Name="SquadsCountLabel" StyleClasses="ConsoleText"
|
||||
FontColorOverride="#ff4444" HorizontalExpand="True" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="AngleRect" VerticalExpand="True" HorizontalExpand="True" Modulate="#FFFFFF">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#330000" BorderColor="#ff4444" BorderThickness="2" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Name="SquadsContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5"/>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="10">
|
||||
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<Label Text="{Loc 'sec-apartment-ui-timers'}" StyleClasses="ConsoleSubHeading" FontColorOverride="#ff8888"/>
|
||||
<Label Name="TimersCountLabel" StyleClasses="ConsoleText"
|
||||
FontColorOverride="#ff4444" HorizontalExpand="True" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="AngleRect" VerticalExpand="True" HorizontalExpand="True" Modulate="#FFFFFF">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#330000" BorderColor="#ff4444" BorderThickness="2" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Name="TimersContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5"/>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 5 10 0">
|
||||
<Label Text="{Loc 'sec-apartment-ui-timers-info'}"
|
||||
StyleClasses="LabelSubText"
|
||||
FontColorOverride="#ff8888"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</TabContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="False" Margin="0 5 0 0">
|
||||
<controls:StripeBack HasBottomEdge="False" HasMargins="False" HorizontalExpand="True">
|
||||
<Label Text="{Loc 'sec-apartment-ui-os'}" HorizontalAlignment="Left" Margin="5 0" StyleClasses="LabelSubText"/>
|
||||
</controls:StripeBack>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
</Control>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" SetHeight="30" Margin="0 5 0 0">
|
||||
<Label Name="Footer" Text="{Loc 'sec-apartment-ui-footer'}"
|
||||
StyleClasses="ConsoleSubText" FontColorOverride="#ff8888"
|
||||
HorizontalExpand="True" HorizontalAlignment="Right" Margin="10 0"/>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
</ui:SecApartmentWindow>
|
||||
@@ -0,0 +1,315 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Corvax.SecApartment.Stylesheets;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Shared.SecApartment;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Corvax.SecApartment;
|
||||
|
||||
[Virtual]
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SecApartmentWindow : BaseWindow
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
[Dependency] private readonly IResourceCache _resCache = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
private readonly ClientGameTicker _gameTicker;
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
private readonly SecApartmentStyles _styles;
|
||||
private string _station = Loc.GetString("sec-apartment-unknown");
|
||||
private Dictionary<string, SquadEntry> _squadEntries = new();
|
||||
private Dictionary<string, CrewMemberInfo> _lastCrewData = new();
|
||||
private Dictionary<NetEntity, TimerEntryControl> _timerControls = new();
|
||||
|
||||
public Action<string>? OnCreateSquad;
|
||||
public Action<string, string>? OnRenameSquad;
|
||||
public Action<string>? OnDeleteSquad;
|
||||
public Action<string, string>? OnUpdateSquadDescription;
|
||||
public Action<string, SquadStatus>? OnChangeSquadStatus;
|
||||
public Action<string, string>? OnAddMemberToSquad;
|
||||
public Action<string, string>? OnRemoveMemberFromSquad;
|
||||
public Action<string, SquadIconNum>? OnChangeSquadIcon;
|
||||
public Action<NetEntity>? OnRemoveTimer;
|
||||
|
||||
private Stylesheet? _previousGlobalStylesheet;
|
||||
|
||||
public SecApartmentWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entitySystem.GetEntitySystem<SpriteSystem>();
|
||||
_gameTicker = _entitySystem.GetEntitySystem<ClientGameTicker>();
|
||||
_styles = new SecApartmentStyles(_resCache);
|
||||
|
||||
SetupAllStyles();
|
||||
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
|
||||
NewSquadName.OnTextChanged += OnSquadNameTextChanged;
|
||||
|
||||
CreateSquadButton.Disabled = string.IsNullOrWhiteSpace(NewSquadName.Text);
|
||||
CreateSquadButton.OnPressed += _ =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(NewSquadName.Text))
|
||||
{
|
||||
var squadName = FormattedMessage.RemoveMarkupOrThrow(NewSquadName.Text);
|
||||
if (squadName.Length > 16) squadName = squadName[..16];
|
||||
|
||||
OnCreateSquad?.Invoke(squadName);
|
||||
NewSquadName.Text = string.Empty;
|
||||
CreateSquadButton.Disabled = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override DragMode GetDragModeFor(Vector2 relativeMousePos) => DragMode.Move;
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
var stationTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
|
||||
StationLabel.Text = Loc.GetString("sec-apartment-ui-mark", ("time", stationTime.ToString("hh\\:mm\\:ss")), ("station", _station));
|
||||
}
|
||||
|
||||
private void SetupAllStyles()
|
||||
{
|
||||
SetupBasicStyles();
|
||||
SetupTabContainer();
|
||||
}
|
||||
|
||||
private void SetupBasicStyles()
|
||||
{
|
||||
StationLabel.FontColorOverride = SecApartmentStyles.HeadingColor;
|
||||
StationLabel.FontOverride = _styles.GetBoldFont(16);
|
||||
|
||||
UnassignedCountLabel.FontColorOverride = SecApartmentStyles.TextColor;
|
||||
UnassignedCountLabel.FontOverride = _styles.GetRegularFont(13);
|
||||
|
||||
SquadsCountLabel.FontColorOverride = SecApartmentStyles.TextColor;
|
||||
SquadsCountLabel.FontOverride = _styles.GetRegularFont(13);
|
||||
|
||||
Footer.FontColorOverride = SecApartmentStyles.SubTextColor;
|
||||
Footer.FontOverride = _styles.GetRegularFont(11);
|
||||
|
||||
StationLabel.AddStyleClass(SecApartmentStyles.StyleClassConsoleHeading);
|
||||
NewSquadName.AddStyleClass(SecApartmentStyles.StyleClassConsoleLineEdit);
|
||||
CreateSquadButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
|
||||
var buttonStyleRule = SecApartmentStyles.CreateButtonRedRule(
|
||||
_styles.GetButtonRedStyle(),
|
||||
_styles.GetBoldFont(11),
|
||||
SecApartmentStyles.TabActiveColor,
|
||||
SecApartmentStyles.TabInactiveColor
|
||||
);
|
||||
|
||||
var lineEditRule = SecApartmentStyles.CreateLineEditRule(
|
||||
_styles.GetLineEditStyle(),
|
||||
_styles.GetRegularFont(11),
|
||||
SecApartmentStyles.TextColor,
|
||||
SecApartmentStyles.PlaceholderColor
|
||||
);
|
||||
|
||||
var optionRule = SecApartmentStyles.CreateOptionButtonRule(
|
||||
_styles.GetOptionButtonStyle(),
|
||||
_styles.GetRegularFont(11),
|
||||
SecApartmentStyles.TextColor,
|
||||
SecApartmentStyles.TabInactiveColor
|
||||
);
|
||||
|
||||
var optionBackgroundRule = SecApartmentStyles.CreateOptionButtonBackgroundRule();
|
||||
|
||||
var rules = new[] { buttonStyleRule, lineEditRule, optionRule, optionBackgroundRule };
|
||||
var stylesheet = CreateCombinedStylesheet(rules);
|
||||
|
||||
_previousGlobalStylesheet = _ui.Stylesheet;
|
||||
UserInterfaceManager.Stylesheet = stylesheet;
|
||||
}
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing && _previousGlobalStylesheet != null)
|
||||
{
|
||||
UserInterfaceManager.Stylesheet = _previousGlobalStylesheet;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTabContainer()
|
||||
{
|
||||
Tabs.SetTabTitle(0, Loc.GetString("sec-apartment-tab-squads"));
|
||||
Tabs.SetTabTitle(1, Loc.GetString("sec-apartment-tab-timers"));
|
||||
|
||||
Tabs.TabFontColorOverride = SecApartmentStyles.TabActiveColor;
|
||||
Tabs.TabFontColorInactiveOverride = SecApartmentStyles.TabInactiveColor;
|
||||
|
||||
var tabRule = SecApartmentStyles.CreateTabContainerRule(
|
||||
_styles.GetTabActiveStyle(),
|
||||
_styles.GetTabInactiveStyle(),
|
||||
_styles.GetPanelStyle(),
|
||||
_styles.GetBoldFont(),
|
||||
SecApartmentStyles.TabActiveColor,
|
||||
SecApartmentStyles.TabInactiveColor
|
||||
);
|
||||
|
||||
Tabs.Stylesheet = CreateCombinedStylesheet(new[] { tabRule });
|
||||
}
|
||||
|
||||
private Stylesheet CreateCombinedStylesheet(StyleRule[] additionalRules)
|
||||
{
|
||||
var existingStyles = _ui.Stylesheet;
|
||||
var combinedRules = new List<StyleRule>();
|
||||
|
||||
if (existingStyles?.Rules != null)
|
||||
combinedRules.AddRange(existingStyles.Rules);
|
||||
|
||||
combinedRules.AddRange(additionalRules);
|
||||
|
||||
return new Stylesheet(combinedRules);
|
||||
}
|
||||
|
||||
private void OnSquadNameTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
CreateSquadButton.Disabled = string.IsNullOrWhiteSpace(NewSquadName.Text);
|
||||
}
|
||||
|
||||
public void UpdateState(SecApartmentUpdateState state)
|
||||
{
|
||||
_station = state.StationName?.ToUpperInvariant() ?? Loc.GetString("sec-apartment-unknown");
|
||||
|
||||
UpdateUnassignedList(state.UnassignedSecurity, state.Squads);
|
||||
UpdateSquadsList(state.Squads);
|
||||
}
|
||||
|
||||
public void UpdateSensorStatuses(SensorStatusUpdateState sensorState)
|
||||
{
|
||||
foreach (var squadEntry in _squadEntries.Values)
|
||||
{
|
||||
squadEntry.UpdateSensorStatuses(sensorState.MemberStatuses, sensorState.SquadLocations);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTimerState(TimerUpdateState state)
|
||||
{
|
||||
UpdateTimersList(state.Timers);
|
||||
}
|
||||
|
||||
private void UpdateUnassignedList(List<CrewMemberInfo> unassigned, List<Squad> squads)
|
||||
{
|
||||
UnassignedCountLabel.Text = unassigned.Count.ToString();
|
||||
UnassignedContainer.Children.Clear();
|
||||
|
||||
foreach (var member in unassigned)
|
||||
{
|
||||
SpriteSpecifier? iconSpecifier = null;
|
||||
if (_prototypeManager.HasIndex<JobIconPrototype>(member.JobIcon))
|
||||
iconSpecifier = _prototypeManager.Index<JobIconPrototype>(member.JobIcon).Icon;
|
||||
else if (_prototypeManager.HasIndex<StatusIconPrototype>(member.JobIcon))
|
||||
iconSpecifier = _prototypeManager.Index<StatusIconPrototype>(member.JobIcon).Icon;
|
||||
|
||||
var entry = new CrewMemberEntry(member, squads, iconSpecifier, _sprite);
|
||||
entry.OnAssignPressed += squadId =>
|
||||
OnAddMemberToSquad?.Invoke(squadId, member.MemberId);
|
||||
UnassignedContainer.AddChild(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSquadsList(List<Squad> squads)
|
||||
{
|
||||
SquadsCountLabel.Text = squads.Count.ToString();
|
||||
SquadsContainer.Children.Clear();
|
||||
_squadEntries.Clear();
|
||||
|
||||
foreach (var squad in squads)
|
||||
{
|
||||
var entry = new SquadEntry(squad, _styles, _prototypeManager, _sprite);
|
||||
entry.OnIconChanged += iconId =>
|
||||
OnChangeSquadIcon?.Invoke(squad.SquadId, iconId);
|
||||
entry.OnRenamePressed += newName =>
|
||||
{
|
||||
var cleanName = FormattedMessage.RemoveMarkupOrThrow(newName);
|
||||
if (cleanName.Length > 16) cleanName = cleanName[..16];
|
||||
|
||||
OnRenameSquad?.Invoke(squad.SquadId, cleanName);
|
||||
};
|
||||
entry.OnDeletePressed += () =>
|
||||
OnDeleteSquad?.Invoke(squad.SquadId);
|
||||
entry.OnUpdateDescriptionPressed += description =>
|
||||
OnUpdateSquadDescription?.Invoke(squad.SquadId, description);
|
||||
entry.OnStatusChanged += status =>
|
||||
OnChangeSquadStatus?.Invoke(squad.SquadId, status);
|
||||
entry.OnRemoveMemberPressed += memberId =>
|
||||
OnRemoveMemberFromSquad?.Invoke(squad.SquadId, memberId);
|
||||
|
||||
SquadsContainer.AddChild(entry);
|
||||
_squadEntries[squad.SquadId] = entry;
|
||||
|
||||
foreach (var member in squad.Members)
|
||||
{
|
||||
_lastCrewData[member.MemberId] = member;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTimersList(List<TimerEntry> timers)
|
||||
{
|
||||
TimersCountLabel.Text = timers.Count.ToString();
|
||||
|
||||
var toRemove = new List<NetEntity>();
|
||||
foreach (var (timerUid, control) in _timerControls)
|
||||
{
|
||||
if (!timers.Any(t => t.TimerUid == timerUid))
|
||||
toRemove.Add(timerUid);
|
||||
}
|
||||
|
||||
foreach (var timerUid in toRemove)
|
||||
{
|
||||
if (_timerControls.TryGetValue(timerUid, out var control))
|
||||
{
|
||||
TimersContainer.Children.Remove(control);
|
||||
_timerControls.Remove(timerUid);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var timer in timers)
|
||||
{
|
||||
if (!_timerControls.TryGetValue(timer.TimerUid, out var control))
|
||||
{
|
||||
control = new TimerEntryControl(timer, _styles);
|
||||
control.OnRemovePressed += () =>
|
||||
OnRemoveTimer?.Invoke(timer.TimerUid);
|
||||
TimersContainer.AddChild(control);
|
||||
_timerControls[timer.TimerUid] = control;
|
||||
}
|
||||
else
|
||||
{
|
||||
control.UpdateTimer(timer);
|
||||
}
|
||||
}
|
||||
|
||||
var sortedChildren = TimersContainer.Children
|
||||
.OfType<TimerEntryControl>()
|
||||
.OrderBy(t => t.RemainingTime.TotalSeconds)
|
||||
.ToList();
|
||||
|
||||
TimersContainer.Children.Clear();
|
||||
foreach (var child in sortedChildren)
|
||||
{
|
||||
TimersContainer.AddChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 10">
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="6">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="8"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<Label Name="SquadNameLabel"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 10 0"
|
||||
StyleClasses="ConsoleHeadingBig" />
|
||||
|
||||
<OptionButton Name="IconDropdown"
|
||||
StyleClasses="SecApartmentOptionButton"
|
||||
MinWidth="40"
|
||||
Margin="0 0 5 0" />
|
||||
|
||||
<LineEdit Name="SquadNameEdit"
|
||||
PlaceHolder="{Loc 'sec-apartment-ui-squads-nameedit-placeholder'}"
|
||||
HorizontalExpand="True"
|
||||
Visible="False" />
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="RenameButton"
|
||||
Text="{Loc 'sec-apartment-ui-squads-rename'}"
|
||||
ToolTip="{Loc 'sec-apartment-ui-squads-rename-tooltip'}"
|
||||
MinWidth="30" />
|
||||
|
||||
<Button Name="SaveNameButton"
|
||||
Text="{Loc 'sec-apartment-ui-squads-save'}"
|
||||
ToolTip="{Loc 'sec-apartment-ui-squads-save-tooltip'}"
|
||||
Visible="False"
|
||||
MinWidth="30" />
|
||||
|
||||
<Button Name="DeleteButton"
|
||||
Text="{Loc 'sec-apartment-ui-squads-delete'}"
|
||||
ToolTip="{Loc 'sec-apartment-ui-squads-delete-tooltip'}"
|
||||
MinWidth="30" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Top"
|
||||
SeparationOverride="15">
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
MinWidth="300">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalExpand="False">
|
||||
<Label Text="{Loc 'sec-apartment-ui-squads-status'}"
|
||||
FontColorOverride="#ff8888"
|
||||
Margin="0 0 5 0" />
|
||||
|
||||
<OptionButton Name="StatusDropdown" StyleClasses="SecApartmentOptionButton"
|
||||
MinWidth="120"
|
||||
Margin="5 0 0 0" />
|
||||
</BoxContainer>
|
||||
|
||||
<RichTextLabel Name="SquadDescriptionLabel"
|
||||
Margin="2 5 0 0"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<LineEdit Name="SquadDescriptionEdit"
|
||||
PlaceHolder="{Loc 'sec-apartment-ui-squads-desc-placeholder'}"
|
||||
HorizontalExpand="True"
|
||||
Visible="False"
|
||||
Margin="2 5 0 0" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="2 5 0 0"
|
||||
HorizontalExpand="False">
|
||||
<Button Name="EditDescriptionButton"
|
||||
Text="{Loc 'sec-apartment-ui-squads-desc-edit'}"
|
||||
MinWidth="180"
|
||||
Visible="False" />
|
||||
|
||||
<Button Name="SaveDescriptionButton"
|
||||
Text="{Loc 'sec-apartment-ui-squads-desc-save'}"
|
||||
MinWidth="100"
|
||||
Visible="False" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
MinWidth="100">
|
||||
<Label Name="MembersCountLabel"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 5 0 0">
|
||||
<Label Text="{Loc 'sec-apartment-ui-squads-location'}"
|
||||
FontColorOverride="#ff8888"
|
||||
Margin="0 0 5 0" />
|
||||
|
||||
<Label Name="SquadLocationLabel"
|
||||
HorizontalExpand="True"
|
||||
FontColorOverride="#ff9999" />
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" HorizontalExpand="True" Margin="0 5 0 5" SetHeight="2">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#ff4444" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="MembersHeaderLabel" HorizontalAlignment="Center"
|
||||
Text="{Loc 'sec-apartment-ui-squads-members'}"
|
||||
Margin="0 0 0 3" />
|
||||
|
||||
<BoxContainer Name="MembersContainer"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="4" />
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</PanelContainer>
|
||||
@@ -0,0 +1,538 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Corvax.SecApartment.Stylesheets;
|
||||
using Content.Shared.Medical.SuitSensor;
|
||||
using Content.Shared.SecApartment;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Corvax.SecApartment;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SquadEntry : PanelContainer
|
||||
{
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public Squad Squad { get; }
|
||||
|
||||
public Action<string>? OnRenamePressed;
|
||||
public Action? OnDeletePressed;
|
||||
public Action<string>? OnUpdateDescriptionPressed;
|
||||
public Action<SquadStatus>? OnStatusChanged;
|
||||
public Action<string>? OnRemoveMemberPressed;
|
||||
public Action<SquadIconNum>? OnIconChanged;
|
||||
|
||||
private readonly SecApartmentStyles _styles;
|
||||
|
||||
private bool _isEditingName = false;
|
||||
private bool _isEditingDescription = false;
|
||||
private Dictionary<string, PanelContainer> _memberPanels = new();
|
||||
|
||||
public SquadEntry(Squad squad, SecApartmentStyles styles, IPrototypeManager protoMan, SpriteSystem sprite)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_prototypeManager = protoMan;
|
||||
_sprite = sprite;
|
||||
_styles = styles;
|
||||
Squad = squad;
|
||||
|
||||
SetupStyles();
|
||||
SetupUI();
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void SetupUI()
|
||||
{
|
||||
RenameButton.OnPressed += _ => ToggleNameEdit();
|
||||
DeleteButton.OnPressed += _ => OnDeletePressed?.Invoke();
|
||||
EditDescriptionButton.OnPressed += _ => ToggleDescriptionEdit();
|
||||
SaveDescriptionButton.OnPressed += _ => SaveDescription();
|
||||
|
||||
SetupIconDropdown();
|
||||
|
||||
foreach (SquadStatus status in Enum.GetValues(typeof(SquadStatus)))
|
||||
{
|
||||
StatusDropdown.AddItem(GetStatusText(status), (int)status);
|
||||
}
|
||||
|
||||
StatusDropdown.OnItemSelected += args =>
|
||||
{
|
||||
StatusDropdown.SelectId(args.Id);
|
||||
var status = (SquadStatus)args.Id;
|
||||
OnStatusChanged?.Invoke(status);
|
||||
};
|
||||
|
||||
SaveNameButton.OnPressed += _ => SaveName();
|
||||
|
||||
UpdateMembersList();
|
||||
}
|
||||
|
||||
private void SetupStyles()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#4a1a1a"),
|
||||
BorderColor = SecApartmentStyles.TabActiveColor,
|
||||
BorderThickness = new Thickness(2),
|
||||
ContentMarginBottomOverride = 8,
|
||||
ContentMarginLeftOverride = 10,
|
||||
ContentMarginRightOverride = 10,
|
||||
ContentMarginTopOverride = 8
|
||||
};
|
||||
|
||||
RenameButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
DeleteButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
EditDescriptionButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
SaveDescriptionButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
SaveNameButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
|
||||
SquadNameEdit.AddStyleClass(SecApartmentStyles.StyleClassConsoleLineEdit);
|
||||
SquadDescriptionEdit.AddStyleClass(SecApartmentStyles.StyleClassConsoleLineEdit);
|
||||
|
||||
IconDropdown.AddStyleClass(SecApartmentStyles.StyleClassOptionButton);
|
||||
StatusDropdown.AddStyleClass(SecApartmentStyles.StyleClassOptionButton);
|
||||
|
||||
IconDropdown.OptionStyleClasses.Add(SecApartmentStyles.StyleClassButtonRed);
|
||||
StatusDropdown.OptionStyleClasses.Add(SecApartmentStyles.StyleClassButtonRed);
|
||||
|
||||
SquadNameLabel.FontColorOverride = SecApartmentStyles.HeadingColor;
|
||||
MembersCountLabel.FontColorOverride = SecApartmentStyles.TextColor;
|
||||
MembersHeaderLabel.FontColorOverride = SecApartmentStyles.SubHeadingColor;
|
||||
}
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
SquadNameLabel.Text = Squad.Name.ToUpperInvariant();
|
||||
SquadNameEdit.Text = Squad.Name;
|
||||
SquadDescriptionLabel.Text = string.IsNullOrWhiteSpace(Squad.Description)
|
||||
? Loc.GetString("sec-apartment-squad-no-desc") : Squad.Description;
|
||||
SquadDescriptionEdit.Text = Squad.Description;
|
||||
|
||||
SquadLocationLabel.Text = Loc.GetString("sec-apartment-unknown");
|
||||
SquadLocationLabel.FontColorOverride = SecApartmentStyles.TabInactiveColor;
|
||||
|
||||
IconDropdown.SelectId((int)Squad.IconId);
|
||||
StatusDropdown.SelectId((int)Squad.Status);
|
||||
|
||||
MembersCountLabel.Text = Loc.GetString("sec-apartment-squad-members", ("count", Squad.Members.Count));
|
||||
|
||||
UpdateEditMode();
|
||||
}
|
||||
|
||||
private void SetupIconDropdown()
|
||||
{
|
||||
IconDropdown.Clear();
|
||||
foreach (SquadIconNum icon in Enum.GetValues(typeof(SquadIconNum)))
|
||||
{
|
||||
IconDropdown.AddItem(GetIconText(icon), (int)icon);
|
||||
}
|
||||
|
||||
IconDropdown.OnItemSelected += args =>
|
||||
{
|
||||
IconDropdown.SelectId(args.Id);
|
||||
var icon = (SquadIconNum)args.Id;
|
||||
OnIconChanged?.Invoke(icon);
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateEditMode()
|
||||
{
|
||||
SquadNameLabel.Visible = !_isEditingName;
|
||||
IconDropdown.Visible = !_isEditingName;
|
||||
SquadNameEdit.Visible = _isEditingName;
|
||||
SaveNameButton.Visible = _isEditingName;
|
||||
RenameButton.Visible = !_isEditingName;
|
||||
|
||||
SquadDescriptionLabel.Visible = !_isEditingDescription;
|
||||
SquadDescriptionEdit.Visible = _isEditingDescription;
|
||||
SaveDescriptionButton.Visible = _isEditingDescription;
|
||||
EditDescriptionButton.Visible = !_isEditingDescription;
|
||||
}
|
||||
|
||||
private void UpdateMembersList()
|
||||
{
|
||||
MembersContainer.Children.Clear();
|
||||
|
||||
if (Squad.Members.Count == 0)
|
||||
{
|
||||
var emptyLabel = new Label
|
||||
{
|
||||
Text = Loc.GetString("sec-apartment-squad-no-members"),
|
||||
FontColorOverride = SecApartmentStyles.TabInactiveColor,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
Margin = new Thickness(0, 5)
|
||||
};
|
||||
MembersContainer.AddChild(emptyLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var member in Squad.Members)
|
||||
{
|
||||
var memberEntry = CreateMemberEntry(member);
|
||||
MembersContainer.AddChild(memberEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private PanelContainer CreateMemberEntry(CrewMemberInfo member)
|
||||
{
|
||||
Color backgroundColor;
|
||||
Color borderColor;
|
||||
|
||||
if (member.SensorStatus == null || !member.SensorStatus.IsAlive)
|
||||
{
|
||||
backgroundColor = Color.FromHex("#1a0a0a");
|
||||
borderColor = Color.FromHex("#990000");
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundColor = Color.FromHex("#3a0f0f");
|
||||
borderColor = Color.FromHex("#ff6666");
|
||||
}
|
||||
|
||||
var panel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = backgroundColor,
|
||||
BorderColor = borderColor,
|
||||
BorderThickness = new Thickness(1),
|
||||
ContentMarginBottomOverride = 4,
|
||||
ContentMarginLeftOverride = 8,
|
||||
ContentMarginRightOverride = 8,
|
||||
ContentMarginTopOverride = 4
|
||||
}
|
||||
};
|
||||
|
||||
var container = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SeparationOverride = 8
|
||||
};
|
||||
|
||||
var infoContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var nameContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = member.Name,
|
||||
FontColorOverride = member.SensorStatus?.IsAlive == false
|
||||
? Color.FromHex("#888888")
|
||||
: SecApartmentStyles.TextColor,
|
||||
FontOverride = _styles.GetBoldFont(12)
|
||||
};
|
||||
|
||||
nameContainer.AddChild(nameLabel);
|
||||
|
||||
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "alive");
|
||||
|
||||
if (member.SensorStatus != null)
|
||||
{
|
||||
if (!member.SensorStatus.IsAlive)
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "dead");
|
||||
}
|
||||
else if (member.SensorStatus.DamagePercentage != null)
|
||||
{
|
||||
var index = MathF.Round(4f * member.SensorStatus.DamagePercentage.Value);
|
||||
|
||||
if (index >= 5)
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical");
|
||||
else
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "health" + index);
|
||||
}
|
||||
}/*
|
||||
else
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/health_icons.rsi"), "Critical");
|
||||
}*///всётаки оставлю чуть больше шансов антагам :D
|
||||
|
||||
var statusIcon = new AnimatedTextureRect
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
Margin = new Thickness(0, 1, 3, 0),
|
||||
};
|
||||
|
||||
statusIcon.SetFromSpriteSpecifier(specifier);
|
||||
statusIcon.DisplayRect.TextureScale = new Vector2(2f, 2f);
|
||||
|
||||
nameContainer.AddChild(statusIcon);
|
||||
|
||||
infoContainer.AddChild(nameContainer);
|
||||
|
||||
var jobContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var jobLabel = new Label
|
||||
{
|
||||
Text = member.JobTitle,
|
||||
FontColorOverride = SecApartmentStyles.SubTextColor,
|
||||
FontOverride = _styles.GetRegularFont(10),
|
||||
Margin = new Thickness(0, 0, 5, 0)
|
||||
};
|
||||
|
||||
jobContainer.AddChild(jobLabel);
|
||||
|
||||
SpriteSpecifier? iconSpecifier = null;
|
||||
if (_prototypeManager.HasIndex<JobIconPrototype>(member.JobIcon))
|
||||
iconSpecifier = _prototypeManager.Index<JobIconPrototype>(member.JobIcon).Icon;
|
||||
else if (_prototypeManager.HasIndex<StatusIconPrototype>(member.JobIcon))
|
||||
iconSpecifier = _prototypeManager.Index<StatusIconPrototype>(member.JobIcon).Icon;
|
||||
|
||||
if (iconSpecifier != null)
|
||||
{
|
||||
var icon = new TextureRect()
|
||||
{
|
||||
TextureScale = new Vector2(2, 2),
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
Texture = _sprite.Frame0(iconSpecifier),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
};
|
||||
|
||||
jobContainer.AddChild(icon);
|
||||
}
|
||||
|
||||
infoContainer.AddChild(jobContainer);
|
||||
|
||||
var removeButton = new Button
|
||||
{
|
||||
Text = Loc.GetString("sec-apartment-squad-remove-name"),
|
||||
ToolTip = Loc.GetString("sec-apartment-squad-remove-tooltip"),
|
||||
MinWidth = 30
|
||||
};
|
||||
removeButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
|
||||
removeButton.OnPressed += _ =>
|
||||
{
|
||||
OnRemoveMemberPressed?.Invoke(member.MemberId);
|
||||
};
|
||||
|
||||
container.AddChild(infoContainer);
|
||||
container.AddChild(removeButton);
|
||||
|
||||
panel.AddChild(container);
|
||||
|
||||
_memberPanels[member.MemberId] = panel;
|
||||
return panel;
|
||||
}
|
||||
|
||||
public void UpdateSensorStatuses(Dictionary<string, SuitSensorStatus?> statuses,
|
||||
Dictionary<string, (string Location, bool HasLocation)> squadLocations)
|
||||
{
|
||||
foreach (var (memberId, panel) in _memberPanels)
|
||||
{
|
||||
if (statuses.TryGetValue(memberId, out var status))
|
||||
UpdateMemberSensorStatus(panel, status);
|
||||
}
|
||||
|
||||
if (squadLocations.TryGetValue(Squad.SquadId, out var locationInfo))
|
||||
UpdateSquadLocation(locationInfo);
|
||||
}
|
||||
|
||||
private void UpdateMemberSensorStatus(PanelContainer panel, SuitSensorStatus? status)
|
||||
{
|
||||
Color backgroundColor;
|
||||
Color borderColor;
|
||||
|
||||
if (status == null || !status.IsAlive)
|
||||
{
|
||||
backgroundColor = Color.FromHex("#1a0a0a");
|
||||
borderColor = Color.FromHex("#990000");
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundColor = Color.FromHex("#3a0f0f");
|
||||
borderColor = Color.FromHex("#ff6666");
|
||||
}
|
||||
|
||||
if (panel.PanelOverride is StyleBoxFlat styleBox)
|
||||
{
|
||||
styleBox.BackgroundColor = backgroundColor;
|
||||
styleBox.BorderColor = borderColor;
|
||||
}
|
||||
|
||||
if (panel.Children.FirstOrDefault() is BoxContainer container)
|
||||
{
|
||||
var infoContainer = container.Children.OfType<BoxContainer>().FirstOrDefault();
|
||||
if (infoContainer != null)
|
||||
{
|
||||
var nameContainer = infoContainer.Children.OfType<BoxContainer>().FirstOrDefault();
|
||||
if (nameContainer != null)
|
||||
{
|
||||
var nameLabel = nameContainer.Children.OfType<Label>().FirstOrDefault();
|
||||
if (nameLabel != null)
|
||||
{
|
||||
nameLabel.FontColorOverride = status?.IsAlive == false
|
||||
? Color.FromHex("#888888")
|
||||
: SecApartmentStyles.TextColor;
|
||||
}
|
||||
|
||||
var statusIcon = nameContainer.Children.OfType<AnimatedTextureRect>().FirstOrDefault();
|
||||
if (statusIcon != null)
|
||||
{
|
||||
SpriteSpecifier specifier = new SpriteSpecifier.Rsi(
|
||||
new ResPath("Interface/Alerts/human_crew_monitoring.rsi"),
|
||||
"alive"
|
||||
);
|
||||
|
||||
if (status != null)
|
||||
{
|
||||
if (!status.IsAlive)
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(
|
||||
new ResPath("Interface/Alerts/human_crew_monitoring.rsi"),
|
||||
"dead"
|
||||
);
|
||||
}
|
||||
else if (status.DamagePercentage != null)
|
||||
{
|
||||
var index = MathF.Round(4f * status.DamagePercentage.Value);
|
||||
if (index >= 5)
|
||||
specifier = new SpriteSpecifier.Rsi(
|
||||
new ResPath("Interface/Alerts/human_crew_monitoring.rsi"),
|
||||
"critical"
|
||||
);
|
||||
else
|
||||
specifier = new SpriteSpecifier.Rsi(
|
||||
new ResPath("Interface/Alerts/human_crew_monitoring.rsi"),
|
||||
"health" + index
|
||||
);
|
||||
}
|
||||
}/*
|
||||
else
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/health_icons.rsi"), "Critical");
|
||||
}*///всётаки оставлю больше шансов антагам :D
|
||||
|
||||
statusIcon.SetFromSpriteSpecifier(specifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSquadLocation((string Location, bool HasLocation) locationInfo)
|
||||
{
|
||||
if (locationInfo.HasLocation && !string.IsNullOrWhiteSpace(locationInfo.Location))
|
||||
{
|
||||
SquadLocationLabel.Text = locationInfo.Location;
|
||||
SquadLocationLabel.FontColorOverride = SecApartmentStyles.TextColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
SquadLocationLabel.Text = Loc.GetString("sec-apartment-unknown");
|
||||
SquadLocationLabel.FontColorOverride = SecApartmentStyles.TabInactiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleNameEdit()
|
||||
{
|
||||
_isEditingName = !_isEditingName;
|
||||
UpdateEditMode();
|
||||
|
||||
if (_isEditingName)
|
||||
{
|
||||
SquadNameEdit.Text = Squad.Name;
|
||||
SquadNameEdit.GrabKeyboardFocus();
|
||||
SquadNameEdit.CursorPosition = SquadNameEdit.Text.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleDescriptionEdit()
|
||||
{
|
||||
_isEditingDescription = !_isEditingDescription;
|
||||
UpdateEditMode();
|
||||
|
||||
if (_isEditingDescription)
|
||||
{
|
||||
SquadDescriptionEdit.Text = Squad.Description ?? "";
|
||||
SquadDescriptionEdit.GrabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveName()
|
||||
{
|
||||
var newName = SquadNameEdit.Text.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(newName) && newName != Squad.Name)
|
||||
{
|
||||
OnRenamePressed?.Invoke(newName);
|
||||
}
|
||||
_isEditingName = false;
|
||||
UpdateEditMode();
|
||||
}
|
||||
|
||||
private void SaveDescription()
|
||||
{
|
||||
var description = SquadDescriptionEdit.Text.Trim();
|
||||
OnUpdateDescriptionPressed?.Invoke(description);
|
||||
_isEditingDescription = false;
|
||||
UpdateEditMode();
|
||||
}
|
||||
|
||||
private string GetStatusText(SquadStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
SquadStatus.Active => Loc.GetString("sec-apartment-active"),
|
||||
SquadStatus.OnBreak => Loc.GetString("sec-apartment-on-break"),
|
||||
_ => Loc.GetString("sec-apartment-unknown")
|
||||
};
|
||||
}
|
||||
|
||||
private string GetIconText(SquadIconNum icon)
|
||||
{
|
||||
return icon switch
|
||||
{
|
||||
SquadIconNum.Alpha => Loc.GetString("sec-apartment-icon-alpha"),
|
||||
SquadIconNum.Beta => Loc.GetString("sec-apartment-icon-beta"),
|
||||
SquadIconNum.Gamma => Loc.GetString("sec-apartment-icon-gamma"),
|
||||
SquadIconNum.Delta => Loc.GetString("sec-apartment-icon-delta"),
|
||||
SquadIconNum.Epsilon => Loc.GetString("sec-apartment-icon-epsilon"),
|
||||
SquadIconNum.Zeta => Loc.GetString("sec-apartment-icon-zeta"),
|
||||
SquadIconNum.Heta => Loc.GetString("sec-apartment-icon-heta"),
|
||||
SquadIconNum.Theta => Loc.GetString("sec-apartment-icon-theta"),
|
||||
SquadIconNum.Iota => Loc.GetString("sec-apartment-icon-iota"),
|
||||
SquadIconNum.Kappa => Loc.GetString("sec-apartment-icon-kappa"),
|
||||
SquadIconNum.Lambda => Loc.GetString("sec-apartment-icon-lambda"),
|
||||
SquadIconNum.Mu => Loc.GetString("sec-apartment-icon-mu"),
|
||||
SquadIconNum.Nu => Loc.GetString("sec-apartment-icon-nu"),
|
||||
SquadIconNum.Xi => Loc.GetString("sec-apartment-icon-xi"),
|
||||
SquadIconNum.Omicron => Loc.GetString("sec-apartment-icon-omicron"),
|
||||
SquadIconNum.Pi => Loc.GetString("sec-apartment-icon-pi"),
|
||||
SquadIconNum.Ro => Loc.GetString("sec-apartment-icon-ro"),
|
||||
SquadIconNum.Sigma => Loc.GetString("sec-apartment-icon-sigma"),
|
||||
SquadIconNum.Tau => Loc.GetString("sec-apartment-icon-tau"),
|
||||
SquadIconNum.Upsilon => Loc.GetString("sec-apartment-icon-upsilon"),
|
||||
SquadIconNum.Fi => Loc.GetString("sec-apartment-icon-fi"),
|
||||
SquadIconNum.Hi => Loc.GetString("sec-apartment-icon-hi"),
|
||||
SquadIconNum.Psi => Loc.GetString("sec-apartment-icon-psi"),
|
||||
SquadIconNum.Omega => Loc.GetString("sec-apartment-icon-omega"),
|
||||
_ => Loc.GetString("sec-apartment-unknown")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using Content.Client.Resources;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Corvax.SecApartment.Stylesheets;
|
||||
|
||||
public sealed class SecApartmentStyles
|
||||
{
|
||||
private readonly IResourceCache _resCache;
|
||||
|
||||
public static Color TabActiveColor => Color.FromHex("#ff4444");
|
||||
public static Color TabInactiveColor => Color.FromHex("#ff8888");
|
||||
public static Color HeadingColor => Color.FromHex("#ff4444");
|
||||
public static Color SubHeadingColor => Color.FromHex("#ff8888");
|
||||
public static Color TextColor => Color.FromHex("#ff9999");
|
||||
public static Color SubTextColor => Color.FromHex("#ff8888");
|
||||
public static Color PlaceholderColor => Color.FromHex("#ff6666");
|
||||
|
||||
public const string StyleClassButtonRed = "ButtonRed";
|
||||
public const string StyleClassConsoleLineEdit = "ConsoleLineEdit";
|
||||
public const string StyleClassConsoleHeading = "ConsoleHeading";
|
||||
public const string StyleClassOptionButton = "SecApartmentOptionButton";
|
||||
|
||||
public SecApartmentStyles(IResourceCache resCache)
|
||||
{
|
||||
_resCache = resCache;
|
||||
}
|
||||
|
||||
private StyleBoxFlat CreateStyleBox(Color backgroundColor, Color borderColor,
|
||||
Thickness borderThickness, Thickness? contentMargin = null)
|
||||
{
|
||||
var style = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = backgroundColor,
|
||||
BorderColor = borderColor,
|
||||
BorderThickness = borderThickness
|
||||
};
|
||||
|
||||
if (contentMargin.HasValue)
|
||||
{
|
||||
style.ContentMarginLeftOverride = contentMargin.Value.Left;
|
||||
style.ContentMarginRightOverride = contentMargin.Value.Right;
|
||||
style.ContentMarginTopOverride = contentMargin.Value.Top;
|
||||
style.ContentMarginBottomOverride = contentMargin.Value.Bottom;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
public StyleBox GetTabActiveStyle() => CreateStyleBox(
|
||||
Color.FromHex("#440000"),
|
||||
TabActiveColor,
|
||||
new Thickness(2, 2, 2, 0),
|
||||
new Thickness(10, 5, 10, 5)
|
||||
);
|
||||
|
||||
public StyleBox GetTabInactiveStyle() => CreateStyleBox(
|
||||
Color.FromHex("#220000"),
|
||||
TabInactiveColor,
|
||||
new Thickness(2, 2, 2, 0),
|
||||
new Thickness(10, 5, 10, 5)
|
||||
);
|
||||
|
||||
public StyleBox GetPanelStyle() => CreateStyleBox(
|
||||
Color.FromHex("#110000"),
|
||||
TabActiveColor,
|
||||
new Thickness(2),
|
||||
new Thickness(5, 5, 5, 5)
|
||||
);
|
||||
|
||||
public StyleBox GetButtonRedStyle() => CreateStyleBox(
|
||||
Color.FromHex("#660000"),
|
||||
Color.FromHex("#ff4444"),
|
||||
new Thickness(1),
|
||||
new Thickness(8, 4, 8, 4)
|
||||
);
|
||||
|
||||
public StyleBox GetLineEditStyle() => CreateStyleBox(
|
||||
Color.FromHex("#110000"),
|
||||
Color.FromHex("#ff4444"),
|
||||
new Thickness(1),
|
||||
new Thickness(4, 2, 4, 2)
|
||||
);
|
||||
|
||||
public Font GetBoldFont(int size = 12) => _resCache.GetFont(new[]
|
||||
{
|
||||
"/Fonts/NotoSans/NotoSans-Bold.ttf",
|
||||
"/Fonts/NotoSans/NotoSansSymbols-Regular.ttf",
|
||||
"/Fonts/NotoSans/NotoSansSymbols2-Regular.ttf"
|
||||
}, size);
|
||||
|
||||
public Font GetRegularFont(int size = 12) => _resCache.GetFont(new[]
|
||||
{
|
||||
"/Fonts/NotoSans/NotoSans-Regular.ttf",
|
||||
"/Fonts/NotoSans/NotoSansSymbols-Regular.ttf",
|
||||
"/Fonts/NotoSans/NotoSansSymbols2-Regular.ttf"
|
||||
}, size);
|
||||
|
||||
public static StyleRule CreateButtonRedRule(StyleBox buttonRedStyle, Font font, Color fontColor, Color disabledColor)
|
||||
{
|
||||
return new StyleRule(
|
||||
new SelectorElement(typeof(Button), new[] { StyleClassButtonRed }, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty("stylebox", buttonRedStyle),
|
||||
new StyleProperty("font-color", fontColor),
|
||||
new StyleProperty("font", font),
|
||||
new StyleProperty("font-color-disabled", disabledColor)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static StyleRule CreateLineEditRule(StyleBox lineEditStyle, Font font, Color textColor, Color placeholderColor)
|
||||
{
|
||||
return new StyleRule(
|
||||
new SelectorElement(typeof(LineEdit), new[] { StyleClassConsoleLineEdit }, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty("stylebox", lineEditStyle),
|
||||
new StyleProperty("font-color", textColor),
|
||||
new StyleProperty("font", font),
|
||||
new StyleProperty("placeholder-color", placeholderColor),
|
||||
new StyleProperty("cursor-color", TabActiveColor),
|
||||
new StyleProperty("selection-color", TabActiveColor.WithAlpha(0.3f))
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public StyleBox GetOptionButtonStyle() => CreateStyleBox(
|
||||
Color.FromHex("#330000"),
|
||||
TabActiveColor,
|
||||
new Thickness(1),
|
||||
new Thickness(6, 3, 6, 3)
|
||||
);
|
||||
|
||||
public static StyleRule CreateOptionButtonRule(StyleBox optionStyle, Font font, Color fontColor, Color disabledColor)
|
||||
{
|
||||
return new StyleRule(
|
||||
new SelectorElement(typeof(OptionButton), new[] { StyleClassOptionButton }, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(ContainerButton.StylePropertyStyleBox, optionStyle),
|
||||
new StyleProperty("font", font),
|
||||
new StyleProperty("font-color", fontColor),
|
||||
new StyleProperty("font-color-disabled", disabledColor)
|
||||
}
|
||||
);
|
||||
}
|
||||
public static StyleRule CreateOptionButtonBackgroundRule()
|
||||
{
|
||||
return new StyleRule(
|
||||
new SelectorElement(typeof(PanelContainer), new[] { OptionButton.StyleClassOptionsBackground }, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#330000"),
|
||||
BorderColor = TabActiveColor,
|
||||
BorderThickness = new Thickness(1)
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
public static StyleRule CreateTabContainerRule(StyleBox tabActiveStyle, StyleBox tabInactiveStyle,
|
||||
StyleBox panelStyle, Font font, Color activeColor, Color inactiveColor)
|
||||
{
|
||||
return new StyleRule(
|
||||
new SelectorElement(typeof(TabContainer), null, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty("tab-stylebox", tabActiveStyle),
|
||||
new StyleProperty("tab-stylebox-inactive", tabInactiveStyle),
|
||||
new StyleProperty("panel-stylebox", panelStyle),
|
||||
new StyleProperty("tab-font-color", activeColor),
|
||||
new StyleProperty("tab-font-color-inactive", inactiveColor),
|
||||
new StyleProperty("font", font)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 5">
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="4">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<Label Name="TimerLabel"
|
||||
HorizontalExpand="True"
|
||||
FontColorOverride="#ff9999"
|
||||
Margin="0 0 10 0" />
|
||||
|
||||
<Label Name="TimeLabel"
|
||||
MinWidth="80"
|
||||
HorizontalAlignment="Right"
|
||||
FontColorOverride="#ff6666" />
|
||||
|
||||
<Button Name="RemoveButton"
|
||||
Text="{Loc 'sec-apartment-ui-timers-remove'}"
|
||||
ToolTip="{Loc 'sec-apartment-ui-timers-remove-tooltip'}"
|
||||
MinWidth="40"
|
||||
Margin="5 0 0 0" />
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<Label Text="{Loc 'sec-apartment-ui-timers-total-time'}"
|
||||
FontColorOverride="#ff8888"
|
||||
Margin="0 0 5 0" />
|
||||
|
||||
<Label Name="TotalTimeLabel"
|
||||
HorizontalExpand="True"
|
||||
FontColorOverride="#ffaaaa" />
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" HorizontalExpand="True" Margin="0 2 0 2" SetHeight="1">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#ff4444" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
|
||||
</BoxContainer>
|
||||
|
||||
</PanelContainer>
|
||||
@@ -0,0 +1,101 @@
|
||||
using Content.Client.Corvax.SecApartment.Stylesheets;
|
||||
using Content.Shared.SecApartment;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Corvax.SecApartment;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TimerEntryControl : PanelContainer
|
||||
{
|
||||
private TimerEntry _timerEntry;
|
||||
private readonly SecApartmentStyles _styles;
|
||||
|
||||
public TimeSpan RemainingTime => _timerEntry.RemainingTime;
|
||||
|
||||
public Action? OnRemovePressed;
|
||||
|
||||
public TimerEntryControl(TimerEntry timerEntry, SecApartmentStyles styles)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_timerEntry = timerEntry;
|
||||
_styles = styles;
|
||||
|
||||
RemoveButton.OnPressed += _ => OnRemovePressed?.Invoke();
|
||||
|
||||
SetupStyles();
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void SetupStyles()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#2a0a0a"),
|
||||
BorderColor = Color.FromHex("#ff4444"),
|
||||
BorderThickness = new Thickness(1),
|
||||
ContentMarginBottomOverride = 6,
|
||||
ContentMarginLeftOverride = 8,
|
||||
ContentMarginRightOverride = 8,
|
||||
ContentMarginTopOverride = 6
|
||||
};
|
||||
|
||||
RemoveButton.AddStyleClass(SecApartmentStyles.StyleClassButtonRed);
|
||||
|
||||
TimerLabel.FontOverride = _styles.GetBoldFont(12);
|
||||
TimeLabel.FontOverride = _styles.GetBoldFont(12);
|
||||
TotalTimeLabel.FontOverride = _styles.GetRegularFont(10);
|
||||
}
|
||||
|
||||
public void UpdateTimer(TimerEntry timerEntry)
|
||||
{
|
||||
_timerEntry = timerEntry;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void UpdateDisplay()
|
||||
{
|
||||
TimerLabel.Text = _timerEntry.Label;
|
||||
|
||||
var remaining = _timerEntry.RemainingTime;
|
||||
UpdateTimeLabel(remaining);
|
||||
|
||||
TotalTimeLabel.Text = FormatTimeSpan(_timerEntry.TotalTime);
|
||||
}
|
||||
|
||||
private void UpdateTimeLabel(TimeSpan remaining)
|
||||
{
|
||||
if (remaining < TimeSpan.Zero)
|
||||
{
|
||||
var overdue = -remaining;
|
||||
TimeLabel.Text = $"-{FormatTimeSpan(overdue)}";
|
||||
TimeLabel.FontColorOverride = Color.FromHex("#ff0000");
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeLabel.Text = FormatTimeSpan(remaining);
|
||||
|
||||
if (remaining.TotalSeconds < 30)
|
||||
TimeLabel.FontColorOverride = Color.FromHex("#ff3333");
|
||||
else if (remaining.TotalSeconds < 60)
|
||||
TimeLabel.FontColorOverride = Color.FromHex("#ff9933");
|
||||
else
|
||||
TimeLabel.FontColorOverride = Color.FromHex("#ff6666");
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatTimeSpan(TimeSpan timeSpan)
|
||||
{
|
||||
if (timeSpan.TotalHours >= 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{timeSpan.Minutes:00}:{timeSpan.Seconds:00}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,31 @@
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#1A1A1A" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="5">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-voice-categories'}" StyleClasses="LabelHeading" HorizontalAlignment="Center" Margin="0 0 0 5"/>
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<GridContainer Name="CategoriesContainer" Columns="4" HorizontalExpand="True"/>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="5">
|
||||
<Label Text="{Loc 'humanoid-profile-editor-voice-categories'}" StyleClasses="LabelHeading"
|
||||
HorizontalAlignment="Center" Margin="0 0 0 5"/>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Name="CategoriesContainer" Orientation="Vertical" VerticalExpand="True"
|
||||
SeparationOverride="5"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<ScrollContainer VerticalExpand="True" Margin="5">
|
||||
<GridContainer Name="VoicesGrid" Columns="3" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
<PanelContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#2A2A2A" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="5">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
|
||||
<LineEdit Name="SearchEdit" MinWidth="400" HorizontalExpand="True"
|
||||
PlaceHolder="{Loc 'humanoid-profile-editor-voice-placeholder'}"/>
|
||||
<Label Name="ResultsLabel" MinWidth="100" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<GridContainer Name="VoicesGrid" Columns="2" HorizontalExpand="True" Margin="0 5 0 0"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Damage;
|
||||
|
||||
@@ -55,7 +57,7 @@ public sealed partial class DamageVisualsComponent : Component
|
||||
/// (for example, Brute), and has a value
|
||||
/// of a DamageVisualizerSprite (see below)
|
||||
/// </summary>
|
||||
[DataField("damageOverlayGroups")] public Dictionary<string, DamageVisualizerSprite>? DamageOverlayGroups;
|
||||
[DataField("damageOverlayGroups")] public Dictionary<ProtoId<DamageGroupPrototype>, DamageVisualizerSprite>? DamageOverlayGroups;
|
||||
|
||||
/// <summary>
|
||||
/// Sets if you want sprites to overlay the
|
||||
@@ -84,7 +86,7 @@ public sealed partial class DamageVisualsComponent : Component
|
||||
/// what kind of damage combination
|
||||
/// you would want, on which threshold.
|
||||
/// </remarks>
|
||||
[DataField("damageGroup")] public string? DamageGroup;
|
||||
[DataField("damageGroup")] public ProtoId<DamageGroupPrototype>? DamageGroup;
|
||||
|
||||
/// <summary>
|
||||
/// Set this if you want incoming damage to be
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -28,6 +29,7 @@ namespace Content.Client.Damage;
|
||||
public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -174,7 +176,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
// See if that group is in our entity's damage container.
|
||||
else if (!damageVisComp.Overlay && damageVisComp.DamageGroup != null)
|
||||
{
|
||||
if (!damageContainer.SupportedGroups.Contains(damageVisComp.DamageGroup))
|
||||
if (!damageContainer.SupportedGroups.Contains(damageVisComp.DamageGroup.Value))
|
||||
{
|
||||
Log.Error($"Damage keys were invalid for entity {entity}.");
|
||||
damageVisComp.Valid = false;
|
||||
@@ -384,7 +386,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
if (!AppearanceSystem.TryGetData<DamageVisualizerGroupData>(uid, DamageVisualizerKeys.DamageUpdateGroups,
|
||||
out var data, component))
|
||||
{
|
||||
data = new DamageVisualizerGroupData(Comp<DamageableComponent>(uid).DamagePerGroup.Keys.ToList());
|
||||
data = new DamageVisualizerGroupData(_damageable.GetDamagePerGroup(uid).Keys.ToList());
|
||||
}
|
||||
|
||||
UpdateDamageVisuals(data.GroupList, (uid, damageComponent, spriteComponent, damageVisComp));
|
||||
@@ -486,11 +488,10 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
/// </summary>
|
||||
private void UpdateDamageVisuals(Entity<DamageableComponent, SpriteComponent, DamageVisualsComponent> entity)
|
||||
{
|
||||
var damageComponent = entity.Comp1;
|
||||
var spriteComponent = entity.Comp2;
|
||||
var damageVisComp = entity.Comp3;
|
||||
|
||||
if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageVisComp.LastDamageThreshold, damageVisComp, out var threshold))
|
||||
if (!CheckThresholdBoundary(_damageable.GetTotalDamage(entity.AsNullable()), damageVisComp.LastDamageThreshold, damageVisComp, out var threshold))
|
||||
return;
|
||||
|
||||
damageVisComp.LastDamageThreshold = threshold;
|
||||
@@ -513,11 +514,11 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
/// according to the list of damage groups
|
||||
/// passed into it.
|
||||
/// </summary>
|
||||
private void UpdateDamageVisuals(List<string> delta, Entity<DamageableComponent, SpriteComponent, DamageVisualsComponent> entity)
|
||||
private void UpdateDamageVisuals(List<ProtoId<DamageGroupPrototype>> delta, Entity<DamageableComponent, SpriteComponent, DamageVisualsComponent> entity)
|
||||
{
|
||||
var damageComponent = entity.Comp1;
|
||||
var spriteComponent = entity.Comp2;
|
||||
var damageVisComp = entity.Comp3;
|
||||
var damage = _damageable.GetAllDamage((entity.Owner, entity.Comp1));
|
||||
|
||||
foreach (var damageGroup in delta)
|
||||
{
|
||||
@@ -525,7 +526,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
continue;
|
||||
|
||||
if (!_prototypeManager.TryIndex<DamageGroupPrototype>(damageGroup, out var damageGroupPrototype)
|
||||
|| !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal))
|
||||
|| !damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal))
|
||||
continue;
|
||||
|
||||
if (!damageVisComp.LastThresholdPerGroup.TryGetValue(damageGroup, out var lastThreshold)
|
||||
@@ -590,7 +591,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
|
||||
}
|
||||
else if (damageVisComp.DamageGroup != null)
|
||||
{
|
||||
UpdateDamageVisuals(new List<string>() { damageVisComp.DamageGroup }, entity);
|
||||
UpdateDamageVisuals(new() { damageVisComp.DamageGroup.Value }, entity);
|
||||
}
|
||||
else if (damageVisComp.DamageOverlay != null)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Changelog;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Corvax.ExportSprites;
|
||||
using Content.Client.DebugMon;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.FeedbackPopup;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.GhostKick;
|
||||
@@ -24,6 +26,7 @@ using Content.Client.UserInterface;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Client.Voting;
|
||||
using Content.Shared.Ame.Components;
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Client;
|
||||
@@ -49,6 +52,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly EntityScreenshotGenerator _entityScreenshotGenerator = default!; // Corvax-Wiki
|
||||
[Dependency] private readonly IParallaxManager _parallaxManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IStylesheetManager _stylesheetManager = default!;
|
||||
@@ -76,6 +80,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!;
|
||||
[Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
|
||||
|
||||
public override void PreInit()
|
||||
{
|
||||
@@ -132,6 +137,7 @@ namespace Content.Client.Entry
|
||||
|
||||
_componentFactory.GenerateNetIds();
|
||||
_adminManager.Initialize();
|
||||
_entityScreenshotGenerator.Initialize(); // Corvax-Wiki
|
||||
_screenshotHook.Initialize();
|
||||
_fullscreenHook.Initialize();
|
||||
_changelogManager.Initialize();
|
||||
@@ -171,6 +177,7 @@ namespace Content.Client.Entry
|
||||
_userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme));
|
||||
_documentParsingManager.Initialize();
|
||||
_titleWindowManager.Initialize();
|
||||
_feedbackManager.Initialize();
|
||||
|
||||
_baseClient.RunLevelChanged += (_, args) =>
|
||||
{
|
||||
@@ -184,6 +191,9 @@ namespace Content.Client.Entry
|
||||
// Disable engine-default viewport since we use our own custom viewport control.
|
||||
_userInterfaceManager.MainViewport.Visible = false;
|
||||
|
||||
if (_entityScreenshotGenerator.PostInit()) // Corvax-Wiki
|
||||
return;
|
||||
|
||||
SwitchToDefaultState();
|
||||
}
|
||||
|
||||
@@ -223,6 +233,8 @@ namespace Content.Client.Entry
|
||||
|
||||
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
|
||||
{
|
||||
_entityScreenshotGenerator.Update(); // Corvax-Wiki
|
||||
|
||||
if (level == ModUpdateLevel.FramePreEngine)
|
||||
{
|
||||
_debugMonitorManager.FrameUpdate();
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class ClientFeedbackManager : SharedFeedbackManager
|
||||
{
|
||||
/// <summary>
|
||||
/// A read-only set representing the currently displayed feedback popups.
|
||||
/// </summary>
|
||||
public override IReadOnlySet<ProtoId<FeedbackPopupPrototype>> DisplayedPopups => _displayedPopups;
|
||||
|
||||
private readonly HashSet<ProtoId<FeedbackPopupPrototype>> _displayedPopups = [];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
NetManager.RegisterNetMessage<FeedbackPopupMessage>(ReceivedPopupMessage);
|
||||
NetManager.RegisterNetMessage<OpenFeedbackPopupMessage>(_ => Open());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the feedback popup window.
|
||||
/// </summary>
|
||||
public void Open()
|
||||
{
|
||||
InvokeDisplayedPopupsChanged(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Display(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
|
||||
{
|
||||
if (prototypes == null || !NetManager.IsClient)
|
||||
return;
|
||||
|
||||
var count = _displayedPopups.Count;
|
||||
_displayedPopups.UnionWith(prototypes);
|
||||
InvokeDisplayedPopupsChanged(_displayedPopups.Count > count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Remove(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
|
||||
{
|
||||
if (!NetManager.IsClient)
|
||||
return;
|
||||
|
||||
if (prototypes == null)
|
||||
{
|
||||
_displayedPopups.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayedPopups.ExceptWith(prototypes);
|
||||
}
|
||||
|
||||
InvokeDisplayedPopupsChanged(false);
|
||||
}
|
||||
|
||||
private void ReceivedPopupMessage(FeedbackPopupMessage message)
|
||||
{
|
||||
if (message.Remove)
|
||||
{
|
||||
Remove(message.FeedbackPrototypes);
|
||||
return;
|
||||
}
|
||||
|
||||
Display(message.FeedbackPrototypes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
MinHeight="100">
|
||||
<PanelContainer StyleClasses="BackgroundPanel" ModulateSelfOverride="#2b2b31"/>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<!-- Title -->
|
||||
<PanelContainer StyleIdentifier="FeedbackBorderThinBottom">
|
||||
<RichTextLabel Name="TitleLabel" Margin="12 6 6 6" />
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Description -->
|
||||
<RichTextLabel Name="DescriptionLabel" StyleClasses="LabelLight" Margin="12 4 12 8" VerticalExpand="True"/>
|
||||
|
||||
<!-- Footer -->
|
||||
<PanelContainer StyleIdentifier="FeedbackBorderThinTop">
|
||||
<BoxContainer>
|
||||
<Label FontColorOverride="#b1b1b2" StyleClasses="LabelSmall" Name="TypeLabel" Margin="14 6 6 6" />
|
||||
<Button Name="LinkButton" Text="{Loc feedbackpopup-control-button-text}" MinWidth="80"
|
||||
Margin="8 6 14 6" HorizontalExpand="True" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class FeedbackEntry : Control
|
||||
{
|
||||
private readonly IUriOpener _uri;
|
||||
|
||||
private readonly FeedbackPopupPrototype? _prototype;
|
||||
|
||||
public FeedbackEntry(ProtoId<FeedbackPopupPrototype> popupProto, IPrototypeManager proto, IUriOpener uri)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_uri = uri;
|
||||
|
||||
_prototype = proto.Index(popupProto);
|
||||
|
||||
// Title
|
||||
TitleLabel.Text = _prototype.Title;
|
||||
DescriptionLabel.Text = _prototype.Description;
|
||||
TypeLabel.Text = _prototype.ResponseType;
|
||||
|
||||
LinkButton.Visible = !string.IsNullOrEmpty(_prototype.ResponseLink);
|
||||
|
||||
// link button
|
||||
if (!string.IsNullOrEmpty(_prototype.ResponseLink))
|
||||
{
|
||||
LinkButton.OnPressed += OnButtonPressed;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_prototype?.ResponseLink))
|
||||
_uri.OpenUri(_prototype.ResponseLink);
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
// magic
|
||||
TitleLabel.SetWidth = Width - TitleLabel.Margin.SumHorizontal;
|
||||
TitleLabel.InvalidateArrange();
|
||||
DescriptionLabel.SetWidth = Width - DescriptionLabel.Margin.SumHorizontal;
|
||||
DescriptionLabel.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class FeedbackPopupSheetlet : Sheetlet<PalettedStylesheet>
|
||||
{
|
||||
public override StyleRule[] GetRules(PalettedStylesheet sheet, object config)
|
||||
{
|
||||
var borderTop = new StyleBoxFlat()
|
||||
{
|
||||
BorderColor = sheet.SecondaryPalette.Base,
|
||||
BorderThickness = new Thickness(0, 1, 0, 0),
|
||||
};
|
||||
|
||||
var borderBottom = new StyleBoxFlat()
|
||||
{
|
||||
BorderColor = sheet.SecondaryPalette.Base,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||
};
|
||||
|
||||
return
|
||||
[
|
||||
E<PanelContainer>()
|
||||
.Identifier("FeedbackBorderThinTop")
|
||||
.Prop(PanelContainer.StylePropertyPanel, borderTop),
|
||||
E<PanelContainer>()
|
||||
.Identifier("FeedbackBorderThinBottom")
|
||||
.Prop(PanelContainer.StylePropertyPanel, borderBottom),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
/// <summary>
|
||||
/// This handles getting feedback popup messages from the server and making a popup in the client.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class FeedbackPopupUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = null!;
|
||||
[Dependency] private readonly IUriOpener _uri = null!;
|
||||
|
||||
private FeedbackPopupWindow _window = null!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_window = new FeedbackPopupWindow(_proto, _uri);
|
||||
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
SubscribeNetworkEvent<RoundEndMessageEvent>(OnRoundEnd);
|
||||
|
||||
_feedbackManager.DisplayedPopupsChanged += OnPopupsChanged;
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
if (_window.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEnd(RoundEndMessageEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
// Add round end prototypes.
|
||||
var roundEndPrototypes = _feedbackManager.GetOriginFeedbackPrototypes(true);
|
||||
if (roundEndPrototypes.Count == 0)
|
||||
return;
|
||||
|
||||
_feedbackManager.Display(roundEndPrototypes);
|
||||
|
||||
// Even if no new prototypes were added, we still want to open the window.
|
||||
if (!_window.IsOpen)
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
private void OnPopupsChanged(bool newPopups)
|
||||
{
|
||||
UpdateWindow(_feedbackManager.DisplayedPopups);
|
||||
|
||||
if (newPopups && !_window.IsOpen)
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
|
||||
{
|
||||
UpdateWindow(_feedbackManager.DisplayedPopups);
|
||||
}
|
||||
|
||||
private void UpdateWindow(IReadOnlyCollection<ProtoId<FeedbackPopupPrototype>> prototypes)
|
||||
{
|
||||
_window.Update(prototypes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc feedbackpopup-window-name}" MinSize="510 460" RectClipContent="True">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<!-- main box area -->
|
||||
<BoxContainer Margin="12 12 12 5" VerticalExpand="True">
|
||||
<PanelContainer HorizontalExpand="True" StyleClasses="PanelDark">
|
||||
<ScrollContainer HorizontalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Name="NotificationContainer" HorizontalExpand="True" Orientation="Vertical" Margin="10" SeparationOverride="10" />
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical" SetHeight="30" Margin="2 0 0 0">
|
||||
<BoxContainer SetHeight="33" Margin="10 0 10 5">
|
||||
<Label Text="{Loc feedbackpopup-control-ui-footer}" Margin="6 0" StyleClasses="PdaContentFooterText"/>
|
||||
<Label Name="NumNotifications" Margin="6 0" HorizontalExpand="True" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,49 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class FeedbackPopupWindow : FancyWindow
|
||||
{
|
||||
private readonly IPrototypeManager _proto;
|
||||
private readonly IUriOpener _uri;
|
||||
|
||||
public FeedbackPopupWindow(IPrototypeManager proto, IUriOpener uri)
|
||||
{
|
||||
_proto = proto;
|
||||
_uri = uri;
|
||||
RobustXamlLoader.Load(this);
|
||||
DisplayNoEntryLabel();
|
||||
}
|
||||
|
||||
public void Update(IReadOnlyCollection<ProtoId<FeedbackPopupPrototype>> prototypes)
|
||||
{
|
||||
NotificationContainer.RemoveAllChildren();
|
||||
|
||||
if (prototypes.Count == 0)
|
||||
DisplayNoEntryLabel();
|
||||
|
||||
foreach (var proto in prototypes)
|
||||
{
|
||||
NotificationContainer.AddChild(new FeedbackEntry(proto, _proto, _uri));
|
||||
}
|
||||
|
||||
NumNotifications.Text = Loc.GetString("feedbackpopup-control-total-surveys", ("num", prototypes.Count));
|
||||
}
|
||||
|
||||
private void DisplayNoEntryLabel()
|
||||
{
|
||||
NotificationContainer.AddChild(new Label()
|
||||
{
|
||||
Text = Loc.GetString("feedbackpopup-control-no-entries"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ using Content.Client.Chemistry.EntitySystems;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.ControlExtensions;
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Contraband;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Metabolism;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -132,17 +133,18 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
|
||||
#region Effects
|
||||
if (_chemistryGuideData.ReagentGuideRegistry.TryGetValue(reagent.ID, out var guideEntryRegistry) &&
|
||||
guideEntryRegistry.GuideEntries != null &&
|
||||
guideEntryRegistry.GuideEntries.Values.Any(pair => pair.EffectDescriptions.Any()))
|
||||
guideEntryRegistry.GuideEntries.Values.Any(pair => pair.EffectDescriptions.Any() || pair.Metabolites?.Any() == true))
|
||||
{
|
||||
EffectsDescriptionContainer.Children.Clear();
|
||||
foreach (var (group, effect) in guideEntryRegistry.GuideEntries)
|
||||
foreach (var (stage, effect) in guideEntryRegistry.GuideEntries)
|
||||
{
|
||||
if (!effect.EffectDescriptions.Any())
|
||||
var hasMetabolites = effect.Metabolites?.Any() == true;
|
||||
if (!effect.EffectDescriptions.Any() && !hasMetabolites)
|
||||
continue;
|
||||
|
||||
var groupLabel = new RichTextLabel();
|
||||
groupLabel.SetMarkup(Loc.GetString("guidebook-reagent-effects-metabolism-group-rate",
|
||||
("group", _prototype.Index<MetabolismGroupPrototype>(group).LocalizedName), ("rate", effect.MetabolismRate)));
|
||||
groupLabel.SetMarkup(Loc.GetString("guidebook-reagent-effects-metabolism-stage-rate",
|
||||
("stage", _prototype.Index<MetabolismStagePrototype>(stage).LocalizedName), ("rate", effect.MetabolismRate)));
|
||||
var descriptionLabel = new RichTextLabel
|
||||
{
|
||||
Margin = new Thickness(25, 0, 10, 0)
|
||||
@@ -155,9 +157,20 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
|
||||
{
|
||||
descMsg.AddMarkupOrThrow(effectString);
|
||||
i++;
|
||||
if (i < descriptionsCount)
|
||||
if (i < descriptionsCount || hasMetabolites)
|
||||
descMsg.PushNewline();
|
||||
}
|
||||
if (hasMetabolites)
|
||||
{
|
||||
var metabolites = new List<string>();
|
||||
foreach (var (metabolite, ratio) in effect.Metabolites!)
|
||||
{
|
||||
metabolites.Add(Loc.GetString("guidebook-reagent-effects-metabolite-item", ("rate", (double)ratio), ("reagent", _prototype.Index(metabolite).LocalizedName)));
|
||||
}
|
||||
metabolites.Sort();
|
||||
|
||||
descMsg.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-effects-metabolites", ("items", ContentLocalizationManager.FormatList(metabolites))));
|
||||
}
|
||||
descriptionLabel.SetMessage(descMsg);
|
||||
|
||||
EffectsDescriptionContainer.AddChild(groupLabel);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Content.Client.Guidebook.RichText;
|
||||
|
||||
/// <summary>
|
||||
/// RichText tag that can display values extracted from entity prototypes.
|
||||
/// In order to be accessed by this tag, the desired field/property must
|
||||
/// To be accessed by this tag, the desired field/property must
|
||||
/// be tagged with <see cref="Shared.Guidebook.GuidebookDataAttribute"/>.
|
||||
/// </summary>
|
||||
public sealed class ProtodataTag : IMarkupTagHandler
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Numerics;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
@@ -30,6 +31,7 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly IPrototypeManager _prototypes;
|
||||
private readonly IResourceCache _cache;
|
||||
private readonly DamageableSystem _damageable;
|
||||
|
||||
public HealthAnalyzerControl()
|
||||
{
|
||||
@@ -40,6 +42,7 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
_spriteSystem = _entityManager.System<SpriteSystem>();
|
||||
_prototypes = dependencies.Resolve<IPrototypeManager>();
|
||||
_cache = dependencies.Resolve<IResourceCache>();
|
||||
_damageable = _entityManager.System<DamageableSystem>();
|
||||
}
|
||||
|
||||
public void Populate(HealthAnalyzerUiState state)
|
||||
@@ -79,9 +82,9 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
NameLabel.SetMessage(name);
|
||||
|
||||
SpeciesLabel.Text =
|
||||
_entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
|
||||
out var humanoidAppearanceComponent)
|
||||
? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
|
||||
_entityManager.TryGetComponent<HumanoidProfileComponent>(target.Value,
|
||||
out var humanoidComponent)
|
||||
? Loc.GetString(_prototypes.Index(humanoidComponent.Species).Name)
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
|
||||
|
||||
// Basic Diagnostic
|
||||
@@ -101,7 +104,7 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
|
||||
// Total Damage
|
||||
|
||||
DamageLabel.Text = damageable.TotalDamage.ToString();
|
||||
DamageLabel.Text = _damageable.GetTotalDamage(target.Value).ToString();
|
||||
|
||||
// Alerts
|
||||
|
||||
@@ -132,10 +135,11 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
// Damage Groups
|
||||
|
||||
var damageSortedGroups =
|
||||
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
|
||||
_damageable.GetDamagePerGroup(target.Value)
|
||||
.OrderByDescending(damage => damage.Value)
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
|
||||
var damagePerType = _damageable.GetAllDamage(target.Value).DamageDict;
|
||||
|
||||
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
|
||||
}
|
||||
@@ -152,8 +156,8 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
|
||||
}
|
||||
|
||||
private void DrawDiagnosticGroups(
|
||||
Dictionary<string, FixedPoint2> groups,
|
||||
IReadOnlyDictionary<string, FixedPoint2> damageDict)
|
||||
Dictionary<ProtoId<DamageGroupPrototype>, FixedPoint2> groups,
|
||||
IReadOnlyDictionary<ProtoId<DamageTypePrototype>, FixedPoint2> damageDict)
|
||||
{
|
||||
GroupsContainer.RemoveAllChildren();
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersSystem
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HideableHumanoidLayersComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<HideableHumanoidLayersComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnComponentInit(Entity<HideableHumanoidLayersComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
UpdateSprite(ent);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<HideableHumanoidLayersComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateSprite(ent);
|
||||
}
|
||||
|
||||
public override void SetLayerOcclusion(
|
||||
Entity<HideableHumanoidLayersComponent?> ent,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags source)
|
||||
{
|
||||
base.SetLayerOcclusion(ent, layer, visible, source);
|
||||
|
||||
if (Resolve(ent, ref ent.Comp))
|
||||
UpdateSprite((ent, ent.Comp));
|
||||
}
|
||||
|
||||
private void UpdateSprite(Entity<HideableHumanoidLayersComponent> ent)
|
||||
{
|
||||
foreach (var item in ent.Comp.LastHiddenLayers)
|
||||
{
|
||||
if (ent.Comp.HiddenLayers.ContainsKey(item))
|
||||
continue;
|
||||
|
||||
var evt = new HumanoidLayerVisibilityChangedEvent(item, true);
|
||||
RaiseLocalEvent(ent, ref evt);
|
||||
|
||||
if (!_sprite.LayerMapTryGet(ent.Owner, item, out var index, true))
|
||||
continue;
|
||||
|
||||
_sprite.LayerSetVisible(ent.Owner, index, true);
|
||||
}
|
||||
|
||||
foreach (var item in ent.Comp.HiddenLayers.Keys)
|
||||
{
|
||||
if (ent.Comp.LastHiddenLayers.Contains(item))
|
||||
continue;
|
||||
|
||||
var evt = new HumanoidLayerVisibilityChangedEvent(item, false);
|
||||
RaiseLocalEvent(ent, ref evt);
|
||||
|
||||
if (!_sprite.LayerMapTryGet(ent.Owner, item, out var index, true))
|
||||
continue;
|
||||
|
||||
_sprite.LayerSetVisible(ent.Owner, index, false);
|
||||
}
|
||||
|
||||
ent.Comp.LastHiddenLayers.Clear();
|
||||
ent.Comp.LastHiddenLayers.UnionWith(ent.Comp.HiddenLayers.Keys);
|
||||
}
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
using Content.Client.DisplacementMap;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Humanoid;
|
||||
|
||||
public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly DisplacementMapSystem _displacement = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true);
|
||||
Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateSprite((uid, component, Comp<SpriteComponent>(uid)));
|
||||
}
|
||||
|
||||
private void OnCvarChanged(bool value)
|
||||
{
|
||||
var humanoidQuery = AllEntityQuery<HumanoidAppearanceComponent, SpriteComponent>();
|
||||
while (humanoidQuery.MoveNext(out var uid, out var humanoidComp, out var spriteComp))
|
||||
{
|
||||
UpdateSprite((uid, humanoidComp, spriteComp));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSprite(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
UpdateLayers(entity);
|
||||
ApplyMarkingSet(entity);
|
||||
ApplyHeight(entity.Comp1); // WL-Height
|
||||
|
||||
var humanoidAppearance = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
sprite[_sprite.LayerMapReserve((entity.Owner, sprite), HumanoidVisualLayers.Eyes)].Color = humanoidAppearance.EyeColor;
|
||||
}
|
||||
|
||||
private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
|
||||
=> humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);
|
||||
|
||||
private void UpdateLayers(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var component = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
var oldLayers = new HashSet<HumanoidVisualLayers>(component.BaseLayers.Keys);
|
||||
component.BaseLayers.Clear();
|
||||
|
||||
// add default species layers
|
||||
var speciesProto = _prototypeManager.Index(component.Species);
|
||||
var baseSprites = _prototypeManager.Index(speciesProto.SpriteSet);
|
||||
foreach (var (key, id) in baseSprites.Sprites)
|
||||
{
|
||||
oldLayers.Remove(key);
|
||||
if (!component.CustomBaseLayers.ContainsKey(key))
|
||||
SetLayerData(entity, key, id, sexMorph: true);
|
||||
}
|
||||
|
||||
// add custom layers
|
||||
foreach (var (key, info) in component.CustomBaseLayers)
|
||||
{
|
||||
oldLayers.Remove(key);
|
||||
SetLayerData(entity, key, info.Id, sexMorph: false, color: info.Color);
|
||||
}
|
||||
|
||||
// hide old layers
|
||||
// TODO maybe just remove them altogether?
|
||||
foreach (var key in oldLayers)
|
||||
{
|
||||
if (_sprite.LayerMapTryGet((entity.Owner, sprite), key, out var index, false))
|
||||
sprite[index].Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLayerData(
|
||||
Entity<HumanoidAppearanceComponent, SpriteComponent> entity,
|
||||
HumanoidVisualLayers key,
|
||||
string? protoId,
|
||||
bool sexMorph = false,
|
||||
Color? color = null)
|
||||
{
|
||||
var component = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
var layerIndex = _sprite.LayerMapReserve((entity.Owner, sprite), key);
|
||||
var layer = sprite[layerIndex];
|
||||
layer.Visible = !IsHidden(component, key);
|
||||
|
||||
if (color != null)
|
||||
layer.Color = color.Value;
|
||||
|
||||
if (protoId == null)
|
||||
return;
|
||||
|
||||
if (sexMorph)
|
||||
protoId = HumanoidVisualLayersExtension.GetSexMorph(key, component.Sex, protoId);
|
||||
|
||||
var proto = _prototypeManager.Index<HumanoidSpeciesSpriteLayer>(protoId);
|
||||
component.BaseLayers[key] = proto;
|
||||
|
||||
if (proto.MatchSkin)
|
||||
layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha);
|
||||
|
||||
if (proto.BaseSprite != null)
|
||||
_sprite.LayerSetSprite((entity.Owner, sprite), layerIndex, proto.BaseSprite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a profile directly into a humanoid.
|
||||
/// </summary>
|
||||
/// <param name="uid">The humanoid entity's UID</param>
|
||||
/// <param name="profile">The profile to load.</param>
|
||||
/// <param name="humanoid">The humanoid entity's humanoid component.</param>
|
||||
/// <remarks>
|
||||
/// This should not be used if the entity is owned by the server. The server will otherwise
|
||||
/// override this with the appearance data it sends over.
|
||||
/// </remarks>
|
||||
public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref humanoid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var customBaseLayers = new Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo>();
|
||||
|
||||
var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(profile.Species);
|
||||
var markings = new MarkingSet(speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
|
||||
|
||||
// Add markings that doesn't need coloring. We store them until we add all other markings that doesn't need it.
|
||||
var markingFColored = new Dictionary<Marking, MarkingPrototype>();
|
||||
foreach (var marking in profile.Appearance.Markings)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
if (!prototype.ForcedColoring)
|
||||
{
|
||||
markings.AddBack(prototype.MarkingCategory, marking);
|
||||
}
|
||||
else
|
||||
{
|
||||
markingFColored.Add(marking, prototype);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// legacy: remove in the future?
|
||||
//markings.RemoveCategory(MarkingCategories.Hair);
|
||||
//markings.RemoveCategory(MarkingCategories.FacialHair);
|
||||
|
||||
// We need to ensure hair before applying it or coloring can try depend on markings that can be invalid
|
||||
var hairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.Hair, out var hairAlpha, _prototypeManager)
|
||||
? profile.Appearance.SkinColor.WithAlpha(hairAlpha)
|
||||
: profile.Appearance.HairColor;
|
||||
var hair = new Marking(profile.Appearance.HairStyleId,
|
||||
new[] { hairColor });
|
||||
|
||||
var facialHairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.FacialHair, out var facialHairAlpha, _prototypeManager)
|
||||
? profile.Appearance.SkinColor.WithAlpha(facialHairAlpha)
|
||||
: profile.Appearance.FacialHairColor;
|
||||
var facialHair = new Marking(profile.Appearance.FacialHairStyleId,
|
||||
new[] { facialHairColor });
|
||||
|
||||
if (_markingManager.CanBeApplied(profile.Species, profile.Sex, hair, _prototypeManager))
|
||||
{
|
||||
markings.AddBack(MarkingCategories.Hair, hair);
|
||||
}
|
||||
if (_markingManager.CanBeApplied(profile.Species, profile.Sex, facialHair, _prototypeManager))
|
||||
{
|
||||
markings.AddBack(MarkingCategories.FacialHair, facialHair);
|
||||
}
|
||||
|
||||
// Finally adding marking with forced colors
|
||||
foreach (var (marking, prototype) in markingFColored)
|
||||
{
|
||||
var markingColors = MarkingColoring.GetMarkingLayerColors(
|
||||
prototype,
|
||||
profile.Appearance.SkinColor,
|
||||
profile.Appearance.EyeColor,
|
||||
markings
|
||||
);
|
||||
markings.AddBack(prototype.MarkingCategory, new Marking(marking.MarkingId, markingColors));
|
||||
}
|
||||
|
||||
markings.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _prototypeManager);
|
||||
markings.EnsureSexes(profile.Sex, _markingManager);
|
||||
markings.EnsureDefault(
|
||||
profile.Appearance.SkinColor,
|
||||
profile.Appearance.EyeColor,
|
||||
_markingManager);
|
||||
|
||||
DebugTools.Assert(IsClientSide(uid));
|
||||
|
||||
humanoid.MarkingSet = markings;
|
||||
humanoid.PermanentlyHidden = new HashSet<HumanoidVisualLayers>();
|
||||
humanoid.HiddenLayers = new Dictionary<HumanoidVisualLayers, SlotFlags>();
|
||||
humanoid.CustomBaseLayers = customBaseLayers;
|
||||
humanoid.Sex = profile.Sex;
|
||||
humanoid.Gender = profile.Gender;
|
||||
humanoid.Age = profile.Age;
|
||||
humanoid.Height = profile.Height; // WL-Height
|
||||
humanoid.Species = profile.Species;
|
||||
humanoid.SkinColor = profile.Appearance.SkinColor;
|
||||
humanoid.EyeColor = profile.Appearance.EyeColor;
|
||||
|
||||
UpdateSprite((uid, humanoid, Comp<SpriteComponent>(uid)));
|
||||
}
|
||||
|
||||
private void ApplyMarkingSet(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
// I am lazy and I CBF resolving the previous mess, so I'm just going to nuke the markings.
|
||||
// Really, markings should probably be a separate component altogether.
|
||||
ClearAllMarkings(entity);
|
||||
|
||||
var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) ||
|
||||
_configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity);
|
||||
// The reason we're splitting this up is in case the character already has undergarment equipped in that slot.
|
||||
var applyUndergarmentTop = censorNudity;
|
||||
var applyUndergarmentBottom = censorNudity;
|
||||
|
||||
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var markingPrototype))
|
||||
{
|
||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, entity);
|
||||
if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop)
|
||||
applyUndergarmentTop = false;
|
||||
else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom)
|
||||
applyUndergarmentBottom = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
|
||||
|
||||
AddUndergarments(entity, applyUndergarmentTop, applyUndergarmentBottom);
|
||||
}
|
||||
|
||||
private void ClearAllMarkings(Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
foreach (var markingList in humanoid.ClientOldMarkings.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
RemoveMarking(marking, (entity, sprite));
|
||||
}
|
||||
}
|
||||
|
||||
humanoid.ClientOldMarkings.Clear();
|
||||
|
||||
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
RemoveMarking(marking, (entity, sprite));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveMarking(Marking marking, Entity<SpriteComponent> spriteEnt)
|
||||
{
|
||||
if (!_markingManager.TryGetMarking(marking, out var prototype))
|
||||
return;
|
||||
|
||||
foreach (var sprite in prototype.Sprites)
|
||||
{
|
||||
if (sprite is not SpriteSpecifier.Rsi rsi)
|
||||
continue;
|
||||
|
||||
var layerId = $"{marking.MarkingId}-{rsi.RsiState}";
|
||||
if (!_sprite.LayerMapTryGet(spriteEnt.AsNullable(), layerId, out var index, false))
|
||||
continue;
|
||||
|
||||
_sprite.LayerMapRemove(spriteEnt.AsNullable(), layerId);
|
||||
_sprite.RemoveLayer(spriteEnt.AsNullable(), index);
|
||||
|
||||
// If this marking is one that can be displaced, we need to remove the displacement as well; otherwise
|
||||
// altering a marking at runtime can lead to the renderer falling over.
|
||||
// The Vulps must be shaved.
|
||||
// (https://github.com/space-wizards/space-station-14/issues/40135).
|
||||
if (prototype.CanBeDisplaced)
|
||||
_displacement.EnsureDisplacementIsNotOnSprite(spriteEnt, layerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddUndergarments(Entity<HumanoidAppearanceComponent, SpriteComponent> entity, bool undergarmentTop, bool undergarmentBottom)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
|
||||
if (undergarmentTop && humanoid.UndergarmentTop != null)
|
||||
{
|
||||
var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { new Color() });
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
// Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
|
||||
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List<Marking> { marking });
|
||||
ApplyMarking(prototype, null, true, entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (undergarmentBottom && humanoid.UndergarmentBottom != null)
|
||||
{
|
||||
var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking> { marking });
|
||||
ApplyMarking(prototype, null, true, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMarking(MarkingPrototype markingPrototype,
|
||||
IReadOnlyList<Color>? colors,
|
||||
bool visible,
|
||||
Entity<HumanoidAppearanceComponent, SpriteComponent> entity)
|
||||
{
|
||||
var humanoid = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
if (!_sprite.LayerMapTryGet((entity.Owner, sprite), markingPrototype.BodyPart, out var targetLayer, false))
|
||||
return;
|
||||
|
||||
visible &= !IsHidden(humanoid, markingPrototype.BodyPart);
|
||||
visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting)
|
||||
&& setting.AllowsMarkings;
|
||||
|
||||
for (var j = 0; j < markingPrototype.Sprites.Count; j++)
|
||||
{
|
||||
var markingSprite = markingPrototype.Sprites[j];
|
||||
|
||||
if (markingSprite is not SpriteSpecifier.Rsi rsi)
|
||||
return;
|
||||
|
||||
var layerId = $"{markingPrototype.ID}-{rsi.RsiState}";
|
||||
|
||||
if (!_sprite.LayerMapTryGet((entity.Owner, sprite), layerId, out _, false))
|
||||
{
|
||||
var layer = _sprite.AddLayer((entity.Owner, sprite), markingSprite, targetLayer + j + 1);
|
||||
_sprite.LayerMapSet((entity.Owner, sprite), layerId, layer);
|
||||
_sprite.LayerSetSprite((entity.Owner, sprite), layerId, rsi);
|
||||
}
|
||||
|
||||
_sprite.LayerSetVisible((entity.Owner, sprite), layerId, visible);
|
||||
|
||||
if (!visible || setting == null) // this is kinda implied
|
||||
continue;
|
||||
|
||||
// Okay so if the marking prototype is modified but we load old marking data this may no longer be valid
|
||||
// and we need to check the index is correct.
|
||||
// So if that happens just default to white?
|
||||
if (colors != null && j < colors.Count)
|
||||
_sprite.LayerSetColor((entity.Owner, sprite), layerId, colors[j]);
|
||||
else
|
||||
_sprite.LayerSetColor((entity.Owner, sprite), layerId, Color.White);
|
||||
|
||||
if (humanoid.MarkingsDisplacement.TryGetValue(markingPrototype.BodyPart, out var displacementData) && markingPrototype.CanBeDisplaced)
|
||||
_displacement.TryAddDisplacement(displacementData, (entity.Owner, sprite), targetLayer + j + 1, layerId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetSkinColor(EntityUid uid, Color skinColor, bool sync = true, bool verify = true, HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (!Resolve(uid, ref humanoid) || humanoid.SkinColor == skinColor)
|
||||
return;
|
||||
|
||||
base.SetSkinColor(uid, skinColor, false, verify, humanoid);
|
||||
|
||||
if (!TryComp(uid, out SpriteComponent? sprite))
|
||||
return;
|
||||
|
||||
foreach (var (layer, spriteInfo) in humanoid.BaseLayers)
|
||||
{
|
||||
if (!spriteInfo.MatchSkin)
|
||||
continue;
|
||||
|
||||
var index = _sprite.LayerMapReserve((uid, sprite), layer);
|
||||
sprite[index].Color = skinColor.WithAlpha(spriteInfo.LayerAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayerVisibility(
|
||||
Entity<HumanoidAppearanceComponent> ent,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags? slot,
|
||||
ref bool dirty)
|
||||
{
|
||||
base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
|
||||
|
||||
var sprite = Comp<SpriteComponent>(ent);
|
||||
if (!_sprite.LayerMapTryGet((ent.Owner, sprite), layer, out var index, false))
|
||||
{
|
||||
if (!visible)
|
||||
return;
|
||||
index = _sprite.LayerMapReserve((ent.Owner, sprite), layer);
|
||||
}
|
||||
|
||||
var spriteLayer = sprite[index];
|
||||
if (spriteLayer.Visible == visible)
|
||||
return;
|
||||
|
||||
spriteLayer.Visible = visible;
|
||||
|
||||
// I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
|
||||
// Just a week away...
|
||||
|
||||
foreach (var markingList in ent.Comp.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
|
||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, (ent, ent.Comp, sprite));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user