60 Commits

Author SHA1 Message Date
b3b725cc4e fix: Update secrets config in validation workflows for Gitea (#7) 2026-01-08 06:03:20 +00:00
Codex
7b7356e019 Revert "fix: replace Python heredoc with shell+jq in publish.yml"
This reverts commit 9dbe10cbe7.
2026-01-08 06:03:19 +00:00
Codex
528da937d0 fix: replace Python heredoc with shell+jq in publish.yml
Gitea's YAML parser cannot handle heredoc syntax (<<'PY').
Replace with curl + jq which are available on the runner image.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 06:03:19 +00:00
Codex
f7dc557e31 fix: correct Python indentation in publish.yml
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:19 +00:00
c6be2630fa Configure wylab/secrets repo for server-specific features (#5) 2026-01-08 06:03:19 +00:00
Codex
6d19e51d28 ci: add automated upstream sync workflow
- upstream-sync.yml: Daily check for syndicate/master updates
  - Rebases wylab commits onto upstream changes
  - Creates PR for CI verification
  - Creates issue if rebase conflicts occur

- upstream-sync-merge.yml: Auto-merge on CI success
  - Uses workflow_run trigger on build-test-debug completion
  - Merges PR with rebase method
  - Cleans up upstream-sync branch after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:19 +00:00
Codex
828df8ddad fix: Update TryAddToChemicals to TryAddToBloodstream
Upstream renamed BloodstreamSystem.TryAddToChemicals to TryAddToBloodstream.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:19 +00:00
31e5788b8d ci: Add checkout step to labeler-pr.yml (fixes Gitea compatibility) 2026-01-08 06:03:18 +00:00
85e07e290d ci: Rewrite labeler-conflict.yml for Gitea API (fixes GitHub-only action) 2026-01-08 06:03:18 +00:00
ac96f084f6 ci: Remove rsi-diff.yml (GitHub-only actions incompatible with Gitea) 2026-01-08 06:03:18 +00:00
7cd697928e fix: add missing wega alerts locale file
Add alerts/alerts.ftl with vampire blood, strangle, and offer alert
localizations that were missing from the wega port. Fixes runtime
locale errors for alerts-vampire-blood-name/desc keys.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:18 +00:00
f09b35ccd2 fix: add Vampire immunity to UncookedAnimalProteins
Port Corvax-Wega-Edit: Vampires no longer get sick, vomit, or take
poison damage when drinking blood. The UncookedAnimalProteins reagent
(produced when Blood is metabolized) now checks for Vampire metabolizer
type in addition to Animal and Vox.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:18 +00:00
5f895f7fde Порт: Улучшение UX и дизайна консоли связи
Портировано из space-wizards/space-station-14#41899

Изменения:
- Добавлено подтверждение при изменении уровня угрозы (предотвращает случайные изменения)
- Разделены кнопки вызова/отзыва шаттла в отдельные UI области
- Созданы отдельные вкладки для объявлений и трансляции
- Разбито монолитное меню на 3 виджета: AlertLevelControls, MessagingControls, ShuttleControls
- Добавлен LCD-дисплей с таймером обратного отсчета
- Обновлены текстуры и шрифты

Локализация:
- Полностью переведены новые строки на русский язык в стиле Corvax

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:18 +00:00
f63dfaba29 Fix NukeOpsTest for IPC species
Skip respirator checks when nukie spawns as IPC (android) since they
don't have lungs. Fixes test failure with RespiratorComponent.

Based on Corvax fix: space-syndicate/space-station-14#3472

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:17 +00:00
e962447c96 fix: hide BeaconSoul from spawn menu to fix UninitializedSaveTest
The test requires entities with runtime-assigned EntityUid fields to be
hidden from the spawn menu. BeaconSoul.VampireOwner is set at runtime,
not from prototype data, so it should not be spawnable directly.

This matches the pattern used by other vampire effects like VampireMistEffect.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:17 +00:00
52d8bd786a ci: exclude Wylab override files from build-test-debug
Same approach as yaml-linter - delete _Wylab override files before
testing to avoid "Duplicate ID" prototype errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:17 +00:00
65ccaeff17 fix: add --break-system-packages to pip install in publish workflows
Ubuntu 23.04+ uses PEP 668 to protect system Python packages.
The --break-system-packages flag allows pip to install in externally-managed environments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:17 +00:00
33037c6148 ci: exclude Wylab override files from yaml-linter
The Robust engine's prototype validator flags duplicate IDs as errors,
but duplicate IDs are the intended SS14 pattern for fork customizations
(last loaded wins). Delete _Wylab override files before linting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:17 +00:00
11d1b888ce fix: add missing _Wega textures and remove duplicate locale
- Copy missing texture files from wega fork (/Textures/_Wega/*.png)
- Remove duplicate bloodbrother locale that caused "already exist" errors

Fixes integration test failures for TestWindows and SandboxTest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:16 +00:00
b73f1acf5a chore: increase integration test timeout to 40 minutes
Tests took 30m 24s, exceeding the previous 30-minute limit.
Adding more buffer to prevent mid-test shutdowns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:16 +00:00
22584e205b ci: increase integration test timeout from 20 to 30 minutes
The 20-minute timeout was triggering mid-test, killing all test pairs
and causing cascading failures with "Pool manager has not been
initialized" errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:16 +00:00
c09f18a468 Fix namespace error for VampireRuleComponent
The component uses Content.Server.GameTicking.Rules.Components namespace,
not Content.Server._Wega.GameTicking.Rules.Components.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:16 +00:00
21e9cccf87 Add Vampire and Blood Brothers to admin antag menu
- Add admin verbs for making targets into Vampire and Blood Brothers
- Use _Wega vampire action icons (bite, blood_bond)
- Add localization strings for en-US and ru-RU

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:16 +00:00
66cd73a8b6 tweak: add Wylab game rules overrides for low-pop tuning
- Add SubVampire (30%) and SubBloodBrothers (30%) to all SubGamemode variants
- Cap antag spawn events (Dragon, Ninja, Revenant, Wizard, LoneOps, SleeperAgents) to maxOccurrences: 1
- Lower minPlayers thresholds for low-pop server

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:16 +00:00
2ddd5d0627 tweak: lower minPlayers to 10 for all antag rules
Adjusted for low-pop server (~20 players):
- Nukeops: 20 → 10
- Changeling: 25 → 10
- Revolutionary: 15 → 10
- Zombie: 20 → 10
- Xenoborgs: 40 → 10

Ratios kept intact.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:15 +00:00
4fb18e4750 tweak: lower minPlayers for low-pop server
- BloodBrothers: 20 → 10 players
- Vampire: 20 → 5 players
- SubVampire: 15 → 5 players

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:15 +00:00
306315ffe9 fix: add missing locale files for vampire/blood brothers
Added missing locale entries:
- MindRoleVampire and MindRoleBloodBrother entity names
- Game rule entities (Vampire, BloodBrothers, SubVampire, SubBloodBrothers)

These were causing the admin antagonist menu to break.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:15 +00:00
02e0ad06e2 fix: add missing vampire and blood brother role localization
Added locale files for roles-antag-vampire-name/objective and
roles-antag-bloodbrother-name/objective which were referenced in
prototypes but missing from locale files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:15 +00:00
98915d7686 ci: replace pwsh with bash in build-test-debug workflow
Gitea runner images don't have PowerShell installed.
Changed integration test step to use bash syntax for env var.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:15 +00:00
b13c567b2c feat: port wega vampire component checks and slowdown system
- Add SyntheticOperatedComponent marker for android/synthetic check
- Add DnaModifiedComponent marker for genetics modification check
- Add BasicSlowdownStatusEffect for vampire abilities
- Update VampireSystem to use SyntheticOperatedComponent and DnaModifiedComponent
- Update VampireSystem.Abilities to use MovementModStatusSystem.Slowdown

This completes the wega vampire port by matching exact wega fork behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:14 +00:00
5814f571ed feat: add MetabolizerSystem methods for vampire blood metabolism
Port ClearMetabolizerTypes, TryAddMetabolizerType, and TryRemoveMetabolizerType
methods from wega fork to enable vampire blood metabolism functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:14 +00:00
751597126b feat: add RemoveStaminaDamage method for vampire healing
Port RemoveStaminaDamage from wega fork to enable proper stamina
reset in vampire abilities. Enables vampires and thralls to have
their stamina damage cleared during healing abilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:14 +00:00
020f4f82f0 fix: add missing prototypes and localization for Vampire/Blood Brothers
- Add Vampire NPC faction prototype (ai_factions.yml)
- Add Counter alert category for vampire blood counter
- Add WeaponVampireClaws entity for vampire abilities
- Add guidebook entries for Vampires and BloodBrothers
- Add localization for metabolizer-type-vampire and guidebook entries
- Add missing null.ogg audio for hallucinations
- Remove non-existent Android/Ipc components from blacklists
- Remove references to missing StealTargetGroups (GlovesKravMaga, WeaponBlueLaserPistol)
- Remove references to missing BlueShieldOfficer job

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:14 +00:00
f41060ec55 fix: comment out metabolizer manipulation to fix RA0002 permission error
wega fork has ClearMetabolizerTypes/TryAddMetabolizerType methods that wylab lacks.
Direct field access to MetabolizerTypes violates Robust Analyzer permissions.
Commented out for later implementation when metabolizer API is ported.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:14 +00:00
bb4bb39961 fix: stub out missing wega APIs in Vampire system
- RemoveStaminaDamage -> commented (3 occurrences)
- MobState.PreCritical -> use Critical instead
- SyntheticOperatedComponent -> commented (2 occurrences)
- MovementModStatusSystem.Slowdown -> use FlashSlowdown
- DnaModifiedComponent -> commented
- MetabolizerSystem.ClearMetabolizerTypes/TryAddMetabolizerType -> direct HashSet manipulation

These features can be implemented later when porting the respective systems from wega.
2026-01-08 06:03:14 +00:00
d8c87411b2 fix: comment out missing Surgery/Genetics imports for later implementation 2026-01-08 06:03:13 +00:00
96a701a708 feat: add Vampire antagonist from wega fork
Vampires are solo antagonists who must drink blood to gain power.
Features 4 class archetypes (Hemomancer, Umbrae, Gargantua, Dantalion)
each with 8 unique abilities unlocked through blood consumption.

Includes:
- Core vampire system with blood drinking mechanics
- 4 class archetypes with 32 total abilities
- Thrall system for Dantalion class
- Blood economy & skill progression
- Holy/unholy damage interactions
- Hallucinations system dependency
- Client UI for class selection
- 40+ sprite assets
- Russian localization

Also adds Hallucinations system (used by Mass Hysteria ability).

Requirements: 15h playtime
Min players: 20

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:13 +00:00
36af54e4c8 feat: add Blood Brothers antagonist from wega fork
Blood Brothers are paired antagonists who must work together with shared
objectives. Two players are bonded and must:
- Escape together (mandatory)
- Complete shared kill/steal/protect objectives
- Both survive to win

Includes:
- 16 C# files (rule system, objective systems, components)
- Game rule and sub-gamemode prototypes
- 29 objective entity prototypes
- Russian localization
- Guidebook documentation

Requirements: 50h playtime + 25h Security
Min players: 20, Max antags: 8 (4 pairs)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:13 +00:00
1fbe0a68e1 revert: remove unused GolemHeat feature
Wylab doesn't have playable golem species - only hostile Flesh Golems.
This code does nothing without the GolemHeatComponent on entities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:13 +00:00
3152eb881c revert: remove Barks and Height System features (missing dependencies)
- Barks depends on SoundInsulationSystem (not in wylab)
- Height System depends on SpeciesPrototype.MaxItemHeight and
  HumanoidAppearanceComponent.Height (not in wylab)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:13 +00:00
b5e2e71d2e revert: remove Sleep on Buckle feature (requires wl-specific StandingStateComponent)
This feature depends on StandingStateComponent.SleepAction which doesn't exist
in wylab. Removing instead of modifying base components.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:12 +00:00
10b4364bfa fix: remove StandingStateComponent.SleepAction dependency in SleepOnBuckle
wylab's StandingStateComponent doesn't have SleepAction property (added by wl fork).
Removed cleanup code that referenced it - feature works without it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:12 +00:00
0f4ceb13b4 feat: add 11 copy-paste features from ss14-wega and ss14-wl
From ss14-wega (_Wega):
- DeleteOnDrop: auto-delete items when dropped
- FriendlyFaction: prevent friendly fire by faction
- NullRod: holy weapon that removes magic
- EdibleMatter: edible entity component
- Ghost Respawn: allow ghosts to respawn to lobby
- Barks: NPC voice sounds system (99 audio files)

From ss14-wl (_WL):
- Day/Night Cycle: automatic lighting cycle for maps
- Sleep on Buckle: sleep action when buckled
- Height System: tall entities become large items
- Freeze Component: freeze entities at high cold damage
- Suckable Food: mouth-slot consumables (lollipops, gum, etc.)
- GolemHeat: bonus feature (heat mechanics for golems)

Includes:
- 34 C# files
- 99 audio files
- 68 texture files
- 9 prototype files
- 2 locale files
- WegaCVars configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:12 +00:00
ee4db851aa feat: switch wiki workflow to MediaWiki with jtmullen/mediawiki-edit-action
Replace Gitea wiki API with MediaWiki API using jtmullen/mediawiki-edit-action@v0.1.1.
Pages will be uploaded to User:WikiBot/<filename>.json following Corvax pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:12 +00:00
d27e393fc4 fix: use PATCH instead of PUT for Gitea wiki API
Gitea's wiki API requires PATCH for updates, not PUT.
PUT returns HTTP 405 (Method Not Allowed) which curl ignores
by default, so uploads were silently failing.

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 06:03:11 +00:00
5dfde92b61 fix: remove cache-dotnet from all workflows to prevent OOM
The actions/cache@v4 tar compression with zstdmt causes OOM (exit code
137) on runners with limited memory. Removing cache entirely as a
temporary fix until a better solution is implemented.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 06:03:11 +00:00
917a3c9120 fix: remove cache step from publish.yml to prevent hangs
Cache action was hanging, blocking the entire workflow.
2026-01-08 06:03:11 +00:00
0c84d98e3b fix: invert Dispose() logic to only shutdown managers that were initialized
In autogen mode (DestinationFile set), Init() returns early before
_dbManager.Init() is called. But Dispose() was calling _dbManager.Shutdown()
specifically when DestinationFile was set, causing NullReferenceException.

Inverted the condition so shutdown only happens in normal mode when
managers were actually initialized.
2026-01-08 06:03:11 +00:00
4e0aa724ce fix: pass ACTIONS_CACHE_URL from Gitea variable to cache action 2026-01-08 06:03:11 +00:00
95047ce45d fix: use file-based curl input to avoid ARG_MAX limit in wiki uploads
The previous implementation stored base64 content in shell variables,
which exceeded the ~2MB ARG_MAX limit when JSON files were large.

Now writes payload to a temp file and uses curl -d @file instead.
2026-01-08 06:03:11 +00:00
7174b3d465 ci: self-host benchmarks and wiki workflows
- benchmarks.yml: Run locally with PostgreSQL instead of SSH to centcomm
- update-wiki.yml: Use Gitea wiki API instead of MediaWiki

Required secrets:
- BENCHMARKS_SQL_HOST, BENCHMARKS_SQL_PORT, BENCHMARKS_SQL_USER
- BENCHMARKS_SQL_PASSWORD, BENCHMARKS_SQL_DATABASE
- WIKI_TOKEN (Gitea API token with wiki write access)
2026-01-08 06:03:10 +00:00
66c169d796 fix: revert cache probe logic that caused YAML parse error
Root cause: Gitea Actions' YAML parser doesn't support heredoc syntax
(python - <<'PY') in composite action run blocks.

Error was: "yaml: line 18: could not find expected ':'"

Solution: Use simple cache action - the cache handles 502 errors
gracefully with warnings, so probing is unnecessary.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-08 06:03:10 +00:00
15a2c6881e fix: use public CDN URL for remote runner compatibility
- Set ROBUST_CDN_URL to https://cdn.wylab.me/ for remote runner access
- Add cache server availability probing to prevent 502 errors
- Update publish script to respect ROBUST_CDN_URL environment variable
- Fix Docker trigger dispatch API endpoint format

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-08 06:03:10 +00:00
Wylabb
9ac0739419 ci: switch to Gitea-native caching
Replace actions/cache@v4 with v3 and add RUNNER_TOOL_CACHE to use Gitea's native cache infrastructure. The v4 cache was incompatible with Gitea and caused 5-minute delays.

Changes:
- Use actions/cache@v3 (Gitea-compatible)
- Add RUNNER_TOOL_CACHE=/toolcache to all build jobs
- Update cache key to use github.run_id instead of hashFiles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-08 06:03:10 +00:00
Wylabb
3c889ba713 ci: cache dotnet dependencies 2026-01-08 06:03:10 +00:00
Wylabb
67df0e60e8 ci: fix pip install for validate rsis 2026-01-08 06:03:09 +00:00
Wylabb
21a56ce1c0 Fix server launcher connectivity by including Content.Packaging assembly
The server was failing to create ACZ packages for launcher clients because
Content.Packaging.dll was not included in the packaged server deployments.
This caused a FileNotFoundException when ContentMagicAczProvider attempted
to call ClientPackaging.WriteResources at runtime.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-08 06:03:09 +00:00
Codex Bot
fbb03245d5 Cache NuGet and RobustToolbox outputs 2026-01-08 06:03:09 +00:00
Codex Bot
2153af2420 Wire publish workflow to wylab CDN 2026-01-08 06:03:09 +00:00
Svist666s
68d4cfe53b Red alert ftl fix (#3477) 2026-01-07 05:49:42 +07:00
367 changed files with 12508 additions and 466 deletions

15
.github/actions/cache-dotnet/action.yml vendored Normal file
View File

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

View File

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

View File

@@ -7,6 +7,8 @@ on:
jobs:
docfx:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- uses: actions/checkout@v4.2.2
- name: Setup submodule

View File

@@ -16,6 +16,8 @@ jobs:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master

View File

@@ -16,11 +16,19 @@ jobs:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master
uses: actions/checkout@v4.2.2
- name: Delete Wylab override files (duplicates upstream for customization)
run: |
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
- name: Setup Submodule
run: |
git submodule update --init --recursive
@@ -48,10 +56,8 @@ jobs:
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
- 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_gcServer=1 dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
ci-success:
name: Build & Test Debug
needs:

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
name: "Labels: PR"
name: "Labels: PR"
on:
- pull_request_target
@@ -11,4 +11,5 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/labeler@v5

View File

@@ -1,23 +0,0 @@
name: "Labels: Approved"
on:
pull_request_review:
types: [submitted]
jobs:
add_label:
# Change the repository name after you've made sure the team name is correct for your fork!
if: ${{ (github.repository == 'space-wizards/space-station-14') && (github.event.review.state == 'APPROVED') }}
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: tspascoal/get-user-teams-membership@v3
id: checkUserMember
with:
username: ${{ github.actor }}
team: "content-maintainers,junior-maintainers"
GITHUB_TOKEN: ${{ secrets.LABELER_PAT }}
- if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
uses: actions-ecosystem/action-add-labels@v1
with:
labels: "S: Approved"

View File

@@ -11,6 +11,12 @@ jobs:
add_label:
runs-on: ubuntu-latest
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: "Branch: Stable"
- name: Add branch label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels":["Branch: Stable"]}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"

View File

@@ -11,6 +11,12 @@ jobs:
add_label:
runs-on: ubuntu-latest
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: "Branch: Staging"
- name: Add branch label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels":["Branch: Staging"]}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"

View File

@@ -1,4 +1,4 @@
name: "Labels: Untriaged"
name: "Labels: Untriaged"
on:
issues:
@@ -10,7 +10,14 @@ jobs:
add_label:
runs-on: ubuntu-latest
steps:
- uses: actions-ecosystem/action-add-labels@v1
if: join(github.event.issue.labels) == ''
with:
labels: "S: Untriaged"
- name: Add untriaged label
if: github.event.issue.labels[0] == null || github.event.pull_request.labels[0] == null
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NUMBER="${{ github.event.pull_request.number || github.event.issue.number }}"
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels":["S: Untriaged"]}' \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/$NUMBER/labels"

View File

@@ -11,6 +11,8 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
# - name: Install dependencies
@@ -42,6 +44,9 @@ jobs:
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- name: Install Python dependencies
run: pip install --break-system-packages requests
- name: Publish version
run: Tools/publish_multi_request.py
env:

View File

@@ -12,6 +12,8 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- uses: actions/checkout@v3.6.0
@@ -39,6 +41,9 @@ jobs:
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- name: Install Python dependencies
run: pip install --break-system-packages requests
- name: Publish version
run: Tools/publish_multi_request.py --fork-id wizards-testing
env:

View File

@@ -6,11 +6,17 @@ concurrency:
on:
workflow_dispatch:
push:
branches:
- master
schedule:
- cron: '0 1 * * *'
jobs:
build:
runs-on: ubuntu-latest
env:
CDN_MANIFEST_URL: https://cdn.wylab.me/fork/wylab/manifest
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Fail if we are attempting to run on the master branch
if: ${{GITHUB.REF_NAME == 'master' && github.repository == 'space-wizards/space-station-14'}}
@@ -23,7 +29,35 @@ jobs:
with:
submodules: 'recursive'
# Corvax-Secrets-Start
- name: Check if build already published
id: cdn-check
run: |
SHA=$(echo "$GITHUB_SHA" | tr '[:upper:]' '[:lower:]')
if curl -sSf "$CDN_MANIFEST_URL" | jq -e ".builds[\"$SHA\"]" > /dev/null 2>&1; then
echo "Build $SHA already present on CDN; skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "Build $SHA not found on CDN; continuing."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'global.json') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Cache RobustToolbox build output
uses: actions/cache@v4
with:
path: RobustToolbox/bin
key: ${{ runner.os }}-robust-${{ hashFiles('RobustToolbox/**/*.csproj', 'global.json') }}
restore-keys: |
${{ runner.os }}-robust-
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
@@ -32,46 +66,69 @@ jobs:
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST *" > ~/.ssh/config
echo " Hostname github.com" >> ~/.ssh/config
echo " Port 22" >> ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git clone git@github.com:corvax-nexus/secrets.git Secrets
cp -R Secrets/Resources/Prototypes Resources/Prototypes/CorvaxSecrets
cp -R Secrets/Resources/ServerPrototypes Resources/Prototypes/CorvaxSecretsServer
cp -R Secrets/Resources/Locale Resources/Locale/ru-RU/corvax-secrets
cp -R Secrets/Resources/Textures Resources/Textures/CorvaxSecrets
cp -R Secrets/Resources/Audio Resources/Audio/CorvaxSecrets
# Corvax-Secrets-End
printf '%s\n' 'Host git.wylab.me' ' Hostname git.wylab.me' ' Port 22' ' User git' ' IdentityFile ~/.ssh/id_rsa' ' StrictHostKeyChecking no' ' UserKnownHostsFile /dev/null' > ~/.ssh/config
chmod 600 ~/.ssh/config
git clone git@git.wylab.me:wylab/secrets.git Secrets
[ -d Secrets/Resources/Prototypes ] && cp -R Secrets/Resources/Prototypes Resources/Prototypes/WylabSecrets
[ -d Secrets/Resources/ServerPrototypes ] && cp -R Secrets/Resources/ServerPrototypes Resources/Prototypes/WylabSecretsServer
[ -d Secrets/Resources/Locale ] && cp -R Secrets/Resources/Locale Resources/Locale/ru-RU/wylab-secrets
[ -d Secrets/Resources/Textures ] && cp -R Secrets/Resources/Textures Resources/Textures/WylabSecrets
[ -d Secrets/Resources/Audio ] && cp -R Secrets/Resources/Audio Resources/Audio/WylabSecrets
# Wylab-Secrets-End
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Get Engine Tag
run: |
cd RobustToolbox
git fetch --depth=1
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Install dependencies
run: dotnet restore
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Build Packaging
run: dotnet build Content.Packaging --configuration Release --no-restore /m
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Publish version
run: Tools/publish_multi_request.py
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
FORK_ID: ${{ vars.FORK_ID }}
GITHUB_REPOSITORY: wylab/wylab-station-14
FORK_ID: wylab
ROBUST_CDN_URL: https://cdn.wylab.me/
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
- name: Trigger Docker image rebuild
if: ${{ success() && steps.cdn-check.outputs.skip != 'true' }}
env:
DISPATCH_TOKEN: ${{ secrets.DOCKER_TRIGGER_TOKEN }}
TARGET_REPO: wylab/WS14-Docker-Linux-Server
PAYLOAD: ${{ github.sha }}
run: |
if [ -z "${DISPATCH_TOKEN}" ]; then
echo "No DOCKER_TRIGGER_TOKEN configured; skipping dispatch."
exit 0
fi
curl -sSL -X POST \
-H "Authorization: token ${DISPATCH_TOKEN}" \
-H "Content-Type: application/json" \
https://git.wylab.me/api/v1/repos/${TARGET_REPO}/actions/workflows/main.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"commit\":\"${PAYLOAD}\"}}"
# - name: Publish changelog (Discord)
# continue-on-error: true

View File

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

View File

@@ -31,6 +31,8 @@ jobs:
name: Test Packaging
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master
@@ -48,19 +50,19 @@ jobs:
cd RobustToolbox/
git submodule update --init --recursive
# Corvax-Secrets-Start
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: ${{ env.SSH_KEY != '' }}
run: |
mkdir ~/.ssh
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST *" > ~/.ssh/config
echo "StrictHostKeyChecking no" >> ~/.ssh/config
echo "HOST git.wylab.me" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git -c submodule.Secrets.update=checkout submodule update --init
# Corvax-Secrets-End
# Wylab-Secrets-End
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0

View File

@@ -3,7 +3,7 @@ name: Update Wiki
on:
workflow_dispatch:
push:
branches: [ master, jsondump ]
branches: [ master ]
paths:
- '.github/workflows/update-wiki.yml'
- 'Content.Shared/Chemistry/**.cs'
@@ -19,6 +19,8 @@ jobs:
update-wiki:
name: Build and Publish JSON blobs to wiki
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master
@@ -51,42 +53,42 @@ jobs:
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
continue-on-error: true
- name: Upload chem_prototypes.json to wiki
- name: Upload chem_prototypes to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/chem_prototypes.json
edit_summary: Update chem_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json
api_url: https://wiki.wylab.me/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Upload react_prototypes.json to wiki
- name: Upload react_prototypes to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/react_prototypes.json
edit_summary: Update react_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json
api_url: https://wiki.wylab.me/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Upload entity_prototypes.json to wiki
- name: Upload entity_prototypes to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/entity_prototypes.json
edit_summary: Update entity_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json
api_url: https://wiki.wylab.me/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Upload mealrecipes_prototypes.json to wiki
- name: Upload mealrecipes_prototypes to wiki
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/mealrecipes_prototypes.json
edit_summary: Update mealrecipes_prototypes.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json
api_url: https://wiki.wylab.me/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}

View File

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

99
.github/workflows/upstream-sync.yml vendored Normal file
View File

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

View File

@@ -15,19 +15,19 @@ jobs:
- uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: git submodule update --init
# Corvax-Secrets-Start
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: ${{ env.SSH_KEY != '' }}
run: |
mkdir ~/.ssh
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST *" > ~/.ssh/config
echo "StrictHostKeyChecking no" >> ~/.ssh/config
echo "HOST git.wylab.me" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git -c submodule.Secrets.update=checkout submodule update --init
# Corvax-Secrets-End
# Wylab-Secrets-End
- name: Pull engine updates
uses: space-wizards/submodule-dependency@v0.1.5
- uses: PaulRitter/yaml-schema-validator@v1

View File

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

View File

@@ -15,19 +15,19 @@ jobs:
- uses: actions/checkout@v4.2.2
- name: Setup Submodule
run: git submodule update --init
# Corvax-Secrets-Start
# Wylab-Secrets-Start
- name: Setup secrets
env:
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
if: ${{ env.SSH_KEY != '' }}
run: |
mkdir ~/.ssh
mkdir -p ~/.ssh
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "HOST *" > ~/.ssh/config
echo "StrictHostKeyChecking no" >> ~/.ssh/config
echo "HOST git.wylab.me" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
git -c submodule.Secrets.update=checkout submodule update --init
# Corvax-Secrets-End
# Wylab-Secrets-End
- name: Pull engine updates
uses: space-wizards/submodule-dependency@v0.1.5
- uses: PaulRitter/yaml-schema-validator@v1

View File

@@ -12,8 +12,15 @@ jobs:
name: YAML Linter
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- uses: actions/checkout@v4.2.2
- name: Delete Wylab override files (duplicates upstream for customization)
run: |
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
- name: Setup submodule
run: |
git submodule update --init --recursive

View File

@@ -0,0 +1,14 @@
using Content.Shared.Communications;
using Robust.Shared.Prototypes;
namespace Content.Client.Communications;
[RegisterComponent]
public sealed partial class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent
{
/// <summary>
/// The prototype ID to use in the UI to show what entities a broadcast will display on
/// </summary>
[DataField]
public EntProtoId ScreenDisplayId = "Screen";
}

View File

@@ -1,9 +1,8 @@
using Content.Shared.CCVar;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Communications;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Client.Communications.UI
{
@@ -23,37 +22,31 @@ namespace Content.Client.Communications.UI
base.Open();
_menu = this.CreateWindow<CommunicationsConsoleMenu>();
_menu.OnAnnounce += AnnounceButtonPressed;
_menu.OnBroadcast += BroadcastButtonPressed;
_menu.OnAlertLevel += AlertLevelSelected;
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
_menu.OnRadioAnnounce += RadioAnnounceButtonPressed;
_menu.OnScreenBroadcast += ScreenBroadcastButtonPressed;
_menu.OnAlertLevelChanged += AlertLevelSelected;
_menu.OnShuttleCalled += CallShuttle;
_menu.OnShuttleRecalled += RecallShuttle;
if (EntMan.TryGetComponent<CommunicationsConsoleComponent>(Owner, out var console))
{
_menu.SetBroadcastDisplayEntity(console.ScreenDisplayId);
}
}
public void AlertLevelSelected(string level)
{
if (_menu!.AlertLevelSelectable)
{
_menu.CurrentLevel = level;
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
}
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
}
public void EmergencyShuttleButtonPressed()
{
if (_menu!.CountdownStarted)
RecallShuttle();
else
CallShuttle();
}
public void AnnounceButtonPressed(string message)
public void RadioAnnounceButtonPressed(string message)
{
var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
var msg = SharedChatSystem.SanitizeAnnouncement(message, maxLength);
SendMessage(new CommunicationsConsoleAnnounceMessage(msg));
}
public void BroadcastButtonPressed(string message)
public void ScreenBroadcastButtonPressed(string message)
{
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
}
@@ -77,20 +70,7 @@ namespace Content.Client.Communications.UI
if (_menu != null)
{
_menu.CanAnnounce = commsState.CanAnnounce;
_menu.CanBroadcast = commsState.CanBroadcast;
_menu.CanCall = commsState.CanCall;
_menu.CountdownStarted = commsState.CountdownStarted;
_menu.AlertLevelSelectable = commsState.AlertLevels != null && !float.IsNaN(commsState.CurrentAlertDelay) && commsState.CurrentAlertDelay <= 0;
_menu.CurrentLevel = commsState.CurrentAlert;
_menu.CountdownEnd = commsState.ExpectedCountdownEnd;
_menu.UpdateCountdown();
_menu.UpdateAlertLevels(commsState.AlertLevels, _menu.CurrentLevel);
_menu.AlertLevelButton.Disabled = !_menu.AlertLevelSelectable;
_menu.EmergencyShuttleButton.Disabled = !_menu.CanCall;
_menu.AnnounceButton.Disabled = !_menu.CanAnnounce;
_menu.BroadcastButton.Disabled = !_menu.CanBroadcast;
_menu.UpdateState(commsState);
}
}
}

View File

@@ -1,62 +1,32 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
<comms:CommunicationsConsoleMenu xmlns="https://spacestation14.io"
xmlns:comms="clr-namespace:Content.Client.Communications.UI"
xmlns:widgets="clr-namespace:Content.Client.Communications.UI.Widgets"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'comms-console-menu-title'}"
MinSize="400 300">
MouseFilter="Stop" MinSize="400 660" SetWidth="450">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<controls:LayeredImageContainer StyleClasses="BrightAngleRectOutline" VerticalExpand="True">
<BoxContainer Orientation="Vertical" Margin="12 8 12 12" VerticalExpand="True">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'comms-console-menu-title'}"
StyleClasses="FancyWindowTitle" HorizontalExpand="True"/>
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"
Modulate="#646464" VerticalAlignment="Center" Margin="0 0 0 6"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<PanelContainer StyleClasses="PanelDark" VerticalExpand="True">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<widgets:MessagingControls Name="MessagingControls" VerticalExpand="True" />
<!-- Main Container -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="False"
VerticalExpand="True"
Margin="6 6 6 5">
<TextEdit Name="MessageInput"
VerticalExpand="True"
HorizontalExpand="True"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
MinHeight="100"/>
<!-- ButtonsPart -->
<BoxContainer Orientation="Vertical"
VerticalAlignment="Bottom"
SeparationOverride="4">
<!-- AnnouncePart -->
<BoxContainer Orientation="Vertical"
Margin="0 2">
<Button Name="AnnounceButton"
Access="Public"
Text="{Loc 'comms-console-menu-announcement-button'}"
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}"
StyleClasses="OpenLeft"
Margin="0 0 1 0"
Disabled="True"/>
<Button Name="BroadcastButton"
Access="Public"
Text="{Loc 'comms-console-menu-broadcast-button'}"
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}"
StyleClasses="OpenBoth"/>
<OptionButton Name="AlertLevelButton"
Access="Public"
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
StyleClasses="OpenRight"/>
<controls:HSpacer Spacing="20" />
<widgets:AlertLevelControls Name="AlertLevelControls" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</controls:LayeredImageContainer>
<!-- EmergencyPart -->
<BoxContainer Orientation="Vertical"
SeparationOverride="6">
<widgets:ShuttleControls Name="ShuttleControls" />
<RichTextLabel Name="CountdownLabel"/>
<Button Name="EmergencyShuttleButton"
Access="Public"
Text="Placeholder Text"
ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
</comms:CommunicationsConsoleMenu>

View File

@@ -1,137 +1,83 @@
using System.Globalization;
using Content.Client.UserInterface.Controls;
using Content.Shared.CCVar;
using System.Numerics;
using Content.Shared.Communications;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Prototypes;
namespace Content.Client.Communications.UI
namespace Content.Client.Communications.UI;
[GenerateTypedNameReferences]
public sealed partial class CommunicationsConsoleMenu : BaseWindow
{
[GenerateTypedNameReferences]
public sealed partial class CommunicationsConsoleMenu : FancyWindow
private const int DragMoveSize = 40;
private const int DragResizeSize = 7;
public event Action? OnShuttleCalled;
public event Action? OnShuttleRecalled;
public event Action<string>? OnAlertLevelChanged;
public event Action<string>? OnRadioAnnounce;
public event Action<string>? OnScreenBroadcast;
public CommunicationsConsoleMenu()
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
public bool CanAnnounce;
public bool CanBroadcast;
public bool CanCall;
public bool AlertLevelSelectable;
public bool CountdownStarted;
public string CurrentLevel = string.Empty;
public TimeSpan? CountdownEnd;
CloseButton.OnPressed += _ => Close();
public event Action? OnEmergencyLevel;
public event Action<string>? OnAlertLevel;
public event Action<string>? OnAnnounce;
public event Action<string>? OnBroadcast;
MessagingControls.OnRadioAnnounce += message => OnRadioAnnounce?.Invoke(message);
MessagingControls.OnScreenBroadcast += message => OnScreenBroadcast?.Invoke(message);
public CommunicationsConsoleMenu()
AlertLevelControls.OnAlertLevelChanged += newLevel => OnAlertLevelChanged?.Invoke(newLevel);
ShuttleControls.OnShuttleCalled += () => OnShuttleCalled?.Invoke();
ShuttleControls.OnShuttleRecalled += () => OnShuttleRecalled?.Invoke();
}
/// <summary>
/// Use the specified prototype ID as an example display in the
/// UI subsection where users type broadcast messages
/// </summary>
public void SetBroadcastDisplayEntity(EntProtoId broadcastEntityId)
{
MessagingControls.SetBroadcastDisplayEntity(broadcastEntityId);
}
/// <summary>
/// Configure UI to be consistent with the input state
/// </summary>
public void UpdateState(CommunicationsConsoleInterfaceState commsState)
{
MessagingControls.CanRadioAnnounce = commsState.CanAnnounce;
MessagingControls.CanScreenBroadcast = commsState.CanBroadcast;
var alertLevelSelectable = commsState.AlertLevels != null && commsState.CurrentAlertDelay <= 0;
AlertLevelControls.UpdateAlertLevels(commsState.AlertLevels, commsState.CurrentAlert, commsState.CurrentAlertColor, alertLevelSelectable);
ShuttleControls.UpdateState(commsState.CanCall, commsState.CountdownStarted, commsState.ExpectedCountdownEnd);
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
if (relativeMousePos.Y < DragMoveSize)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
MessageInput.Placeholder = new Rope.Leaf(_loc.GetString("comms-console-menu-announcement-placeholder"));
var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
MessageInput.OnTextChanged += (args) =>
{
if (args.Control.TextLength > maxAnnounceLength)
{
AnnounceButton.Disabled = true;
AnnounceButton.ToolTip = Loc.GetString("comms-console-message-too-long");
}
else
{
AnnounceButton.Disabled = !CanAnnounce;
AnnounceButton.ToolTip = null;
}
};
AnnounceButton.OnPressed += _ => OnAnnounce?.Invoke(Rope.Collapse(MessageInput.TextRope));
AnnounceButton.Disabled = !CanAnnounce;
BroadcastButton.OnPressed += _ => OnBroadcast?.Invoke(Rope.Collapse(MessageInput.TextRope));
BroadcastButton.Disabled = !CanBroadcast;
AlertLevelButton.OnItemSelected += args =>
{
var metadata = AlertLevelButton.GetItemMetadata(args.Id);
if (metadata != null && metadata is string cast)
{
OnAlertLevel?.Invoke(cast);
}
};
AlertLevelButton.Disabled = !AlertLevelSelectable;
EmergencyShuttleButton.OnPressed += _ => OnEmergencyLevel?.Invoke();
EmergencyShuttleButton.Disabled = !CanCall;
return DragMode.Move;
}
protected override void FrameUpdate(FrameEventArgs args)
else
{
base.FrameUpdate(args);
UpdateCountdown();
}
var mode = DragMode.None;
// The current alert could make levels unselectable, so we need to ensure that the UI reacts properly.
// If the current alert is unselectable, the only item in the alerts list will be
// the current alert. Otherwise, it will be the list of alerts, with the current alert
// selected.
public void UpdateAlertLevels(List<string>? alerts, string currentAlert)
{
AlertLevelButton.Clear();
if (alerts == null)
if (relativeMousePos.Y > Size.Y - DragResizeSize)
{
var name = currentAlert;
if (_loc.TryGetString($"alert-level-{currentAlert}", out var locName))
{
name = locName;
}
AlertLevelButton.AddItem(name);
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, currentAlert);
}
else
{
foreach (var alert in alerts)
{
var name = alert;
if (_loc.TryGetString($"alert-level-{alert}", out var locName))
{
name = locName;
}
AlertLevelButton.AddItem(name);
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, alert);
if (alert == currentAlert)
{
AlertLevelButton.Select(AlertLevelButton.ItemCount - 1);
}
}
}
}
public void UpdateCountdown()
{
if (!CountdownStarted)
{
CountdownLabel.SetMessage(string.Empty);
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-call-shuttle");
return;
mode |= DragMode.Bottom;
}
var diff = MathHelper.Max((CountdownEnd - _timing.CurTime) ?? TimeSpan.Zero, TimeSpan.Zero);
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-recall-shuttle");
var infoText = Loc.GetString($"comms-console-menu-time-remaining",
("time", diff.ToString(@"hh\:mm\:ss", CultureInfo.CurrentCulture)));
CountdownLabel.SetMessage(infoText);
if (relativeMousePos.X > Size.X - DragResizeSize)
{
mode |= DragMode.Right;
}
return mode;
}
}
}

View File

@@ -0,0 +1,42 @@
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Client.Stylesheets.Stylesheets;
using Content.Client.Stylesheets.SheetletConfigs;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client.UserInterface.Controls;
[CommonSheetlet]
public sealed class CommunicationsConsoleSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, IButtonConfig, IIconConfig
{
public override StyleRule[] GetRules(T sheet, object config)
{
var lcdFontLarge = ResCache.GetFont("/Fonts/7SegmentDisplayDigits.ttf", 20);
return [
E<Label>().Class("LabelLCDBig")
.Prop("font-color", sheet.NegativePalette.Text)
.Prop("font", lcdFontLarge),
/// Large red texture button
E<TextureButton>().Identifier("TemptingRedButton")
.Prop(TextureButton.StylePropertyTexture, sheet.GetTextureOr(sheet.RoundedButtonPath, NanotrasenStylesheet.TextureRoot))
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.Element),
E<TextureButton>().Identifier("TemptingRedButton")
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.Element),
E<TextureButton>().Identifier("TemptingRedButton")
.Pseudo(ContainerButton.StylePseudoClassDisabled)
.Prop(TextureButton.StylePropertyTexture, ResCache.GetTexture("/Textures/Interface/Nano/rounded_locked_button.svg.96dpi.png"))
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.DisabledElement),
E<TextureButton>().Identifier("TemptingRedButton").Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.HoveredElement),
E<TextureRect>().Identifier("ScrewHead")
.Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture("/Textures/Interface/Diegetic/screw.svg.96dpi.png")),
];
}
}

View File

@@ -0,0 +1,22 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical">
<!-- Info about the current alert level -->
<BoxContainer HorizontalAlignment="Center">
<Label StyleClasses="StatusFieldTitle" Margin="0 0 9 0"
Text="{Loc 'comms-console-alert-current-level-header'}" />
<Label Name="CurrentAlertLevelLabel" Margin="9 0 0 0"/>
</BoxContainer>
<Label Name="CurrentAlertLevelFlavorLabel" Align="Center" />
<!-- Controls for changing the alert level -->
<BoxContainer Margin="9 12 9 6">
<OptionButton Name="AlertLevelSelector" StyleClasses="OpenRight"
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
HorizontalExpand="True" SizeFlagsStretchRatio="4" />
<Button Name="ConfirmAlertLevelButton" StyleClasses="OpenLeft"
HorizontalExpand="True" SizeFlagsStretchRatio="1"
Text="{Loc 'comms-console-confirm-alert-level-button'}"
Disabled="True" />
</BoxContainer>
</BoxContainer>

View File

@@ -0,0 +1,137 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Communications.UI.Widgets;
[GenerateTypedNameReferences]
public sealed partial class AlertLevelControls : BoxContainer
{
[Dependency] private readonly ILocalizationManager _loc = default!;
private bool _alertLevelSelectable;
private string _currentAlertLevel = string.Empty;
public event Action<string>? OnAlertLevelChanged;
public AlertLevelControls()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
AlertLevelSelector.OnItemSelected += args =>
{
AlertLevelSelector.Select(args.Id);
EnableDisableConfirmLevelChangeButton();
};
ConfirmAlertLevelButton.OnPressed += _ =>
{
var metadata = AlertLevelSelector.GetItemMetadata(AlertLevelSelector.SelectedId);
if (metadata is string cast)
{
OnAlertLevelChanged?.Invoke(cast);
}
};
AlertLevelSelector.Disabled = !_alertLevelSelectable;
}
/// <summary>
/// Updates the UI components to display the current alert level and the
/// selectable alert levels
/// </summary>
public void UpdateAlertLevels(List<string>? alerts,
string currentAlert,
Color currentAlertColor,
bool alertLevelSelectable)
{
_alertLevelSelectable = alertLevelSelectable;
_currentAlertLevel = currentAlert;
CurrentAlertLevelLabel.Text = GetLocalizedAlertName(currentAlert);
CurrentAlertLevelLabel.ModulateSelfOverride = currentAlertColor;
AlertLevelSelector.Disabled = alerts == null || !_alertLevelSelectable;
// If user had changed the selection, but hadn't pressed the confirm
// button at the point we received an update message, we want to
// remember what they had selected, so we can attempt to re-select it
// if it's still an option in the new set of alert level possibilities.
string? previousSelection = null;
if (AlertLevelSelector.ItemCount > 1)
{
var metadata = AlertLevelSelector.GetItemMetadata(AlertLevelSelector.SelectedId);
if (metadata is string cast)
{
previousSelection = cast;
}
}
// The server will only send alert levels which are selectable, but
// unfortunately, for a short time after changing the alert level, no
// level is selectable, so we won't have any levels to put into the
// alert level list. Here, we make a dummy item to handle that; this
// item also informs the user what this combo box is for.
AlertLevelSelector.Clear();
AlertLevelSelector.AddItem(Loc.GetString("comms-console-change-alert-level-button"));
AlertLevelSelector.Select(0);
if (alerts != null)
{
foreach (var alert in alerts)
{
AlertLevelSelector.AddItem(GetLocalizedAlertName(alert));
AlertLevelSelector.SetItemMetadata(AlertLevelSelector.ItemCount - 1, alert);
if (alert == previousSelection)
{
AlertLevelSelector.Select(AlertLevelSelector.ItemCount - 1);
}
}
}
if (_loc.TryGetString($"comms-console-level-{currentAlert}-flavour-label", out var flavour))
{
CurrentAlertLevelFlavorLabel.Text = flavour;
}
else
{
CurrentAlertLevelFlavorLabel.Text = string.Empty;
}
EnableDisableConfirmLevelChangeButton();
}
/// <summary>
/// Configure the "confirm alert level changed" button to be consistent with
/// the server state and user interactions
/// </summary>
private void EnableDisableConfirmLevelChangeButton()
{
// Disable the button when:
// 1. Server says we can't change level
// 2. User selected special info option (id 0)
// 3. User selected the same level the station is currently on
var selectedId = AlertLevelSelector.SelectedId;
ConfirmAlertLevelButton.Disabled = !_alertLevelSelectable || selectedId == 0;
if (selectedId != 0)
{
var metadata = AlertLevelSelector.GetItemMetadata(selectedId);
if (metadata is string selected)
{
ConfirmAlertLevelButton.Disabled |= selected == _currentAlertLevel;
}
}
}
/// <summary>
/// Utility function to convert an alert level identifier into a localized string
/// </summary>
private string GetLocalizedAlertName(string alertName)
{
if (_loc.TryGetString($"alert-level-{alertName}", out var locName))
{
return locName;
}
return alertName;
}
}

View File

@@ -0,0 +1,34 @@
<TabContainer xmlns="https://spacestation14.io" >
<!-- Controls for sending radio announcements -->
<BoxContainer Orientation="Vertical" VerticalExpand="True"
TabContainer.TabTitle="{Loc 'comms-console-announce-tab-title'}">
<Label StyleClasses="StatusFieldTitle" Align="Center"
Text="{Loc 'comms-console-station-announcements-header'}" />
<PanelContainer StyleClasses="highlight"
Margin="6" VerticalExpand="True">
<PanelContainer StyleClasses="BackgroundDark" Margin="2"
HorizontalExpand="True" VerticalExpand="True">
<TextEdit Name="RadioMessageInput"
Margin="2 0 0 0" MinHeight="100"
HorizontalExpand="True" VerticalExpand="True" />
</PanelContainer>
</PanelContainer>
<Button Name="AnnounceButton" HorizontalExpand="True" Margin="6"
Text="{Loc 'comms-console-menu-announcement-button'}"
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}" />
</BoxContainer>
<!-- Controls for putting messages on station screens (aka broadcasting) -->
<BoxContainer Orientation="Vertical" VerticalExpand="True"
TabContainer.TabTitle="{Loc 'comms-console-broadcast-tab-title'}">
<Label StyleClasses="StatusFieldTitle" Align="Center" VAlign="Top"
VerticalExpand="True" VerticalAlignment="Top"
Text="{Loc 'comms-console-station-broadcast-header'}" />
<SpriteView Name="BroadcastEntityDisplay" Scale="8 8" />
<LineEdit Name="ScreenMessageInput" Margin="6 0 6 0"
PlaceHolder="{Loc 'comms-console-menu-broadcast-placeholder'}" />
<Button Name="BroadcastButton" HorizontalExpand="True" Margin="6"
Text="{Loc 'comms-console-menu-broadcast-button'}"
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}" />
</BoxContainer>
</TabContainer>

View File

@@ -0,0 +1,117 @@
using Content.Shared.CCVar;
using Content.Shared.TextScreen;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Communications.UI.Widgets;
[GenerateTypedNameReferences]
public sealed partial class MessagingControls : TabContainer
{
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
// Entity temporarily created to display a screen preview
private EntityUid _broadcastDisplayEntity = EntityUid.Invalid;
public event Action<string>? OnRadioAnnounce;
public event Action<string>? OnScreenBroadcast;
private bool _canRadioAnnounce;
public bool CanRadioAnnounce
{
set
{
_canRadioAnnounce = value;
SyncButtonState();
}
}
private bool _canScreenBroadcast;
public bool CanScreenBroadcast
{
set
{
_canScreenBroadcast = value;
SyncButtonState();
}
}
public MessagingControls()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
RadioMessageInput.Placeholder = new Rope.Leaf(_loc.GetString("comms-console-menu-announcement-placeholder"));
RadioMessageInput.OnTextChanged += (_) => SyncButtonState();
AnnounceButton.OnPressed += _ =>
{
OnRadioAnnounce?.Invoke(Rope.Collapse(RadioMessageInput.TextRope));
};
var appearanceSystem = _entMan.System<SharedAppearanceSystem>();
ScreenMessageInput.OnTextChanged += args =>
{
if (_broadcastDisplayEntity.IsValid())
{
appearanceSystem.SetData(_broadcastDisplayEntity, TextScreenVisuals.ScreenText, args.Text);
}
};
BroadcastButton.OnPressed += _ =>
{
OnScreenBroadcast?.Invoke(ScreenMessageInput.Text);
};
SyncButtonState();
}
public void SetBroadcastDisplayEntity(EntProtoId broadcastEntityId)
{
_broadcastDisplayEntity = _entMan.Spawn(broadcastEntityId);
if (_broadcastDisplayEntity.IsValid())
{
BroadcastEntityDisplay.SetEntity(_broadcastDisplayEntity);
}
}
protected override void ExitedTree()
{
if (_broadcastDisplayEntity.IsValid())
{
_entMan.DeleteEntity(_broadcastDisplayEntity);
}
}
private void SyncButtonState()
{
if (_canRadioAnnounce)
{
var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
if (RadioMessageInput.TextLength > maxAnnounceLength)
{
AnnounceButton.Disabled = true;
AnnounceButton.ToolTip = _loc.GetString("comms-console-message-too-long");
}
else
{
AnnounceButton.Disabled = false;
AnnounceButton.ToolTip = _loc.GetString("comms-console-menu-announcement-button-tooltip");
}
}
else
{
AnnounceButton.Disabled = true;
AnnounceButton.ToolTip = _loc.GetString("comms-console-message-cannot-send");
}
BroadcastButton.Disabled = !_canScreenBroadcast;
}
}

View File

@@ -0,0 +1,73 @@
<Control xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<controls:StripeBack
HasTopEdge="False" HasBottomEdge="False" StyleClasses="status-warning" >
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="4"/>
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="4"/>
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="4"/>
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="4"/>
<!-- Controls for calling, recalling and displaying info for the emergency shuttle -->
<PanelContainer StyleClasses="PanelDark" Margin="20">
<BoxContainer Orientation="Vertical" Margin="8">
<Label StyleClasses="FancyWindowTitle" Align="Center"
Text="{Loc 'comms-console-shuttle-controls-header'}" />
<!-- This contains some blank `Controls` to act as dummies. This is because the
GridContainer will always left-align the children, while we want them centered.
The dummy Controls will attempt to take an even amount of space each, which
will re-center our real controls -->
<GridContainer Columns="7" HorizontalExpand="True" Margin="0 8 0 0">
<!-- Row 1: the buttons themselves -->
<Control HorizontalExpand="True" />
<controls:LayeredImageContainer>
<Control.StyleClasses>
<sys:String>PanelMount</sys:String>
<sys:String>PanelDark</sys:String>
</Control.StyleClasses>
<TextureButton Name="EmergencyShuttleCallButton"
StyleIdentifier="TemptingRedButton" MinSize="60 60"
ToolTip="{Loc 'comms-console-menu-call-shuttle'}" />
</controls:LayeredImageContainer>
<Control HorizontalExpand="True" />
<controls:LayeredImageContainer
VerticalAlignment="Center">
<Control.StyleClasses>
<sys:String>PanelMount</sys:String>
<sys:String>PanelDark</sys:String>
</Control.StyleClasses>
<Label Name="CountdownLabel" StyleClasses="LabelLCDBig"
ReservesSpace="True" Margin="6"/>
</controls:LayeredImageContainer>
<Control HorizontalExpand="True" />
<controls:LayeredImageContainer>
<Control.StyleClasses>
<sys:String>PanelMount</sys:String>
<sys:String>PanelDark</sys:String>
</Control.StyleClasses>
<TextureButton Name="EmergencyShuttleRecallButton"
StyleIdentifier="TemptingRedButton" MinSize="60 60"
ToolTip="{Loc 'comms-console-menu-recall-shuttle'}" />
</controls:LayeredImageContainer>
<Control HorizontalExpand="True" />
<!-- Row 2: The labels for the buttons -->
<Control HorizontalExpand="True" />
<Label Text="{Loc 'comms-console-call-button-label'}" Align="Center" />
<Control HorizontalExpand="True" />
<Label Text="{Loc 'comms-console-shuttle-status-label'}" Align="Center" />
<Control HorizontalExpand="True" />
<Label Text="{Loc 'comms-console-recall-button-label'}" Align="Center" />
<Control HorizontalExpand="True" />
</GridContainer>
</BoxContainer>
</PanelContainer>
</controls:StripeBack>
</Control>

View File

@@ -0,0 +1,99 @@
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Client.Communications.UI.Widgets;
[GenerateTypedNameReferences]
public sealed partial class ShuttleControls : Control
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private bool _canCall;
private bool _countdownStarted;
private TimeSpan? _countdownEnd;
public event Action? OnShuttleCalled;
public event Action? OnShuttleRecalled;
public ShuttleControls()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
EmergencyShuttleCallButton.OnPressed += _ => OnShuttleCalled?.Invoke();
EmergencyShuttleRecallButton.OnPressed += _ => OnShuttleRecalled?.Invoke();
SyncButtonState();
}
public void UpdateState(bool canCallShuttle, bool countdownStarted, TimeSpan? expectedCountdownEnd)
{
_canCall = canCallShuttle;
_countdownStarted = countdownStarted;
_countdownEnd = expectedCountdownEnd;
SyncButtonState();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateCountdown();
}
/// <summary>
/// Syncs and animates the display of the time-to-shuttle-arrival label
/// </summary>
private void UpdateCountdown()
{
// Set the label on the LCD countdown
var countdown = _countdownEnd == null ? 0.0f : (float)Math.Max(_countdownEnd.Value.Subtract(_timing.CurTime).TotalSeconds, 0);
var remainingWholeSeconds = _countdownStarted ? (int)Math.Ceiling(countdown) : 0;
var message = (remainingWholeSeconds / 60).ToString("D2") + ":" + (remainingWholeSeconds % 60).ToString("D2");
CountdownLabel.Text = message;
CountdownLabel.Visible = _countdownStarted;
if(countdown == 0)
{
CountdownLabel.ModulateSelfOverride = Color.White;
}
else
{
// Blink the LCD
var alpha = 1.0f;
if (!_cfg.GetCVar(CCVars.ReducedMotion))
{
var subSecondsRemaining = countdown - (float)Math.Floor(countdown);
var lightEnableBlend = SmoothStep(0.1f, 0.3f, subSecondsRemaining);
var lightDisableBlend = SmoothStep(0.9f, 0.95f, subSecondsRemaining);
alpha = lightEnableBlend - lightDisableBlend;
}
CountdownLabel.ModulateSelfOverride = new Color(1.0f, 1.0f, 1.0f, alpha);
}
}
private float SmoothStep(float stepBegin, float stepEnd, float x)
{
if (x < stepBegin)
return 0;
if (x >= stepEnd)
return 1;
var t = (x - stepBegin) / (stepEnd - stepBegin);
return (3 * t * t) - (2 * t * t * t);
}
/// <summary>
/// Configure the shuttle call/recall buttons to have a consistent state
/// </summary>
private void SyncButtonState()
{
EmergencyShuttleCallButton.Disabled = !(_canCall && !_countdownStarted);
EmergencyShuttleRecallButton.Disabled = !(_canCall && _countdownStarted);
}
}

View File

@@ -42,7 +42,8 @@ public sealed class PdaSheetlet : Sheetlet<NanotrasenStylesheet>
E<PanelContainer>()
.Class("PdaBorderRect")
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect),
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect)
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#00000040")),
//PDA - Buttons
E<PdaSettingsButton>()

View File

@@ -0,0 +1,70 @@
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Client.Stylesheets.Palette;
using Content.Client.Stylesheets.Sheetlets;
using Content.Client.Stylesheets.Stylesheets;
using Content.Client.Stylesheets.SheetletConfigs;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client.UserInterface.Controls;
[CommonSheetlet]
public sealed class LayeredImageContainerSheetlet : Sheetlet<NanotrasenStylesheet>
{
public override StyleRule[] GetRules(NanotrasenStylesheet sheet, object config)
{
IPanelConfig panelCfg = sheet;
var panelMountBaseTex = ResCache.GetTexture("/Textures/Interface/Diegetic/PanelMountBase.svg.96dpi.png");
var panelMountHighlightTex = ResCache.GetTexture("/Textures/Interface/Diegetic/PanelMountHighlight.svg.96dpi.png");
var panelMountBaseStyleBox = new StyleBoxTexture
{
Texture = panelMountBaseTex,
PatchMarginLeft = 16,
PatchMarginTop = 16,
PatchMarginRight = 24,
PatchMarginBottom = 24
};
var panelMountHighlightStyleBox = new StyleBoxTexture
{
Texture = panelMountHighlightTex,
PatchMarginLeft = 16,
PatchMarginTop = 16,
PatchMarginRight = 24,
PatchMarginBottom = 24
};
var borderTex = sheet.GetTexture(panelCfg.GeometricPanelBorderPath).IntoPatch(StyleBox.Margin.All, 10);
return [
// Adds a raised border with rounded corners around a UI element
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
.Prop(LayeredImageContainer.StylePropertyMinimumContentMargin, new Thickness(10, 10, 16, 16)),
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
.ParentOf(E<PanelContainer>().Identifier("Foreground1"))
.Prop(PanelContainer.StylePropertyPanel, panelMountBaseStyleBox),
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
.ParentOf(E<PanelContainer>().Identifier("Foreground2"))
.Prop(PanelContainer.StylePropertyPanel, panelMountHighlightStyleBox),
E<LayeredImageContainer>().Class(StyleClass.PanelDark)
.ParentOf(E<PanelContainer>().Identifier("Foreground1"))
.Prop(Control.StylePropertyModulateSelf, sheet.SecondaryPalette.BackgroundDark),
/// Bright AngleRect with a subtle outline
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassBrightAngleRect)
.ParentOf(E<PanelContainer>().Identifier("Background1"))
.Prop(PanelContainer.StylePropertyPanel, StyleBoxHelpers.BaseStyleBox(sheet))
.Prop(Control.StylePropertyModulateSelf, Palettes.Cyan.BackgroundLight),
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassBrightAngleRect)
.ParentOf(E<PanelContainer>().Identifier("Background2"))
.Prop(PanelContainer.StylePropertyPanel, borderTex)
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#00000040")),
];
}
}

View File

@@ -1,4 +1,5 @@
using Content.Client.Stylesheets.SheetletConfigs;
using Content.Client.Stylesheets.Palette;
using Content.Client.Stylesheets.SheetletConfigs;
using Content.Client.Stylesheets.Stylesheets;
using Content.Client.UserInterface.Controls;
using Robust.Client.Graphics;
@@ -14,16 +15,25 @@ public sealed class StripebackSheetlet<T> : Sheetlet<T> where T : PalettedStyles
{
IStripebackConfig stripebackCfg = sheet;
var stripeTex = sheet.GetTextureOr(stripebackCfg.StripebackPath, NanotrasenStylesheet.TextureRoot);
var stripeBack = new StyleBoxTexture
{
Texture = sheet.GetTextureOr(stripebackCfg.StripebackPath, NanotrasenStylesheet.TextureRoot),
Texture = stripeTex,
Mode = StyleBoxTexture.StretchMode.Tile,
Modulate = sheet.PrimaryPalette.BackgroundDark
};
var stripeBackWarning = new StyleBoxTexture {
Texture = stripeTex,
Mode = StyleBoxTexture.StretchMode.Tile,
Modulate = Palettes.Amber.Element
};
return
[
E<StripeBack>()
.Prop(StripeBack.StylePropertyBackground, stripeBack),
E<StripeBack>().Class(StyleClass.StatusWarning)
.Prop(StripeBack.StylePropertyBackground, stripeBackWarning),
];
}
}

View File

@@ -59,6 +59,7 @@ public static class StyleClass
public const string ButtonBig = "ButtonBig";
public const string CrossButtonRed = "CrossButtonRed";
public const string RefreshButton = "RefreshButton";
public const string ItemStatus = "ItemStatus";

View File

@@ -0,0 +1,8 @@
<controls:LayeredImageContainer xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<PanelContainer StyleIdentifier="Background1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<PanelContainer StyleIdentifier="Background2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<Control Name="ContentsContainer"/>
<PanelContainer StyleIdentifier="Foreground1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<PanelContainer StyleIdentifier="Foreground2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</controls:LayeredImageContainer>

View File

@@ -0,0 +1,46 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// A generic container which can layer multiple styleboxes over/under the
/// contents of of the child controls. This container contains two forground
/// panels and two background panels, each of which can be styled independently.
/// Background panels will be rendered underneath the child controls, while
/// foreground panels will be rendered above. The use of two panels for each
/// level allows for one level to use a stylized graphic, typically including
/// some modulation to color-match the surrounding UI, while the second level
/// can be used for shadows or highlights, which would typically not have such
/// modulation.
/// </summary>
[GenerateTypedNameReferences]
public partial class LayeredImageContainer : Container
{
// Adds a raised border with rounded corners around a UI element
public const string StyleClassPanelMount = "PanelMount";
// Bright AngleRect with a subtle outline
public const string StyleClassBrightAngleRect = "BrightAngleRectOutline";
// The least amount of margin that a child needs to have to avoid drawing under
// undesirable parts of the images. Children can add additional margins if desired
public const string StylePropertyMinimumContentMargin = "MinimumContentMargin";
public LayeredImageContainer()
{
RobustXamlLoader.Load(this);
XamlChildren = ContentsContainer.Children;
}
protected override void StylePropertiesChanged()
{
if (TryGetStyleProperty<Thickness>(StylePropertyMinimumContentMargin, out var contentMargin))
{
ContentsContainer.Margin = contentMargin;
}
}
}

View File

@@ -0,0 +1,20 @@
using Content.Shared.Wega.Ghost.Respawn;
namespace Content.Client.Wega.Ghost.Respawn;
public sealed class GhostRespawnSystem : EntitySystem
{
public TimeSpan? GhostRespawnTime { get; private set; }
public event Action? GhostRespawn;
public override void Initialize()
{
SubscribeNetworkEvent<GhostRespawnEvent>(OnGhostRespawnReset);
}
private void OnGhostRespawnReset(GhostRespawnEvent e)
{
GhostRespawnTime = e.Time;
GhostRespawn?.Invoke();
}
}

View File

@@ -0,0 +1,41 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:local="clr-namespace:Content.Client.Select.Class.UI;assembly=Content.Client"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Content.Client.Select.Class.UI.SelectClassMenu"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
<!-- Main Radial Menu Container -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="64" ReserveSpaceForHiddenChildren="False">
<!-- Button 1: Hemomancer -->
<ui:RadialMenuButton Name="HemomancerButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-hemomancer'}" TargetLayerControlName="Hemomancer">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/claws.png"/>
</ui:RadialMenuButton>
<!-- Button 2: Umbrae -->
<ui:RadialMenuButton Name="UmbraeButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-umbrae'}" TargetLayerControlName="Umbrae">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/cloak.png"/>
</ui:RadialMenuButton>
<!-- Button 3: Gargantua -->
<ui:RadialMenuButton Name="GargantuaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-gargantua'}" TargetLayerControlName="Gargantua">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/swell.png"/>
</ui:RadialMenuButton>
<!-- Button 4: Dantalion -->
<ui:RadialMenuButton Name="DantalionButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-dantalion'}" TargetLayerControlName="Dantalion">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/enthrall.png"/>
</ui:RadialMenuButton>
<!-- Button 5: Bestia -->
<!-- <ui:RadialMenuButton Name="BestiaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-bestia'}" TargetLayerControlName="Bestia">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/rush.png"/>
</ui:RadialMenuButton> -->
</ui:RadialContainer>
</ui:RadialMenu>

View File

@@ -0,0 +1,43 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Vampire;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
namespace Content.Client.Select.Class.UI;
[GenerateTypedNameReferences]
public sealed partial class SelectClassMenu : RadialMenu
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntityNetworkManager _entityNetworkManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
public event Action<string>? OnSelectClass;
public SelectClassMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
InitializeButtons();
}
private void InitializeButtons()
{
HemomancerButton.OnButtonUp += _ => HandleClassSelection("Hemomancer");
UmbraeButton.OnButtonUp += _ => HandleClassSelection("Umbrae");
GargantuaButton.OnButtonUp += _ => HandleClassSelection("Gargantua");
DantalionButton.OnButtonUp += _ => HandleClassSelection("Dantalion");
//BestiaButton.OnButtonUp += _ => HandleClassSelection("Bestia");
}
private void HandleClassSelection(string className)
{
OnSelectClass?.Invoke(className);
var netEntity = _entityManager.GetNetEntity(_playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid);
_entityNetworkManager.SendSystemNetworkMessage(new VampireSelectClassMenuClosedEvent(netEntity, className));
Close();
}
}

View File

@@ -0,0 +1,46 @@
using Content.Client.Select.Class.UI;
using Content.Shared.Vampire;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
namespace Content.Client.UserInterface.Systems.Select.Class
{
public sealed class SelectClassUIController : UIController
{
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private SelectClassMenu? _menu;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<SelectClassPressedEvent>(OnSelectClassMenuReceived);
}
private void OnSelectClassMenuReceived(SelectClassPressedEvent args, EntitySessionEventArgs eventArgs)
{
var session = IoCManager.Resolve<IPlayerManager>().LocalSession;
var userEntity = _entityManager.GetEntity(args.Uid);
if (session?.AttachedEntity.HasValue == true && session.AttachedEntity.Value == userEntity)
{
if (_menu is null)
{
_menu = _uiManager.CreateWindow<SelectClassMenu>();
_menu.OnClose += OnMenuClosed;
_menu.OpenCentered();
}
else
{
_menu.OpenCentered();
}
}
}
private void OnMenuClosed()
{
_menu = null;
}
}
}

View File

@@ -0,0 +1,63 @@
using Content.Client.Alerts;
using Content.Client.Movement.Systems;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Vampire;
public sealed class VampireSystem : SharedVampireSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly ContentEyeSystem _contentEye = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<VampireToggleFovEvent>(OnToggleFoV);
SubscribeLocalEvent<VampireComponent, GetStatusIconsEvent>(GetVampireIcons);
SubscribeLocalEvent<ThrallComponent, GetStatusIconsEvent>(GetThrallIcons);
SubscribeLocalEvent<VampireComponent, UpdateAlertSpriteEvent>(OnUpdateAlert);
}
private void OnToggleFoV(VampireToggleFovEvent args)
{
var userEntity = _entityManager.GetEntity(args.User);
var eyeComponent = _entityManager.GetComponent<EyeComponent>(userEntity);
if (userEntity == _playerManager.LocalEntity)
_contentEye.RequestToggleFov(userEntity, eyeComponent);
}
private void GetVampireIcons(Entity<VampireComponent> ent, ref GetStatusIconsEvent args)
{
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
private void GetThrallIcons(Entity<ThrallComponent> ent, ref GetStatusIconsEvent args)
{
if (HasComp<VampireComponent>(ent))
return;
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
private void OnUpdateAlert(Entity<VampireComponent> ent, ref UpdateAlertSpriteEvent args)
{
if (args.Alert.ID != ent.Comp.BloodAlert)
return;
var blood = Math.Clamp(ent.Comp.CurrentBlood.Int(), 0, 999);
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit1, $"{(blood / 100) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit2, $"{(blood / 10) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit3, $"{blood % 10}");
}
}

View File

@@ -4,7 +4,7 @@
public sealed class PoolManagerTestEventHandler
{
// This value is completely arbitrary.
private static TimeSpan MaximumTotalTestingTimeLimit => TimeSpan.FromMinutes(20);
private static TimeSpan MaximumTotalTestingTimeLimit => TimeSpan.FromMinutes(40);
private static TimeSpan HardStopTimeLimit => MaximumTotalTestingTimeLimit.Add(TimeSpan.FromMinutes(1));
[OneTimeSetUp]

View File

@@ -226,15 +226,20 @@ public sealed class NukeOpsTest
Assert.That(total, Is.GreaterThan(3));
// Check the nukie commander passed basic training and figured out how to breathe.
// Skip respirator checks for IPC (they don't breathe)
var isIpc = entMan.GetComponent<MetaDataComponent>(player).EntityPrototype?.ID == "MobIpc";
var totalSeconds = 30;
var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds);
var increment = 5;
var resp = entMan.GetComponent<RespiratorComponent>(player);
RespiratorComponent? resp = null;
if (!isIpc)
resp = entMan.GetComponent<RespiratorComponent>(player);
var damage = entMan.GetComponent<DamageableComponent>(player);
for (var tick = 0; tick < totalTicks; tick += increment)
{
await pair.RunTicksSync(increment);
Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
if (!isIpc)
Assert.That(resp!.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
}

View File

@@ -47,6 +47,7 @@ public static class ServerPackaging
"Content.Server",
"Content.Shared",
"Content.Shared.Database",
"Content.Packaging",
};
private static readonly List<string> ServerExtraAssemblies = new()

View File

@@ -31,6 +31,8 @@ public sealed partial class AdminVerbSystem
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
private static readonly EntProtoId DefaultWizardRule = "Wizard";
private static readonly EntProtoId DefaultVampireRule = "Vampire";
private static readonly EntProtoId DefaultBloodBrothersRule = "BloodBrothers";
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
// All antag verbs have names so invokeverb works.
@@ -207,6 +209,36 @@ public sealed partial class AdminVerbSystem
};
args.Verbs.Add(wizard);
var vampireName = Loc.GetString("admin-verb-text-make-vampire");
Verb vampire = new()
{
Text = vampireName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_Wega/Interface/Actions/actions_vampire.rsi"), "bite"),
Act = () =>
{
_antag.ForceMakeAntag<VampireRuleComponent>(targetPlayer, DefaultVampireRule);
},
Impact = LogImpact.High,
Message = string.Join(": ", vampireName, Loc.GetString("admin-verb-make-vampire")),
};
args.Verbs.Add(vampire);
var bloodBrothersName = Loc.GetString("admin-verb-text-make-blood-brothers");
Verb bloodBrothers = new()
{
Text = bloodBrothersName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_Wega/Interface/Actions/actions_vampire.rsi"), "blood_bond"),
Act = () =>
{
_antag.ForceMakeAntag<BloodBrotherRuleComponent>(targetPlayer, DefaultBloodBrothersRule);
},
Impact = LogImpact.High,
Message = string.Join(": ", bloodBrothersName, Loc.GetString("admin-verb-make-blood-brothers")),
};
args.Verbs.Add(bloodBrothers);
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
args.Verbs.Add(paradox);
}

View File

@@ -67,6 +67,33 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
}
}
// WyLab-Wega-Start
public bool TryAddMetabolizerType(MetabolizerComponent component, string metabolizerType)
{
if (!_prototypeManager.HasIndex<MetabolizerTypePrototype>(metabolizerType))
return false;
if (component.MetabolizerTypes == null)
component.MetabolizerTypes = new();
return component.MetabolizerTypes.Add(metabolizerType);
}
public bool TryRemoveMetabolizerType(MetabolizerComponent component, string metabolizerType)
{
if (component.MetabolizerTypes == null)
return true;
return component.MetabolizerTypes.Remove(metabolizerType);
}
public void ClearMetabolizerTypes(MetabolizerComponent component)
{
if (component.MetabolizerTypes != null)
component.MetabolizerTypes.Clear();
}
// WyLab-Wega-End
private void OnApplyMetabolicMultiplier(Entity<MetabolizerComponent> ent, ref ApplyMetabolicMultiplierEvent args)
{
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;

View File

@@ -135,6 +135,7 @@ namespace Content.Server.Communications
List<string>? levels = null;
string currentLevel = default!;
float currentDelay = 0;
var currentAlertColor = Color.White;
if (stationUid != null)
{
@@ -150,6 +151,11 @@ namespace Content.Server.Communications
{
levels.Add(id);
}
if (id == alertComp.CurrentLevel)
{
currentAlertColor = detail.Color;
}
}
}
@@ -163,6 +169,7 @@ namespace Content.Server.Communications
CanCallOrRecall(comp),
levels,
currentLevel,
currentAlertColor,
currentDelay,
_roundEndSystem.ExpectedCountdownEnd
));

View File

@@ -207,7 +207,7 @@ namespace Content.Server.Entry
protected override void Dispose(bool disposing)
{
var dest = _cfg.GetCVar(CCVars.DestinationFile);
if (!string.IsNullOrEmpty(dest))
if (string.IsNullOrEmpty(dest))
{
_playTimeTracking.Shutdown();
_dbManager.Shutdown();

View File

@@ -0,0 +1,127 @@
using Content.Server._WL.DayNight;
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Server.GameObjects;
using Robust.Shared.Console;
using Robust.Shared.Map;
using System.Linq;
using System.Numerics;
namespace Content.Server._WL.Administration.Commands
{
[AdminCommand(AdminFlags.Mapping)]
public sealed partial class DayNightCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
public override string Command => "daynight";
public override string Description
=>
"""
Добавляет карте смену дня и ночи.
Желательно, чтоб это была планета.
Также желательно, чтобы эта команда использовалась только с неинициализированными картами.
""";
public override string Help => "daynight <mapId> <fullCycle> <dayRatio> <nightRatio> <dayColor> <nightColor>";
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(_mapMan.GetAllMapIds().Select(x => x.ToString()), "MapId");
}
else if (args.Length == 2)
{
return CompletionResult.FromHint("FullCycle in seconds");
}
else if (args.Length == 3)
{
return CompletionResult.FromHint("Day ratio an integer");
}
else if (args.Length == 4)
{
return CompletionResult.FromHint("Night ration an integer");
}
else if (args.Length == 5)
{
return CompletionResult.FromHint("Day Hex");
}
else if (args.Length == 6)
{
return CompletionResult.FromHint("Night Hex");
}
return CompletionResult.Empty;
}
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 6 && args.Length != 4)
{
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
return;
}
var mapSys = _entMan.System<MapSystem>();
if (!int.TryParse(args[0], out var mapIntegerId))
{
shell.WriteError("MapId должно быть числом!");
return;
}
var mapId = new MapId(mapIntegerId);
if (!mapSys.MapExists(mapId))
{
shell.WriteError($"Карты с ID равнм {mapIntegerId} не существует!");
return;
}
if (!int.TryParse(args[1], out var fullCycleTime) || fullCycleTime <= 0)
{
shell.WriteError("fullCycleTime должен представлять целое число большее нуля!");
return;
}
if (!int.TryParse(args[2], out var dayRatio) || dayRatio <= 0)
{
shell.WriteError("dayRatio должен представлять целое число большее нуля!");
return;
}
if (!int.TryParse(args[3], out var nightRatio) || nightRatio <= 0)
{
shell.WriteError("nightRatio должен представлять целое число большее нуля!");
return;
}
if (!mapSys.TryGetMap(mapId, out var mapUid) || mapUid == null)
{
shell.WriteError("Неизвестная ошибка.");
return;
}
var dayNnightComp = _entMan.EnsureComponent<DayNightComponent>(mapUid.Value);
dayNnightComp.DayNightRatio = new Vector2(dayRatio, nightRatio);
dayNnightComp.FullCycle = TimeSpan.FromSeconds(fullCycleTime);
if (args.Length != 6)
return;
var dayColor = Color.TryFromHex(args[4]);
var nightColor = Color.TryFromHex(args[5]);
if (dayColor != null)
{
dayNnightComp.DayHex = args[4];
}
if (nightColor != null)
{
dayNnightComp.NightHex = args[5];
}
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Numerics;
namespace Content.Server._WL.DayNight
{
[RegisterComponent]
public sealed partial class DayNightComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
[DataField]
public TimeSpan FullCycle = TimeSpan.FromSeconds(1200);
[ViewVariables(VVAccess.ReadOnly)]
[DataField("ratio")]
public Vector2 DayNightRatio = new(6, 4);
[ViewVariables(VVAccess.ReadOnly)]
[DataField("day")]
public string DayHex = "#F7CA68FF";
[ViewVariables(VVAccess.ReadOnly)]
[DataField("night")]
public string NightHex = "#0f1026";
[ViewVariables(VVAccess.ReadOnly)]
public bool WasInit = false;
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan NextCycle;
}
}

View File

@@ -0,0 +1,97 @@
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
using System.Numerics;
namespace Content.Server._WL.DayNight
{
public sealed partial class DayNightSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTime = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly MapSystem _mapSys = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DayNightComponent, MapInitEvent>(OnMapInit, after: [typeof(SharedMapSystem)]);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<DayNightComponent>();
while (query.MoveNext(out var map, out var dayNightComp))
{
if (!TryComp<MapLightComponent>(map, out var mapLightComp))
continue;
if (!TryComp<MapComponent>(map, out var mapComponent))
continue;
if (!dayNightComp.WasInit || mapComponent.MapPaused)
continue;
if (_gameTime.CurTime >= dayNightComp.NextCycle)
dayNightComp.NextCycle += dayNightComp.FullCycle;
var color = CalculateColor(
_gameTime.CurTime,
dayNightComp.FullCycle,
dayNightComp.NextCycle,
Color.FromHex(dayNightComp.DayHex),
Color.FromHex(dayNightComp.NightHex),
dayNightComp.DayNightRatio);
if (color == mapLightComp.AmbientLightColor) //Оптимизация для случаев, если цикл дня и ночи огромен.
continue;
_mapSys.SetAmbientLight(mapComponent.MapId, color);
}
}
private void OnMapInit(EntityUid station, DayNightComponent comp, MapInitEvent args)
{
if (!TryComp<MapComponent>(station, out var mapComponent))
return;
_mapSys.SetAmbientLight(mapComponent.MapId, Color.FromHex(comp.DayHex));
comp.NextCycle = _gameTime.CurTime + comp.FullCycle;
comp.WasInit = true;
}
public static Color CalculateColor(TimeSpan currentTime, TimeSpan fullCycle, TimeSpan nextCycle, Color dayColor, Color nightColor, Vector2 dayNightRatio)
{
currentTime = currentTime - (nextCycle - fullCycle);
var pair = dayNightRatio.X + dayNightRatio.Y;
var dayTime = fullCycle.TotalMinutes / pair * dayNightRatio.X;
var nightTime = fullCycle.TotalMinutes / pair * dayNightRatio.Y;
var isDay = currentTime.TotalMinutes <= dayTime;
var filledPercentage = isDay
? currentTime.TotalMinutes / dayTime
: (currentTime.TotalMinutes - dayTime) / nightTime;
var r = isDay
? dayColor.R + (nightColor.R - dayColor.R) * filledPercentage
: nightColor.R + (dayColor.R - nightColor.R) * filledPercentage;
var g = isDay
? dayColor.G + (nightColor.G - dayColor.G) * filledPercentage
: nightColor.G + (dayColor.G - nightColor.G) * filledPercentage;
var b = isDay
? dayColor.B + (nightColor.B - dayColor.B) * filledPercentage
: nightColor.B + (dayColor.B - nightColor.B) * filledPercentage;
var result = new Color((float) r, (float) g, (float) b);
return result;
}
}
}

View File

@@ -0,0 +1,19 @@
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Server._WL.Destructible.Components
{
[RegisterComponent]
public sealed partial class FrozenComponent : Component
{
[DataField] public LocId FrozenPrefix = "frozen-entity-prefix";
[DataField] public LocId FrozenPopup = "frozen-entity-popup";
[DataField] public LocId FrozenHealthString = "frozen-entity-health-string";
[DataField] public string BaseName;
[DataField] public Color BaseSkinColor;
[DataField] public ProtoId<DamageTypePrototype> FrozenDamage = "Cold";
}
}

View File

@@ -0,0 +1,62 @@
using Content.Server._WL.Destructible.Components;
using Content.Server.Humanoid;
using Content.Shared.Cloning;
using Content.Shared.Cloning.Events;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.Systems;
using Content.Shared.HealthExaminable;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Rejuvenate;
namespace Content.Server._WL.Destructible.Systems
{
public sealed partial class FrozenSystem : EntitySystem
{
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly HumanoidAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FrozenComponent, RefreshNameModifiersEvent>(OnRefreshName);
SubscribeLocalEvent<FrozenComponent, BeforeDamageChangedEvent>(BeforeDamageChanged);
SubscribeLocalEvent<FrozenComponent, CloningEvent>(OnClone);
SubscribeLocalEvent<FrozenComponent, HealthBeingExaminedEvent>(OnHealthExamine);
SubscribeLocalEvent<FrozenComponent, RejuvenateEvent>(OnRejuvenate);
}
private void OnRefreshName(EntityUid ent, FrozenComponent comp, RefreshNameModifiersEvent args)
{
args.AddModifier(comp.FrozenPrefix);
args.AddModifier(comp.BaseName, int.MinValue);
}
private void BeforeDamageChanged(EntityUid ent, FrozenComponent comp, ref BeforeDamageChangedEvent args)
{
args.Damage.DamageDict[comp.FrozenDamage.Id] = 0f;
args.Damage.TrimZeros();
}
private void OnClone(EntityUid ent, FrozenComponent comp, ref CloningEvent args)
{
var target = args.CloneUid;
_metaData.SetEntityName(target, comp.BaseName, raiseEvents: true);
_appearance.SetSkinColor(target, comp.BaseSkinColor);
}
private void OnHealthExamine(EntityUid ent, FrozenComponent comp, HealthBeingExaminedEvent args)
{
args.Message.AddMarkupOrThrow("\n" + Loc.GetString(comp.FrozenHealthString));
}
private void OnRejuvenate(EntityUid ent, FrozenComponent comp, RejuvenateEvent args)
{
_metaData.SetEntityName(ent, comp.BaseName, raiseEvents: true);
_appearance.SetSkinColor(ent, comp.BaseSkinColor);
RemComp<FrozenComponent>(ent);
}
}
}

View File

@@ -0,0 +1,81 @@
using Content.Server._WL.Destructible.Components;
using Content.Server.Destructible;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Server.Humanoid;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.NameModifier.Components;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Enums;
namespace Content.Server._WL.Destructible.Thresholds.Behaviors
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class FrozeBodyBehavior : IThresholdBehavior
{
public const float InterpolateStrength = 0.88f;
public static readonly Color InterpolateColor = Color.CadetBlue;
public void Execute(EntityUid bodyId, DestructibleSystem system, EntityUid? cause = null)
{
var entMan = system.EntityManager;
var humanoidAppearanceSys = entMan.System<HumanoidAppearanceSystem>();
var transformSys = entMan.System<TransformSystem>();
var popupSys = entMan.System<SharedPopupSystem>();
var metaDataSys = entMan.System<MetaDataSystem>();
var frozenComp = entMan.EnsureComponent<FrozenComponent>(bodyId);
//Обновляем цвет кожи
if (!entMan.TryGetComponent<HumanoidAppearanceComponent>(bodyId, out var humanoidAppearnceComp))
return;
var curColor = humanoidAppearnceComp.SkinColor;
frozenComp.BaseSkinColor = curColor;
humanoidAppearanceSys.SetSkinColor(
bodyId,
Color.InterpolateBetween(curColor, InterpolateColor, InterpolateStrength),
sync: true,
verify: false
);
//Устанавливаем префикс
var baseName = Identity.Name(bodyId, entMan);
frozenComp.BaseName = baseName;
var genderString = humanoidAppearnceComp.Gender switch
{
Gender.Male => "male",
Gender.Female => "female",
_ => "other"
};
var newName = $"{Loc.GetString(frozenComp.FrozenPrefix, ("gender", genderString))} {baseName}";
metaDataSys.SetEntityName(bodyId, newName);
//Запрещаем хил тела и разрешаем клонирование, убрав компонент гниения
entMan.RemoveComponent<PerishableComponent>(bodyId);
entMan.RemoveComponent<InjectableSolutionComponent>(bodyId);
//Поп-ап
var msg = Loc.GetString(frozenComp.FrozenPopup,
("name", baseName),
("gender", genderString));
popupSys.PopupCoordinates(
msg,
transformSys.GetMoverCoordinates(bodyId),
Robust.Shared.Player.Filter.Pvs(bodyId),
true,
PopupType.LargeCaution);
}
}
}

View File

@@ -0,0 +1,39 @@
using Content.Server._WL.Nutrition.Systems;
using Content.Shared.Clothing.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Server._WL.Nutrition.Components;
[RegisterComponent]
public sealed partial class SuckableFoodComponent : Component
{
[DataField]
public string Solution { get; set; } = "food";
/// <summary>
/// Количество поглощаемой из контейнера жидкости в секунду.
/// </summary>
[DataField]
public FixedPoint2 DissolveAmount { get; set; } = FixedPoint2.New(0.05f);
/// <summary>
/// Не указывайте сущности в прототипе, у которых есть <see cref="SuckableFoodComponent"/>, иначе будет runtime-ошибочка.
/// </summary>
[DataField("entityOnDissolve")]
public EntProtoId<ClothingComponent>? EquippedEntityOnDissolve { get; set; }
[DataField]
public ComponentRegistry? ComponentsOverride { get; set; }
[DataField]
public bool CanSuck { get; set; } = true;
[DataField]
public bool DeleteOnEmpty { get; set; } = true;
public bool IsSucking => SuckingEntity != null && CanSuck;
[Access(typeof(SuckableFoodSystem))]
public EntityUid? SuckingEntity;
}

View File

@@ -0,0 +1,19 @@
using Content.Server._WL.Nutrition.Components;
using Robust.Shared.Containers;
namespace Content.Server._WL.Nutrition.Events;
public sealed partial class SuckableFoodDissolvedEvent : EntityEventArgs
{
public Entity<SuckableFoodComponent> Suckable { get; }
public BaseContainer Container { get; }
public EntityUid Sucker { get; }
public SuckableFoodDissolvedEvent(Entity<SuckableFoodComponent> suckable, BaseContainer container, EntityUid sucker)
{
Suckable = suckable;
Container = container;
Sucker = sucker;
}
}

View File

@@ -0,0 +1,189 @@
using Content.Server._WL.Nutrition.Components;
using Content.Server._WL.Nutrition.Events;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Prototypes;
using Robust.Server.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using System.Diagnostics.CodeAnalysis;
namespace Content.Server._WL.Nutrition.Systems;
public sealed partial class SuckableFoodSystem : EntitySystem
{
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly FlavorProfileSystem _flavor = default!;
private const float UpdatePeriod = 2f; // in seconds
private float _updateTimer = 0f;
private static readonly LocId PutInMouthLoc = "food-sweets-put-in-mouth-popup-message";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SuckableFoodComponent, GotEquippedEvent>(OnEquip);
SubscribeLocalEvent<SuckableFoodComponent, GotUnequippedEvent>(ResetSucker);
SubscribeLocalEvent<SuckableFoodComponent, ComponentShutdown>(ResetSucker);
SubscribeLocalEvent<SuckableFoodComponent, SuckableFoodDissolvedEvent>(OnDissolved);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_updateTimer += frameTime;
var isNewLoop = _updateTimer >= UpdatePeriod;
var query = EntityQueryEnumerator<SuckableFoodComponent, SolutionContainerManagerComponent>();
while (query.MoveNext(out var food, out var suckableComp, out var solContainerManComp))
{
if (!Exists(suckableComp.SuckingEntity))
{
suckableComp.SuckingEntity = null;
continue;
}
if (isNewLoop)
{
var sucker = suckableComp.SuckingEntity.Value;
if (!TryComp<BloodstreamComponent>(sucker, out var bloodstreamComp))
continue;
suckableComp.CanSuck = _mobState.IsAlive(sucker); // TODO: вынести в отдельное событие
if (!suckableComp.IsSucking)
continue;
if (!EnsureSolutionEntity((food, suckableComp, solContainerManComp), out var solutionEntity, out var solution))
continue;
var dissolvedSol = _solutionContainerSystem.SplitSolution(solutionEntity.Value, suckableComp.DissolveAmount * UpdatePeriod);
if (solution.Volume == FixedPoint2.Zero)
{
if (_container.TryGetContainingContainer(food, out var container))
{
var ev = new SuckableFoodDissolvedEvent((food, suckableComp), container, sucker);
RaiseLocalEvent(food, ev);
RaiseLocalEvent(ev);
}
continue;
}
_reactiveSystem.DoEntityReaction(sucker, dissolvedSol, ReactionMethod.Ingestion);
_bloodstreamSystem.TryAddToBloodstream((sucker, bloodstreamComp), dissolvedSol);
}
}
if (isNewLoop)
_updateTimer -= UpdatePeriod;
}
public void SetState(Entity<SuckableFoodComponent> foodEnt, EntityUid? sucker)
{
var (food, comp) = foodEnt;
comp.SuckingEntity = sucker;
}
public bool EnsureSolutionEntity(
Entity<SuckableFoodComponent, SolutionContainerManagerComponent?> foodEnt,
[NotNullWhen(true)] out Entity<SolutionComponent>? solEnt,
[NotNullWhen(true)] out Solution? solution)
{
solEnt = null;
solution = null;
if (!Resolve(foodEnt, ref foodEnt.Comp2, false))
return false;
if (!_solutionContainerSystem.EnsureSolutionEntity((foodEnt, foodEnt.Comp2), foodEnt.Comp1.Solution, out var ent))
return false;
solEnt = ent;
solution = ent.Value.Comp.Solution;
return true;
}
private void OnEquip(EntityUid food, SuckableFoodComponent comp, GotEquippedEvent ev)
{
if (ev.SlotFlags.HasFlag(SlotFlags.MASK))
_forensics.TransferDna(food, ev.Equipee);
SetState((food, comp), ev.Equipee);
if (!EnsureSolutionEntity((food, comp), out _, out var sol))
return;
var flavor = _flavor.GetLocalizedFlavorsMessage(food, ev.Equipee, sol);
if (string.IsNullOrEmpty(flavor))
return;
var msg = Loc.GetString(PutInMouthLoc, ("flavor", flavor), ("entity", Identity.Name(food, EntityManager, ev.Equipee)));
_popup.PopupEntity(msg, ev.Equipee, Filter.Entities(ev.Equipee), false);
}
private void ResetSucker<T>(EntityUid food, SuckableFoodComponent comp, T ev)
{
SetState((food, comp), null);
}
private void OnDissolved(EntityUid food, SuckableFoodComponent comp, SuckableFoodDissolvedEvent ev)
{
if (comp.DeleteOnEmpty)
{
_inventory.TryUnequip(ev.Sucker, ev.Container.ID, true, true);
var msg = Loc.GetString("food-sweets-got-dissolved-popup-message", ("entity", Identity.Name(food, EntityManager)));
_popup.PopupEntity(msg, ev.Sucker, Filter.Entities(ev.Sucker), true, Shared.Popups.PopupType.Medium);
TryQueueDel(food);
}
if (comp.EquippedEntityOnDissolve != null)
{
if (_protoMan.TryIndex(comp.EquippedEntityOnDissolve.Value, out var proto)
&& proto.HasComponent<SuckableFoodComponent>(_componentFactory))
{
Log.Error($"EquippedEntityOnDissolve {comp.EquippedEntityOnDissolve.Value} on entity {ToPrettyString(food)} has {nameof(SuckableFoodComponent)}!");
return;
}
var ent = SpawnNextToOrDrop(comp.EquippedEntityOnDissolve.Value, ev.Sucker, overrides: comp.ComponentsOverride);
_inventory.TryEquip(ev.Sucker, ent, ev.Container.ID, true);
}
}
}

View File

@@ -0,0 +1,67 @@
using Content.Server.GameTicking;
using Content.Server.Mind;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Timing;
namespace Content.Server.Wega.Commands;
[AnyCommand()]
public sealed class GhostRespawnCommand : IConsoleCommand
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
public string Command => "ghostrespawn";
public string Description => "Allows the player to return to the lobby if they've been dead long enough, allowing re-entering the round AS ANOTHER CHARACTER.";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (!_configurationManager.GetCVar(WegaCVars.GhostRespawnEnabled))
{
shell.WriteLine("Respawning is disabled, ask an admin to respawn you.");
return;
}
if (shell.Player is null)
{
shell.WriteLine("You cannot run this from the console!");
return;
}
if (shell.Player.AttachedEntity is null)
{
shell.WriteLine("You cannot run this in the lobby, or without an entity.");
return;
}
if (!_entityManager.TryGetComponent<GhostComponent>(shell.Player.AttachedEntity, out var ghost))
{
shell.WriteLine("You are not a ghost.");
return;
}
var mindSystem = _entityManager.EntitySysManager.GetEntitySystem<MindSystem>();
if (!mindSystem.TryGetMind(shell.Player, out _, out _))
{
shell.WriteLine("You have no mind.");
return;
}
var time = (_gameTiming.CurTime - ghost.TimeOfDeath);
var respawnTime = _configurationManager.GetCVar(WegaCVars.GhostRespawnTime);
if (respawnTime > time.TotalSeconds)
{
shell.WriteLine($"You haven't been dead long enough. You have been dead {time.TotalSeconds} seconds of the required {respawnTime}.");
return;
}
var gameTicker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
gameTicker.Respawn(shell.Player);
}
}

View File

@@ -0,0 +1,41 @@
using System.Linq;
using Content.Shared.Friendly.Faction;
using Content.Shared.Mobs.Components;
using Content.Shared.Weapons.Melee.Events;
namespace Content.Server.Friendly.Faction
{
public sealed partial class FriendlyFactionSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FriendlyFactionComponent, MeleeHitEvent>(OnMeleeHit);
}
private void OnMeleeHit(EntityUid uid, FriendlyFactionComponent component, MeleeHitEvent args)
{
if (!TryComp<FriendlyFactionComponent>(args.User, out _))
return;
if (!args.HitEntities.Any())
return;
foreach (var entity in args.HitEntities)
{
if (args.User == entity)
continue;
if (!TryComp<MobStateComponent>(entity, out _))
continue;
if (TryComp<FriendlyFactionComponent>(entity, out var friendlyFaction)
&& friendlyFaction.Faction == component.Faction)
{
args.BonusDamage = -args.BaseDamage;
}
}
}
}
}

View File

@@ -0,0 +1,269 @@
using System.Text;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Blood.Brother;
using Content.Server.Roles;
using Content.Server.Objectives.Systems;
namespace Content.Server.GameTicking.Rules;
public sealed class BloodBrotherRuleSystem : GameRuleSystem<BloodBrotherRuleComponent>
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
[Dependency] private readonly BloodBrotherSharedStealConditionSystem _stealCondition = default!;
[Dependency] private readonly BloodBrotherSharedKillConditionSystem _killCondition = default!;
private static readonly Color BloodBrotherColor = Color.FromHex("#8b0000");
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BloodBrotherRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
}
private void AfterEntitySelected(Entity<BloodBrotherRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
CreateBloodBrotherPair(args.EntityUid, ent);
}
/// <summary>
/// Creates a pair of blood brothers
/// </summary>
public void CreateBloodBrotherPair(EntityUid bloodBrother, Entity<BloodBrotherRuleComponent> component)
{
if (!_mindSystem.TryGetMind(bloodBrother, out var mindId, out var mind))
return;
component.Comp.BloodBrotherMinds.Add(mindId);
EntityUid? brotherMindId = FindUnpairedBrother(mindId, component.Comp);
if (brotherMindId != null)
{
CreateBloodBrotherPairInternal(mindId, brotherMindId.Value, component);
GenerateSharedObjectives(mindId, brotherMindId.Value, component);
}
}
private void GenerateSharedObjectives(EntityUid mindId1, EntityUid mindId2, Entity<BloodBrotherRuleComponent> component)
{
if (!TryComp<MindComponent>(mindId1, out var mind1) || !TryComp<MindComponent>(mindId2, out var mind2))
return;
var currentDifficulty = 0f;
var selectedObjectives = new List<EntityUid>();
foreach (var set in component.Comp.ObjectiveSets)
{
if (!_random.Prob(set.Prob))
continue;
for (var pick = 0; pick < set.MaxPicks && component.Comp.MaxDifficulty > currentDifficulty; pick++)
{
var objective = _objectives.GetRandomObjective(mindId1, mind1, set.Groups, component.Comp.MaxDifficulty - currentDifficulty);
if (objective == null)
continue;
var objectiveComp = Comp<ObjectiveComponent>(objective.Value);
currentDifficulty += objectiveComp.Difficulty;
selectedObjectives.Add(objective.Value);
Log.Debug($"Selected random objective {ToPrettyString(objective)} for blood brothers pair");
}
}
foreach (var mandatoryObjectiveProto in component.Comp.RequiredObjectives)
{
var objective = _objectives.TryCreateObjective(mindId1, mind1, mandatoryObjectiveProto);
if (objective != null)
{
var objectiveComp = Comp<ObjectiveComponent>(objective.Value);
currentDifficulty += objectiveComp.Difficulty;
selectedObjectives.Add(objective.Value);
Log.Debug($"Added mandatory objective {mandatoryObjectiveProto} for blood brothers pair");
}
else
{
Log.Warning($"Failed to create mandatory objective {mandatoryObjectiveProto} for blood brothers");
}
}
foreach (var objective in selectedObjectives)
{
var proto = MetaData(objective).EntityPrototype?.ID;
if (proto == null)
continue;
var objective2 = _objectives.TryCreateObjective(mindId2, mind2, proto);
if (objective2 != null)
{
_mindSystem.AddObjective(mindId2, mind2, objective2.Value);
CopyObjectiveData(objective, objective2.Value, mindId1, mindId2);
Log.Debug($"Created shared objective {proto} for both brothers");
}
}
foreach (var objective in selectedObjectives)
{
_mindSystem.AddObjective(mindId1, mind1, objective);
}
Log.Info($"Generated {selectedObjectives.Count} shared objectives for blood brothers pair ({mindId1} and {mindId2})");
}
private void CopyObjectiveData(EntityUid sourceObjective, EntityUid targetObjective, EntityUid mindId1, EntityUid mindId2)
{
if (TryComp<TargetObjectiveComponent>(sourceObjective, out var sourceTarget)
&& sourceTarget.Target.HasValue && TryComp<TargetObjectiveComponent>(targetObjective, out var targetTarget))
{
_target.SetTarget(targetObjective, sourceTarget.Target.Value, targetTarget);
}
_sharedCondition.CopySharedConditionData(sourceObjective, targetObjective, mindId1, mindId2);
_stealCondition.CopySharedStealConditionData(sourceObjective, targetObjective);
_killCondition.CopySharedKillConditionData(sourceObjective, targetObjective);
}
private EntityUid? FindUnpairedBrother(EntityUid mindId, BloodBrotherRuleComponent component)
{
foreach (var otherMindId in component.BloodBrotherMinds)
{
if (otherMindId == mindId || component.BloodBrotherPairs.ContainsKey(mindId)
|| component.BloodBrotherPairs.ContainsKey(otherMindId)
|| component.BloodBrotherPairs.ContainsValue(mindId)
|| component.BloodBrotherPairs.ContainsValue(otherMindId))
continue;
return otherMindId;
}
return null;
}
private void CreateBloodBrotherPairInternal(EntityUid mindId1, EntityUid mindId2, Entity<BloodBrotherRuleComponent> component)
{
component.Comp.BloodBrotherPairs[mindId1] = mindId2;
component.Comp.BloodBrotherPairs[mindId2] = mindId1;
SetupBloodBrother(mindId1, mindId2, component.Comp);
SetupBloodBrother(mindId2, mindId1, component.Comp);
}
private void SetupBloodBrother(EntityUid mindId, EntityUid brotherMindId, BloodBrotherRuleComponent component)
{
if (!TryComp<MindComponent>(mindId, out var mind) || mind.OwnedEntity == null)
return;
_roleSystem.MindAddRole(mindId, component.BloodBrotherPrototypeId, silent: true);
_roleSystem.MindHasRole<BloodBrotherRoleComponent>(mindId, out var bloodBrotherRole);
if (bloodBrotherRole is not null)
{
EnsureComp<BloodBrotherComponent>(mindId, out var bloodBrotherComp);
bloodBrotherComp.BrotherMind = brotherMindId;
bloodBrotherComp.RequireBothAlive = component.RequireBothAlive;
// Get brother info for RoleBriefingComponent
var brotherName = GetBrotherName(brotherMindId);
var brotherJob = GetBrotherJob(brotherMindId);
var brotherBriefing = Loc.GetString("bloodbrother-role-brother-info",
("brotherName", brotherName),
("brotherJob", brotherJob));
EnsureComp<RoleBriefingComponent>(bloodBrotherRole.Value.Owner, out var briefingComp);
briefingComp.Briefing = brotherBriefing;
if (component.GiveBriefing)
{
SendFullBriefing(mindId, brotherMindId, component);
}
}
if (mind.OwnedEntity != null)
{
_npcFaction.RemoveFaction(mind.OwnedEntity.Value, component.NanoTrasenFaction, false);
_npcFaction.AddFaction(mind.OwnedEntity.Value, component.SyndicateFaction);
}
}
private void SendFullBriefing(EntityUid mindId, EntityUid brotherMindId, BloodBrotherRuleComponent component)
{
if (!TryComp<MindComponent>(mindId, out var mind) || mind.OwnedEntity == null)
return;
var briefing = GenerateFullBriefing(mindId, brotherMindId, component);
_antag.SendBriefing(mind.OwnedEntity.Value, briefing, BloodBrotherColor, component.GreetSoundNotification);
}
private string GenerateFullBriefing(EntityUid mindId, EntityUid brotherMindId, BloodBrotherRuleComponent component)
{
var sb = new StringBuilder();
var issuerPrototype = _prototypeManager.Index(component.ObjectiveIssuers);
var issuer = Loc.GetString(_random.Pick(issuerPrototype.Values));
sb.AppendLine(Loc.GetString("bloodbrother-role-greeting",
("corporation", issuer ?? Loc.GetString("objective-issuer-unknown"))));
var brotherName = GetBrotherName(brotherMindId);
var brotherJob = GetBrotherJob(brotherMindId);
sb.AppendLine("");
sb.AppendLine(Loc.GetString("bloodbrother-role-brother-info",
("brotherName", brotherName),
("brotherJob", brotherJob)));
sb.AppendLine("");
if (component.RequireBothAlive)
{
sb.AppendLine("-> " + Loc.GetString("bloodbrother-role-both-alive-requirement"));
}
sb.AppendLine("-> " + Loc.GetString("bloodbrother-role-both-escape-requirement"));
sb.AppendLine("-> " + Loc.GetString("bloodbrother-role-no-uplink-warning"));
sb.AppendLine("");
sb.AppendLine(Loc.GetString("bloodbrother-role-good-luck"));
return sb.ToString();
}
private string GetBrotherName(EntityUid brotherMindId)
{
if (TryComp<MindComponent>(brotherMindId, out var brotherMind) && brotherMind.CharacterName != null)
return brotherMind.CharacterName;
return Loc.GetString("bloodbrother-unknown-name");
}
private string GetBrotherJob(EntityUid brotherMindId)
{
if (_jobs.MindTryGetJobName(brotherMindId) is { } jobName)
return jobName;
return Loc.GetString("bloodbrother-unknown-job");
}
}

View File

@@ -0,0 +1,81 @@
using Content.Server.Codewords;
using Content.Shared.Dataset;
using Content.Shared.NPC.Prototypes;
using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(BloodBrotherRuleSystem))]
public sealed partial class BloodBrotherRuleComponent : Component
{
public readonly List<EntityUid> BloodBrotherMinds = new();
public readonly Dictionary<EntityUid, EntityUid> BloodBrotherPairs = new();
[DataField]
public EntProtoId BloodBrotherPrototypeId = "MindRoleBloodBrother";
[DataField]
public ProtoId<CodewordFactionPrototype> CodewordFactionPrototypeId = "Traitor";
[DataField]
public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen";
[DataField]
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
[DataField]
public ProtoId<LocalizedDatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
/// <summary>
/// Give blood brothers a briefing in chat.
/// </summary>
[DataField]
public bool GiveBriefing = true;
public int TotalBloodBrothers => BloodBrotherMinds.Count;
[DataField]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
/// <summary>
/// Whether both brothers must survive for individual objectives to count
/// </summary>
[DataField]
public bool RequireBothAlive = true;
/// <summary>
/// A list of required goals for all pairs of brothers
/// </summary>
[DataField]
public List<EntProtoId> RequiredObjectives = new();
/// <summary>
/// A list of goal sets for the brothers
/// </summary>
[DataField]
public List<BloodBrotherObjectiveSet> ObjectiveSets = new();
/// <summary>
/// Maximum difficulty of goals for a pair of brothers
/// </summary>
[DataField]
public float MaxDifficulty = 10f;
}
/// <summary>
/// A set of goals for blood brothers
/// </summary>
[DataDefinition]
public sealed partial class BloodBrotherObjectiveSet
{
[DataField(required: true)]
public ProtoId<WeightedRandomPrototype> Groups;
[DataField]
public float Prob = 1.0f;
[DataField]
public int MaxPicks = 2;
}

View File

@@ -0,0 +1,11 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Stores data for <see cref="VampireRuleSystem"/>.
/// </summary>
[RegisterComponent, Access(typeof(VampireRuleSystem))]
public sealed partial class VampireRuleComponent : Component
{
}

View File

@@ -0,0 +1,179 @@
using Content.Server.Antag;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Roles;
using Content.Server.Actions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Clumsy;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.Nutrition.Components;
using Content.Shared.Temperature.Components;
using Content.Shared.Vampire.Components;
using Content.Shared.Damage.Systems;
namespace Content.Server.GameTicking.Rules
{
public sealed class VampireRuleSystem : GameRuleSystem<VampireRuleComponent>
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly MetabolizerSystem _metabolism = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VampireRuleComponent, AfterAntagEntitySelectedEvent>(OnVampireSelected);
SubscribeLocalEvent<VampireRoleComponent, GetBriefingEvent>(OnVampireBriefing);
}
private void OnVampireSelected(Entity<VampireRuleComponent> mindId, ref AfterAntagEntitySelectedEvent args)
{
var ent = args.EntityUid;
MakeVampire(ent);
_antag.SendBriefing(ent, MakeBriefing(ent), Color.Purple, null);
}
private void OnVampireBriefing(Entity<VampireRoleComponent> vampire, ref GetBriefingEvent args)
{
var ent = args.Mind.Comp.OwnedEntity;
if (ent is null)
return;
args.Append(MakeBriefing(ent.Value));
}
private string MakeBriefing(EntityUid ent)
{
var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
var briefing = isHuman
? Loc.GetString("vampire-role-greeting-human")
: Loc.GetString("vampire-role-greeting-animal");
return briefing;
}
protected override void AppendRoundEndText(EntityUid uid,
VampireRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
var totalBloodDrank = GetTotalBloodDrankInRound();
args.AddLine(Loc.GetString("vampires-drank-total-blood", ("bloodAmount", totalBloodDrank)));
}
private float GetTotalBloodDrankInRound()
{
var totalBloodDrank = 0f;
foreach (var vampireEntity in EntityManager.EntityQuery<VampireComponent>(true))
{
totalBloodDrank += vampireEntity.TotalBloodDrank;
}
return totalBloodDrank;
}
private void MakeVampire(EntityUid vampire)
{
var vampireComponent = EnsureComp<VampireComponent>(vampire);
RemoveUnnecessaryComponents(vampire);
HandleMetabolismAndOrgans(vampire);
SetVampireComponents(vampire, vampireComponent);
UpdateAppearance(vampire);
AddVampireActions(vampire);
}
private void RemoveUnnecessaryComponents(EntityUid vampire)
{
var componentsToRemove = new[]
{
typeof(PacifiedComponent),
typeof(PerishableComponent),
typeof(BarotraumaComponent),
typeof(TemperatureSpeedComponent),
typeof(ThirstComponent),
typeof(ClumsyComponent)
};
foreach (var compType in componentsToRemove)
{
if (HasComp(vampire, compType))
RemComp(vampire, compType);
}
}
private void HandleMetabolismAndOrgans(EntityUid vampire)
{
if (TryComp<BodyComponent>(vampire, out var bodyComponent))
{
foreach (var organ in _body.GetBodyOrgans(vampire, bodyComponent))
{
if (TryComp<MetabolizerComponent>(organ.Id, out var metabolizer))
{
if (TryComp<StomachComponent>(organ.Id, out _))
_metabolism.ClearMetabolizerTypes(metabolizer);
_metabolism.TryAddMetabolizerType(metabolizer, VampireComponent.MetabolizerVampire);
}
}
}
}
private void SetVampireComponents(EntityUid vampire, VampireComponent _)
{
if (TryComp<TemperatureComponent>(vampire, out var temperatureComponent))
temperatureComponent.ColdDamageThreshold = Atmospherics.TCMB;
EnsureComp<UnholyComponent>(vampire);
EnsureComp<VampireComponent>(vampire);
_damage.SetDamageModifierSetId(vampire, "Vampire");
if (TryComp<ReactiveComponent>(vampire, out var reactive))
{
reactive.ReactiveGroups ??= new();
if (!reactive.ReactiveGroups.ContainsKey("Unholy"))
{
reactive.ReactiveGroups.Add("Unholy", new() { ReactionMethod.Touch });
}
}
}
private void UpdateAppearance(EntityUid vampire)
{
if (TryComp<HumanoidAppearanceComponent>(vampire, out var appearanceComponent))
{
appearanceComponent.EyeColor = Color.FromHex("#E22218FF");
Dirty(vampire, appearanceComponent);
}
}
private void AddVampireActions(EntityUid vampire)
{
var actionPrototypes = new[]
{
VampireComponent.DrinkActionPrototype,
VampireComponent.SelectClassActionPrototype,
VampireComponent.RejuvenateActionPrototype,
VampireComponent.GlareActionPrototype
};
foreach (var actionPrototype in actionPrototypes)
{
_actions.AddAction(vampire, actionPrototype);
}
}
}
}

View File

@@ -0,0 +1,76 @@
using System.Runtime.InteropServices;
using Content.Shared.Wega.Ghost.Respawn;
using Content.Shared.GameTicking;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Server.Player;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Wega.Ghost.Respawn;
public sealed class GhostRespawnSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly Dictionary<ICommonSession, TimeSpan> _respawnResetTimes = [];
public override void Initialize()
{
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<MindContainerComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
_player.PlayerStatusChanged += OnPlayerStatusChanged;
}
private void OnMobStateChanged(MobStateChangedEvent e)
{
if (e.NewMobState != MobState.Dead)
return;
if (!_player.TryGetSessionByEntity(e.Target, out var session))
return;
ResetRespawnTime(e.Target, session);
}
private void OnMindRemoved(EntityUid entity, MindContainerComponent component, MindRemovedMessage e)
{
if (e.Mind.Comp.UserId is null)
return;
if (TryComp<MobStateComponent>(entity, out var state) && state.CurrentState == MobState.Dead)
return;
if (!_player.TryGetSessionById(e.Mind.Comp.UserId.Value, out var session))
return;
ResetRespawnTime(entity, session);
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent e)
{
_respawnResetTimes.Clear();
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == Robust.Shared.Enums.SessionStatus.Connected)
SendRespawnResetTime(e.Session, GetRespawnResetTime(e.Session));
}
private void ResetRespawnTime(EntityUid entity, ICommonSession session)
{
ref var respawnTime = ref CollectionsMarshal.GetValueRefOrAddDefault(_respawnResetTimes, session, out _);
respawnTime = _timing.CurTime;
SendRespawnResetTime(session, _timing.CurTime);
}
private void SendRespawnResetTime(ICommonSession session, TimeSpan? time)
{
RaiseNetworkEvent(new GhostRespawnEvent(time), session);
}
public TimeSpan? GetRespawnResetTime(ICommonSession session)
{
return _respawnResetTimes.TryGetValue(session, out var time) ? time : null;
}
}

View File

@@ -0,0 +1,138 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Hallucinations;
using Content.Shared.StatusEffect;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Hallucinations;
// TODO: Full refactor this shit
public sealed partial class HallucinationsSystem : EntitySystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] private readonly StatusEffectsSystem _status = default!;
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HallucinationsComponent, MapInitEvent>(OnHallucinationsInit);
SubscribeLocalEvent<HallucinationsComponent, ComponentShutdown>(OnHallucinationsShutdown);
}
private void OnHallucinationsInit(EntityUid uid, HallucinationsComponent component, MapInitEvent args)
{
component.Layer = _random.Next(100, 150);
if (!_entityManager.TryGetComponent<EyeComponent>(uid, out var eye))
return;
UpdatePreset(component);
_eye.SetVisibilityMask(uid, eye.VisibilityMask | component.Layer, eye);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(uid):player} began to hallucinate.");
}
/// <summary>
/// Updates hallucinations component settings to match prototype
/// </summary>
/// <param name="component">Active HallucinationsComponent</param>
public void UpdatePreset(HallucinationsComponent component)
{
if (component.Proto == null)
return;
var preset = component.Proto;
component.Spawns = preset.Entities;
component.Range = preset.Range;
component.SpawnRate = preset.SpawnRate;
component.MinChance = preset.MinChance;
component.MaxChance = preset.MaxChance;
component.MaxSpawns = preset.MaxSpawns;
component.IncreaseChance = preset.IncreaseChance;
}
private void OnHallucinationsShutdown(EntityUid uid, HallucinationsComponent component, ComponentShutdown args)
{
if (!_entityManager.TryGetComponent<EyeComponent>(uid, out var eye))
return;
_eye.SetVisibilityMask(uid, eye.VisibilityMask & ~component.Layer, eye);
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(uid):player} stopped hallucinating.");
}
/// <summary>
/// Attempts to start hallucinations for target
/// </summary>
/// <param name="target">The target.</param>
/// <param name="key">Status effect key.</param>
/// <param name="time">Duration of hallucinations effect.</param>
/// <param name="refresh">Refresh active effects.</param>
/// <param name="proto">Hallucinations pack prototype.</param>
public bool StartHallucinations(EntityUid target, string key, TimeSpan time, bool refresh, string proto)
{
if (proto == null)
return false;
if (!_proto.TryIndex<HallucinationsPrototype>(proto, out var prototype))
return false;
if (!_status.TryAddStatusEffect<HallucinationsComponent>(target, key, time, refresh))
return false;
var hallucinations = _entityManager.GetComponent<HallucinationsComponent>(target);
hallucinations.Proto = prototype;
UpdatePreset(hallucinations);
hallucinations.CurChance = prototype.MinChance;
return true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<HallucinationsComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var stat, out var xform))
{
if (_timing.CurTime < stat.NextSecond)
continue;
var rate = stat.SpawnRate;
stat.NextSecond = _timing.CurTime + TimeSpan.FromSeconds(rate);
if (stat.CurChance < stat.MaxChance && stat.CurChance + stat.IncreaseChance <= 1)
stat.CurChance = stat.CurChance + stat.IncreaseChance;
if (!_random.Prob(stat.CurChance))
continue;
var selectedEntity = _random.Pick(stat.Spawns);
int spawnCount = _random.Next(3, 6);
stat.SpawnedCount = 0;
var range = stat.Range * 4;
UpdatePreset(stat);
for (int i = 0; i < spawnCount; i++)
{
var newCoords = Transform(uid).MapPosition.Offset(_random.NextVector2(stat.Range));
if (stat.SpawnedCount >= stat.MaxSpawns)
continue;
stat.SpawnedCount++;
var hallucination = Spawn(selectedEntity, newCoords);
EnsureComp<VisibilityComponent>(hallucination, out var visibility);
_visibilitySystem.SetLayer(hallucination, (ushort)stat.Layer, false);
_visibilitySystem.RefreshVisibility(hallucination, visibilityComponent: visibility);
}
}
}
}

View File

@@ -0,0 +1,45 @@
using Content.Shared.Hands;
using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events;
namespace Content.Server.Interaction;
public sealed class DeleteOnDropSystem : EntitySystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeleteOnDropComponent, GotUnequippedEvent>(OnUnequip);
SubscribeLocalEvent<DeleteOnDropComponent, GotUnequippedHandEvent>(OnUnequipHand);
SubscribeLocalEvent<DeleteOnDropComponent, DroppedEvent>(OnDropped);
}
private void OnUnequip(EntityUid uid, DeleteOnDropComponent item, GotUnequippedEvent args)
{
if (!item.DeleteOnDrop || !_entityManager.EntityExists(uid))
return;
QueueDel(uid);
}
private void OnUnequipHand(EntityUid uid, DeleteOnDropComponent item, GotUnequippedHandEvent args)
{
if (!item.DeleteOnDrop || !_entityManager.EntityExists(uid))
return;
QueueDel(uid);
}
private void OnDropped(EntityUid uid, DeleteOnDropComponent item, DroppedEvent args)
{
if (!item.DeleteOnDrop || !_entityManager.EntityExists(uid))
return;
QueueDel(uid);
}
}

View File

@@ -0,0 +1,51 @@
using Content.Server.Bible.Components;
using Content.Shared.Hands;
using Content.Shared.Inventory.Events;
using Content.Shared.NullRod.Components;
namespace Content.Server.NullRod;
public sealed class NullRodSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NullRodComponent, GotEquippedEvent>(OnDidEquip);
SubscribeLocalEvent<NullRodComponent, GotEquippedHandEvent>(OnHandEquipped);
SubscribeLocalEvent<NullRodComponent, GotUnequippedEvent>(OnDidUnequip);
SubscribeLocalEvent<NullRodComponent, GotUnequippedHandEvent>(OnHandUnequipped);
}
private void OnDidEquip(Entity<NullRodComponent> ent, ref GotEquippedEvent args)
{
if (!HasComp<BibleUserComponent>(args.Equipee) || HasComp<NullRodOwnerComponent>(args.Equipee))
return;
EnsureComp<NullRodOwnerComponent>(args.Equipee);
}
private void OnHandEquipped(Entity<NullRodComponent> ent, ref GotEquippedHandEvent args)
{
if (!HasComp<BibleUserComponent>(args.User) || HasComp<NullRodOwnerComponent>(args.User))
return;
EnsureComp<NullRodOwnerComponent>(args.User);
}
private void OnDidUnequip(Entity<NullRodComponent> ent, ref GotUnequippedEvent args)
{
if (!HasComp<NullRodOwnerComponent>(args.Equipee))
return;
RemComp<NullRodOwnerComponent>(args.Equipee);
}
private void OnHandUnequipped(Entity<NullRodComponent> ent, ref GotUnequippedHandEvent args)
{
if (!HasComp<NullRodOwnerComponent>(args.User))
return;
RemComp<NullRodOwnerComponent>(args.User);
}
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// A basic component for the common goals of blood brothers
/// </summary>
[RegisterComponent, Access(typeof(BloodBrotherSharedConditionSystem))]
public sealed partial class BloodBrotherSharedConditionComponent : Component
{
/// <summary>
/// ID brother mind
/// </summary>
[DataField]
public EntityUid? BrotherMind;
/// <summary>
/// Do you need both brothers to be alive to achieve your goals?
/// </summary>
[DataField]
public bool RequireBothAlive = true;
}

View File

@@ -0,0 +1,9 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// A component for the common escape goals of blood brothers
/// </summary>
[RegisterComponent, Access(typeof(BloodBrotherSharedEscapeConditionSystem))]
public sealed partial class BloodBrotherSharedEscapeConditionComponent : Component;

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Objective condition that requires both blood brothers to hijack the shuttle together
/// </summary>
[RegisterComponent, Access(typeof(BloodBrotherSharedHijackConditionSystem))]
public sealed partial class BloodBrotherSharedHijackConditionComponent : Component
{
}

View File

@@ -0,0 +1,9 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// A component for the common survival goals of blood brothers
/// </summary>
[RegisterComponent, Access(typeof(BloodBrotherSharedKeepAliveConditionSystem))]
public sealed partial class BloodBrotherSharedKeepAliveConditionComponent : Component;

View File

@@ -0,0 +1,22 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// A component for the common purpose of killing blood brothers
/// </summary>
[RegisterComponent, Access(typeof(BloodBrotherSharedKillConditionSystem))]
public sealed partial class BloodBrotherSharedKillConditionComponent : Component
{
/// <summary>
/// Whether the target must be dead
/// </summary>
[DataField]
public bool RequireDead = true;
/// <summary>
/// Whether the target must not escape
/// </summary>
[DataField]
public bool RequireMaroon = true;
}

View File

@@ -0,0 +1,72 @@
using Content.Server.Objectives.Systems;
using Content.Shared.Objectives;
using Robust.Shared.Prototypes;
namespace Content.Server.Objectives.Components;
/// <summary>
/// A component for the common purpose of stealing blood brothers
/// </summary>
[RegisterComponent, Access(typeof(BloodBrotherSharedStealConditionSystem))]
public sealed partial class BloodBrotherSharedStealConditionComponent : Component
{
/// <summary>
/// A group of items to be stolen
/// </summary>
[DataField(required: true)]
public ProtoId<StealTargetGroupPrototype> StealGroup;
/// <summary>
/// When enabled, disables generation of this target if there is no entity on the map
/// </summary>
[DataField]
public bool VerifyMapExistence = true;
/// <summary>
/// If true, counts objects that are close to steal areas.
/// </summary>
[DataField]
public bool CheckStealAreas = false;
/// <summary>
/// If the target may be alive but has died, it will not be counted
/// </summary>
[DataField]
public bool CheckAlive = false;
/// <summary>
/// The minimum number of items you need to steal to fulfill a objective
/// </summary>
[DataField]
public int MinCollectionSize = 1;
/// <summary>
/// The maximum number of items you need to steal to fulfill a objective
/// </summary>
[DataField]
public int MaxCollectionSize = 1;
/// <summary>
/// Target collection size after calculation
/// </summary>
[DataField]
public int CollectionSize;
/// <summary>
/// Help newer players by saying e.g. "steal the chief engineer's advanced magboots"
/// </summary>
[DataField("owner")]
public string? OwnerText;
[DataField(required: true)]
public LocId ObjectiveText;
[DataField(required: true)]
public LocId ObjectiveNoOwnerText;
[DataField(required: true)]
public LocId DescriptionText;
[DataField(required: true)]
public LocId DescriptionMultiplyText;
}

View File

@@ -0,0 +1,51 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
namespace Content.Server.Objectives.Systems;
public sealed class BloodBrotherSharedConditionSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
}
public bool CheckBaseConditions(EntityUid mindId, BloodBrotherSharedConditionComponent comp, MindComponent? mind = null)
{
if (!Resolve(mindId, ref mind))
return false;
if (comp.RequireBothAlive && _mind.IsCharacterDeadIc(mind))
return false;
if (comp.RequireBothAlive && comp.BrotherMind.HasValue)
{
if (!TryComp<MindComponent>(comp.BrotherMind.Value, out var brotherMind) ||
_mind.IsCharacterDeadIc(brotherMind))
return false;
}
return true;
}
public bool TryGetSharedCondition(EntityUid objectiveUid, EntityUid mindId, [NotNullWhen(true)] out BloodBrotherSharedConditionComponent? sharedCondition)
{
sharedCondition = null;
return TryComp(objectiveUid, out sharedCondition);
}
public void CopySharedConditionData(EntityUid sourceObjective, EntityUid targetObjective, EntityUid mindId1, EntityUid mindId2)
{
if (TryComp<BloodBrotherSharedConditionComponent>(sourceObjective, out var sourceCondition)
&& TryComp<BloodBrotherSharedConditionComponent>(targetObjective, out var targetCondition))
{
targetCondition.BrotherMind = mindId1;
targetCondition.RequireBothAlive = sourceCondition.RequireBothAlive;
sourceCondition.BrotherMind = mindId2;
}
}
}

View File

@@ -0,0 +1,55 @@
using Content.Server.Objectives.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.Cuffs.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
public sealed class BloodBrotherSharedEscapeConditionSystem : EntitySystem
{
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BloodBrotherSharedEscapeConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(EntityUid uid, BloodBrotherSharedEscapeConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetProgress(uid, args.MindId, args.Mind);
}
private float GetProgress(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
{
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
return 0f;
var currentEscape = CheckEscape(mindId, mind);
var brotherEscape = 0f;
if (sharedCondition?.BrotherMind != null &&
TryComp<MindComponent>(sharedCondition.BrotherMind.Value, out var brotherMind))
{
brotherEscape = CheckEscape(sharedCondition.BrotherMind.Value, brotherMind);
}
return Math.Min(currentEscape, brotherEscape);
}
private float CheckEscape(EntityUid mindId, MindComponent mind)
{
if (mind.OwnedEntity == null || _mind.IsCharacterDeadIc(mind))
return 0f;
if (TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0)
return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 0.5f : 0f;
return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 1f : 0f;
}
}

View File

@@ -0,0 +1,125 @@
using Content.Server.Objectives.Components;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.Cuffs.Components;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
using Robust.Shared.Player;
namespace Content.Server.Objectives.Systems;
public sealed class BloodBrotherSharedHijackConditionSystem : EntitySystem
{
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BloodBrotherSharedHijackConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(EntityUid uid, BloodBrotherSharedHijackConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetProgress(uid, args.MindId, args.Mind);
}
private float GetProgress(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
{
if (!CheckBaseHijackConditions(objectiveUid, mindId, mind))
return 0f;
if (!_emergencyShuttle.EmergencyShuttleArrived)
return 0f;
foreach (var stationData in EntityQuery<StationEmergencyShuttleComponent>())
{
if (stationData.EmergencyShuttle == null)
continue;
if (IsShuttleHijackedByBloodBrothers(stationData.EmergencyShuttle.Value, objectiveUid, mindId))
return 1f;
}
return 0f;
}
private bool CheckBaseHijackConditions(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
{
if (mind.OwnedEntity == null || TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0)
return false;
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
return false;
return true;
}
private bool IsShuttleHijackedByBloodBrothers(EntityUid shuttleGridId, EntityUid objectiveUid, EntityUid mindId)
{
var gridPlayers = Filter.BroadcastGrid(shuttleGridId).Recipients;
var humanoids = GetEntityQuery<HumanoidAppearanceComponent>();
var cuffable = GetEntityQuery<CuffableComponent>();
EntityQuery<MobStateComponent>();
var firstBrotherOnShuttle = false;
var secondBrotherOnShuttle = false;
EntityUid? brotherMindId = null;
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
&& sharedCondition.BrotherMind.HasValue)
{
brotherMindId = sharedCondition.BrotherMind.Value;
}
foreach (var player in gridPlayers)
{
if (player.AttachedEntity == null ||
!_mind.TryGetMind(player.AttachedEntity.Value, out var crewMindId, out _))
continue;
if (mindId == crewMindId)
{
firstBrotherOnShuttle = true;
continue;
}
if (brotherMindId.HasValue && brotherMindId.Value == crewMindId)
{
secondBrotherOnShuttle = true;
continue;
}
var isHumanoid = humanoids.HasComponent(player.AttachedEntity.Value);
if (!isHumanoid)
continue;
var isAntagonist = _role.MindIsAntagonist(crewMindId);
if (isAntagonist)
continue;
var isPersonIncapacitated = _mobState.IsIncapacitated(player.AttachedEntity.Value);
if (isPersonIncapacitated)
continue;
var isPersonCuffed =
cuffable.TryGetComponent(player.AttachedEntity.Value, out var cuffed)
&& cuffed.CuffedHandCount > 0;
if (isPersonCuffed)
continue;
return false;
}
return firstBrotherOnShuttle && secondBrotherOnShuttle;
}
}

View File

@@ -0,0 +1,44 @@
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
public sealed class BloodBrotherSharedKeepAliveConditionSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BloodBrotherSharedKeepAliveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(EntityUid uid, BloodBrotherSharedKeepAliveConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetProgress(uid, args.MindId, args.Mind);
}
private float GetProgress(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
{
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
return 0f;
if (!_target.GetTarget(objectiveUid, out var target))
return 0f;
return CalculateProtectProgress(target.Value);
}
private float CalculateProtectProgress(EntityUid target)
{
if (!TryComp<MindComponent>(target, out var mind))
return 0f;
return _mind.IsCharacterDeadIc(mind) ? 0f : 1f;
}
}

View File

@@ -0,0 +1,80 @@
using Content.Server.Objectives.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Robust.Shared.Configuration;
namespace Content.Server.Objectives.Systems;
public sealed class BloodBrotherSharedKillConditionSystem : EntitySystem
{
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BloodBrotherSharedKillConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(EntityUid uid, BloodBrotherSharedKillConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
if (!_target.GetTarget(uid, out var target))
return;
args.Progress = GetProgress(uid, target.Value, comp.RequireDead, comp.RequireMaroon, args.MindId, args.Mind);
}
private float GetProgress(EntityUid objectiveUid, EntityUid target, bool requireDead, bool requireMaroon, EntityUid mindId, MindComponent mind)
{
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
return 0f;
return CalculateKillProgress(target, requireDead, requireMaroon);
}
private float CalculateKillProgress(EntityUid target, bool requireDead, bool requireMaroon)
{
if (!TryComp<MindComponent>(target, out var mind) || mind.OwnedEntity == null)
return 1f;
var targetDead = _mind.IsCharacterDeadIc(mind);
var targetMarooned = !_emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) || _mind.IsCharacterUnrevivableIc(mind);
if (!_config.GetCVar(CCVars.EmergencyShuttleEnabled) && requireMaroon)
{
requireDead = true;
requireMaroon = false;
}
if (requireDead && !targetDead)
return 0f;
if (requireMaroon && !_emergencyShuttle.EmergencyShuttleArrived)
return 0f;
if (requireMaroon && !_emergencyShuttle.ShuttlesLeft)
return targetMarooned ? 0.5f : 0f;
if (requireMaroon && _emergencyShuttle.ShuttlesLeft)
return targetMarooned ? 1f : 0f;
return 1f;
}
public void CopySharedKillConditionData(EntityUid sourceObjective, EntityUid targetObjective)
{
if (TryComp<BloodBrotherSharedKillConditionComponent>(sourceObjective, out var sourceCondition)
&& TryComp<BloodBrotherSharedKillConditionComponent>(targetObjective, out var targetCondition))
{
targetCondition.RequireDead = sourceCondition.RequireDead;
targetCondition.RequireMaroon = sourceCondition.RequireMaroon;
}
}
}

View File

@@ -0,0 +1,235 @@
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Stacks;
using Content.Shared.Interaction;
using Content.Shared.CartridgeLoader;
namespace Content.Server.Objectives.Systems;
public sealed class BloodBrotherSharedStealConditionSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
private EntityQuery<ContainerManagerComponent> _containerQuery;
private HashSet<Entity<TransformComponent>> _nearestEnts = new();
private HashSet<EntityUid> _countedItems = new();
public override void Initialize()
{
base.Initialize();
_containerQuery = GetEntityQuery<ContainerManagerComponent>();
SubscribeLocalEvent<BloodBrotherSharedStealConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
SubscribeLocalEvent<BloodBrotherSharedStealConditionComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
SubscribeLocalEvent<BloodBrotherSharedStealConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnAssigned(Entity<BloodBrotherSharedStealConditionComponent> condition, ref ObjectiveAssignedEvent args)
{
List<StealTargetComponent?> targetList = new();
var query = AllEntityQuery<StealTargetComponent>();
while (query.MoveNext(out var target))
{
if (condition.Comp.StealGroup != target.StealGroup)
continue;
targetList.Add(target);
}
if (targetList.Count == 0 && condition.Comp.VerifyMapExistence)
{
args.Cancelled = true;
return;
}
var maxSize = condition.Comp.VerifyMapExistence
? Math.Min(targetList.Count, condition.Comp.MaxCollectionSize)
: condition.Comp.MaxCollectionSize;
var minSize = condition.Comp.VerifyMapExistence
? Math.Min(targetList.Count, condition.Comp.MinCollectionSize)
: condition.Comp.MinCollectionSize;
condition.Comp.CollectionSize = _random.Next(minSize, maxSize);
}
private void OnAfterAssign(Entity<BloodBrotherSharedStealConditionComponent> condition, ref ObjectiveAfterAssignEvent args)
{
var group = _proto.Index(condition.Comp.StealGroup);
string localizedName = Loc.GetString(group.Name);
var title = condition.Comp.OwnerText == null
? Loc.GetString(condition.Comp.ObjectiveNoOwnerText, ("itemName", localizedName))
: Loc.GetString(condition.Comp.ObjectiveText, ("owner", Loc.GetString(condition.Comp.OwnerText)), ("itemName", localizedName));
var description = condition.Comp.CollectionSize > 1
? Loc.GetString(condition.Comp.DescriptionMultiplyText, ("itemName", localizedName), ("count", condition.Comp.CollectionSize))
: Loc.GetString(condition.Comp.DescriptionText, ("itemName", localizedName));
_metaData.SetEntityName(condition.Owner, title, args.Meta);
_metaData.SetEntityDescription(condition.Owner, description, args.Meta);
_objectives.SetIcon(condition.Owner, group.Sprite, args.Objective);
}
private void OnGetProgress(Entity<BloodBrotherSharedStealConditionComponent> condition, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetProgress(condition.Owner, (args.MindId, args.Mind), condition);
}
private float GetProgress(EntityUid objectiveUid, Entity<MindComponent> mind, BloodBrotherSharedStealConditionComponent condition)
{
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mind.Owner, out var sharedCondition)
&& !_sharedCondition.CheckBaseConditions(mind.Owner, sharedCondition, mind.Comp))
return 0f;
var currentCount = CountStolenItems(mind, condition);
var brotherCount = 0;
if (sharedCondition?.BrotherMind != null && TryComp<MindComponent>(sharedCondition.BrotherMind.Value, out var brotherMind))
brotherCount = CountStolenItems((sharedCondition.BrotherMind.Value, brotherMind), condition);
var totalCount = Math.Max(currentCount, brotherCount);
var result = totalCount / (float)condition.CollectionSize;
return Math.Clamp(result, 0, 1);
}
private int CountStolenItems(Entity<MindComponent> mind, BloodBrotherSharedStealConditionComponent condition)
{
if (mind.Comp.OwnedEntity == null || !_containerQuery.TryGetComponent(mind.Comp.OwnedEntity.Value, out var currentManager))
return 0;
var containerStack = new Stack<ContainerManagerComponent>();
var count = 0;
_countedItems.Clear();
if (condition.CheckStealAreas)
{
var areasQuery = AllEntityQuery<StealAreaComponent, TransformComponent>();
while (areasQuery.MoveNext(out var uid, out var area, out var xform))
{
if (!IsOwnerOfStealArea(uid, mind.Owner, area))
continue;
_nearestEnts.Clear();
_lookup.GetEntitiesInRange(xform.Coordinates, area.Range, _nearestEnts);
foreach (var ent in _nearestEnts)
{
if (!_interaction.InRangeUnobstructed((uid, xform), (ent, ent.Comp), range: area.Range))
continue;
CheckEntity(ent, condition, ref containerStack, ref count);
}
}
}
if (TryComp<PullerComponent>(mind.Comp.OwnedEntity, out var pull))
{
var pulledEntity = pull.Pulling;
if (pulledEntity != null)
{
CheckEntity(pulledEntity.Value, condition, ref containerStack, ref count);
}
}
do
{
foreach (var container in currentManager.Containers.Values)
{
foreach (var entity in container.ContainedEntities)
{
count += CheckStealTarget(entity, condition);
if (_containerQuery.TryGetComponent(entity, out var containerManager))
containerStack.Push(containerManager);
}
}
} while (containerStack.TryPop(out currentManager));
return count;
}
private bool IsOwnerOfStealArea(EntityUid areaUid, EntityUid mindId, StealAreaComponent area)
{
var owners = new HashSet<EntityUid>();
foreach (var owner in area.Owners)
{
owners.Add(owner);
}
return owners.Contains(mindId);
}
private void CheckEntity(EntityUid entity, BloodBrotherSharedStealConditionComponent condition, ref Stack<ContainerManagerComponent> containerStack, ref int counter)
{
counter += CheckStealTarget(entity, condition);
if (!TryComp<MindContainerComponent>(entity, out var pullMind))
{
if (_containerQuery.TryGetComponent(entity, out var containerManager))
containerStack.Push(containerManager);
}
}
private int CheckStealTarget(EntityUid entity, BloodBrotherSharedStealConditionComponent condition)
{
if (_countedItems.Contains(entity))
return 0;
if (!TryComp<StealTargetComponent>(entity, out var target))
return 0;
if (target.StealGroup != condition.StealGroup)
return 0;
if (TryComp<CartridgeComponent>(entity, out var cartridge) &&
cartridge.InstallationStatus is not InstallationStatus.Cartridge)
return 0;
if (condition.CheckAlive)
{
if (TryComp<MobStateComponent>(entity, out var state))
{
if (!_mobState.IsAlive(entity, state))
return 0;
}
}
_countedItems.Add(entity);
return TryComp<StackComponent>(entity, out var stack) ? stack.Count : 1;
}
public void CopySharedStealConditionData(EntityUid sourceObjective, EntityUid targetObjective)
{
if (TryComp<BloodBrotherSharedStealConditionComponent>(sourceObjective, out var sourceCondition)
&& TryComp<BloodBrotherSharedStealConditionComponent>(targetObjective, out var targetCondition))
{
targetCondition.StealGroup = sourceCondition.StealGroup;
targetCondition.VerifyMapExistence = sourceCondition.VerifyMapExistence;
targetCondition.CheckStealAreas = sourceCondition.CheckStealAreas;
targetCondition.CheckAlive = sourceCondition.CheckAlive;
targetCondition.MinCollectionSize = sourceCondition.MinCollectionSize;
targetCondition.MaxCollectionSize = sourceCondition.MaxCollectionSize;
targetCondition.CollectionSize = sourceCondition.CollectionSize;
targetCondition.OwnerText = sourceCondition.OwnerText;
targetCondition.ObjectiveText = sourceCondition.ObjectiveText;
targetCondition.ObjectiveNoOwnerText = sourceCondition.ObjectiveNoOwnerText;
targetCondition.DescriptionText = sourceCondition.DescriptionText;
targetCondition.DescriptionMultiplyText = sourceCondition.DescriptionMultiplyText;
}
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Roles.Components;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a blood brother.
/// </summary>
[RegisterComponent]
public sealed partial class BloodBrotherRoleComponent : BaseMindRoleComponent;

View File

@@ -0,0 +1,9 @@
using Content.Shared.Roles.Components;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a vampire.
/// </summary>
[RegisterComponent]
public sealed partial class VampireRoleComponent : BaseMindRoleComponent;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,598 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Rotting;
using Content.Server.Bible.Components;
using Content.Server.Body.Systems;
using Content.Server.Chat.Systems;
using Content.Server.Polymorph.Systems;
using Content.Shared.Actions;
using Content.Shared.Alert;
using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Mindshield.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.NullRod.Components;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Content.Shared.Genetics;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Surgery.Components;
using Content.Shared.Nutrition.Components;
using Content.Shared.Inventory;
using Content.Shared.Damage.Systems;
using Content.Shared.Damage.Components;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem : SharedVampireSystem
{
[Dependency] private readonly IAdminLogManager _admin = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly BloodstreamSystem _blood = default!;
[Dependency] private readonly FlammableSystem _flammable = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly RottingSystem _rotting = default!;
[Dependency] private readonly StomachSystem _stomach = default!;
[Dependency] private readonly PolymorphSystem _polymorph = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedActionsSystem _action = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
private static readonly ProtoId<EmotePrototype> Scream = "Scream";
private readonly Dictionary<EntityUid, Dictionary<EntityUid, FixedPoint2>> _bloodConsumedTracker = new();
private bool _isDamageBeingHandled = false;
public override void Initialize()
{
base.Initialize();
// Start
SubscribeLocalEvent<VampireComponent, ComponentStartup>(OnStartup);
// Drinking Blood
SubscribeLocalEvent<VampireComponent, VampireDrinkingBloodActionEvent>(OnDrinkBlood);
SubscribeLocalEvent<VampireComponent, VampireDrinkingBloodDoAfterEvent>(DrinkDoAfter);
// Distribute Damage
SubscribeLocalEvent<VampireComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<ThrallComponent, DamageChangedEvent>(OnDamageChanged);
// Thralls
SubscribeLocalEvent<MindShieldComponent, ComponentStartup>(MindShieldImplanted);
InitializePowers();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var vampireQuery = EntityQueryEnumerator<VampireComponent>();
while (vampireQuery.MoveNext(out var uid, out var vampireComponent))
{
if (IsInSpace(uid))
{
if (vampireComponent.NextSpaceDamageTick <= 0)
{
vampireComponent.NextSpaceDamageTick = 1;
DoSpaceDamage((uid, vampireComponent));
}
vampireComponent.NextSpaceDamageTick -= frameTime;
}
if (vampireComponent.NullDamage > 0)
{
if (vampireComponent.NextNullDamageTick <= 0)
{
vampireComponent.NextNullDamageTick = 2;
vampireComponent.NullDamage -= FixedPoint2.New(2);
if (vampireComponent.NullDamage < 0)
{
vampireComponent.NullDamage = FixedPoint2.Zero;
}
}
vampireComponent.NextNullDamageTick -= frameTime;
}
}
var holyPointQuery = EntityQueryEnumerator<HolyPointComponent>();
while (holyPointQuery.MoveNext(out var uid, out var holyPoint))
{
if (holyPoint.NextTimeTick <= 0)
{
holyPoint.NextTimeTick = 10;
var vampires = _entityLookup.GetEntitiesInRange<VampireComponent>(Transform(uid).Coordinates, holyPoint.Range);
foreach (var vampire in vampires)
{
if (vampire.Comp.TruePowerActive) continue;
if (TryComp(vampire.Owner, out FlammableComponent? flammable))
{
flammable.FireStacks = flammable.MaximumFireStacks;
_flammable.Ignite(vampire.Owner, uid);
_chat.TryEmoteWithoutChat(vampire, _prototypeManager.Index(Scream), true);
_popup.PopupEntity(Loc.GetString("vampire-holy-point"), vampire.Owner, vampire.Owner, PopupType.LargeCaution);
}
}
}
holyPoint.NextTimeTick -= frameTime;
}
}
// Update Alerts
private void OnStartup(EntityUid uid, VampireComponent component, ComponentStartup args)
{
_alerts.ShowAlert(uid, component.BloodAlert);
}
#region Drinking blood
private void OnDrinkBlood(EntityUid uid, VampireComponent component, VampireDrinkingBloodActionEvent args)
{
if (TryDrink(uid, component, args))
{
var doAfterDelay = TimeSpan.FromSeconds(3);
var doAfterEventArgs = new DoAfterArgs(EntityManager, uid, doAfterDelay,
new VampireDrinkingBloodDoAfterEvent() { Volume = 5f },
eventTarget: uid,
target: args.Target,
used: args.Target)
{
BreakOnMove = true,
BreakOnDamage = true,
MovementThreshold = 0.01f,
DistanceThreshold = 0.5f,
NeedHand = true
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-countion"), uid, args.Target, PopupType.MediumCaution);
}
}
private bool TryDrink(EntityUid uid, VampireComponent component, VampireDrinkingBloodActionEvent args)
{
if (args.Target == uid)
{
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-self"), uid, uid, PopupType.SmallCaution);
return false;
}
if (!_interaction.InRangeUnobstructed(uid, args.Target, popup: true) || HasComp<SyntheticOperatedComponent>(args.Target))
return false;
IngestionBlockerComponent? blocker;
if (_inventory.TryGetSlotEntity(uid, "mask", out var maskUid) &&
TryComp(maskUid, out blocker) && blocker.Enabled)
return false;
if (_inventory.TryGetSlotEntity(uid, "head", out var headUid) &&
TryComp(headUid, out blocker) && blocker.Enabled)
return false;
if (_rotting.IsRotten(args.Target))
{
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), uid, uid, PopupType.SmallCaution);
return false;
}
if (TryComp<VampireComponent>(args.Target, out var targetVampireComponent))
{
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-not-vampire"), uid, uid, PopupType.SmallCaution);
return false;
}
if (TryComp<ThrallComponent>(args.Target, out var targetThrallComponent))
{
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-not-thrall"), uid, uid, PopupType.SmallCaution);
return false;
}
return true;
}
private void DrinkDoAfter(EntityUid uid, VampireComponent component, ref VampireDrinkingBloodDoAfterEvent args)
{
if (args.Cancelled || !TryComp<BloodstreamComponent>(args.Target, out var targetBloodstream)
|| targetBloodstream?.BloodSolution is null)
return;
if (_rotting.IsRotten(args.Target!.Value))
{
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), args.User, args.User, PopupType.SmallCaution);
return;
}
var victimBloodRemaining = targetBloodstream.BloodSolution.Value.Comp.Solution.Volume;
if (victimBloodRemaining <= 0)
{
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-empty"), uid, uid, PopupType.SmallCaution);
return;
}
var bloodAlreadyConsumed = GetBloodConsumedByVampire(uid, args.Target.Value);
var maxBloodToConsume = 200;
var maxAvailableBlood = (FixedPoint2)Math.Min((float)victimBloodRemaining, (float)(maxBloodToConsume - bloodAlreadyConsumed));
if (maxAvailableBlood <= 0)
{
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-maxed-out"), uid, uid, PopupType.SmallCaution);
return;
}
var volumeToConsume = (FixedPoint2)Math.Min((float)victimBloodRemaining.Value, args.Volume * 2);
_audio.PlayPvs(component.BloodDrainSound, uid, AudioParams.Default.WithVolume(-3f));
_blood.TryModifyBloodLevel(args.Target.Value, -(byte)(volumeToConsume * 0.5f));
if (HasComp<BibleUserComponent>(args.Target) && !component.TruePowerActive)
{
_damage.TryChangeDamage(uid, VampireComponent.HolyDamage, true);
_popup.PopupEntity(Loc.GetString("vampire-ingest-holyblood"), uid, uid, PopupType.LargeCaution);
_admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(uid):user} attempted to drink {volumeToConsume}u of {ToPrettyString(args.Target):target}'s holy blood");
return;
}
else
{
var bloodSolution = _solution.SplitSolution(targetBloodstream.BloodSolution.Value, volumeToConsume);
if (!TryIngestBlood(uid, component, bloodSolution))
{
_solution.AddSolution(targetBloodstream.BloodSolution.Value, bloodSolution);
return;
}
_admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(uid):user} drank {volumeToConsume}u of {ToPrettyString(args.Target):target}'s blood");
if (HasComp<HumanoidAppearanceComponent>(args.Target) && !HasComp<DnaModifiedComponent>(args.Target))
AddBloodEssence(uid, volumeToConsume * 0.95);
SetBloodConsumedByVampire(uid, args.Target.Value, bloodAlreadyConsumed + volumeToConsume);
if (args.Target.HasValue)
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-countion-doafter"), uid, args.Target.Value, PopupType.SmallCaution);
args.Repeat = true;
}
}
private bool TryIngestBlood(EntityUid uid, VampireComponent component, Solution ingestedSolution, bool force = false)
{
if (TryComp<BodyComponent>(uid, out var body) && _body.TryGetBodyOrganEntityComps<StomachComponent>(uid, out var stomachs))
{
var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Owner, ingestedSolution, stomach));
if (firstStomach is null)
{
_popup.PopupEntity(Loc.GetString("vampire-full-stomach"), uid, uid, PopupType.SmallCaution);
return false;
}
return _stomach.TryTransferSolution(firstStomach.Value.Owner, ingestedSolution, firstStomach.Value);
}
return false;
}
private FixedPoint2 GetBloodConsumedByVampire(EntityUid vampireUid, EntityUid targetUid)
{
if (!_bloodConsumedTracker.ContainsKey(vampireUid))
_bloodConsumedTracker[vampireUid] = new Dictionary<EntityUid, FixedPoint2>();
return _bloodConsumedTracker[vampireUid].GetValueOrDefault(targetUid, 0);
}
private void SetBloodConsumedByVampire(EntityUid vampireUid, EntityUid targetUid, FixedPoint2 amount)
{
if (!_bloodConsumedTracker.ContainsKey(vampireUid))
_bloodConsumedTracker[vampireUid] = new Dictionary<EntityUid, FixedPoint2>();
_bloodConsumedTracker[vampireUid][targetUid] = amount;
}
#endregion
#region Blood Manipulation
private bool AddBloodEssence(EntityUid uid, FixedPoint2 quantity)
{
if (quantity < 0 || !TryComp<VampireComponent>(uid, out var vampireComponent))
return false;
vampireComponent.CurrentBlood += quantity;
vampireComponent.TotalBloodDrank += (float)quantity;
Dirty(uid, vampireComponent);
_alerts.ShowAlert(uid, vampireComponent.BloodAlert);
UpdatePowers(uid, vampireComponent);
return true;
}
private bool SubtractBloodEssence(EntityUid uid, FixedPoint2 quantity)
{
if (!TryComp<VampireComponent>(uid, out var vampireComponent))
return false;
var adjustedQuantity = quantity * (1 + vampireComponent.NullDamage.Float() / 100);
if (adjustedQuantity <= 0 || vampireComponent.CurrentBlood < adjustedQuantity)
return false;
vampireComponent.CurrentBlood -= adjustedQuantity;
Dirty(uid, vampireComponent);
_alerts.ShowAlert(uid, vampireComponent.BloodAlert);
return true;
}
private bool CheckBloodEssence(EntityUid uid, FixedPoint2 quantity)
{
if (!TryComp<VampireComponent>(uid, out var vampireComponent))
return false;
var adjustedQuantity = quantity * (1 + vampireComponent.NullDamage.Float() / 100);
return vampireComponent.CurrentBlood >= adjustedQuantity;
}
private void UpdatePowers(EntityUid uid, VampireComponent component)
{
if (component.CurrentEvolution == null)
return;
var currentBlood = component.CurrentBlood;
var vampireClass = component.CurrentEvolution;
var thresholds = GetThresholdsForClass(vampireClass);
foreach (var threshold in thresholds)
{
if (currentBlood >= threshold.Key)
{
foreach (var skill in threshold.Value)
{
if (!HasSkill(component, skill))
{
AddSkill(uid, component, skill);
_admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(uid)}: added {skill} for {vampireClass}.");
}
}
}
}
if (currentBlood >= 1000 && !component.TruePowerActive)
{
MakeImmuneToHoly(uid, component);
}
}
private bool HasSkill(VampireComponent component, string skill)
{
return component.AcquiredSkills.Contains(skill);
}
private void AddSkill(EntityUid uid, VampireComponent component, string skill)
{
if (!HasSkill(component, skill))
{
component.AcquiredSkills.Add(skill);
_action.AddAction(uid, skill);
}
}
private Dictionary<float, List<string>> GetThresholdsForClass(string vampireClass)
{
switch (vampireClass)
{
case "Hemomancer":
return new Dictionary<float, List<string>>
{
{ 150f, new List<string> { "ActionVampireClaws" } },
{ 250f, new List<string> { "ActionVampireBloodTendrils", "ActionVampireBloodBarrier" } },
{ 400f, new List<string> { "ActionVampireSanguinePool" } },
{ 600f, new List<string> { "ActionVampirePredatorSenses" } },
{ 800f, new List<string> { "ActionVampireBloodEruption" } },
{ 1000f, new List<string> { "ActionVampireBloodBringersRite" } }
};
case "Umbrae":
return new Dictionary<float, List<string>>
{
{ 150f, new List<string> { "ActionVampireCloakOfDarkness" } },
{ 250f, new List<string> { "ActionVampireShadowSnare", "ActionVampireSoulAnchor" } },
{ 400f, new List<string> { "ActionVampireDarkPassage" } },
{ 600f, new List<string> { "ActionVampireExtinguish" } },
{ 800f, new List<string> { "ActionVampireShadowBoxing" } },
{ 1000f, new List<string> { "ActionVampireEternalDarkness" } }
};
case "Gargantua":
return new Dictionary<float, List<string>>
{
{ 150f, new List<string> { "ActionVampireBloodSwell" } },
{ 250f, new List<string> { "ActionVampireBloodRush", "ActionVampireSeismicStomp" } },
{ 400f, new List<string> { "ActionVampireBloodSwellAdvanced" } },
{ 600f, new List<string> { "ActionVampireOverwhelmingForce" } },
{ 800f, new List<string> { "ActionDemonicGrasp" } },
{ 1000f, new List<string> { "ActionVampireCharge" } }
};
case "Dantalion":
return new Dictionary<float, List<string>>
{
{ 150f, new List<string> { "ActionEnthrall", "ActionCommune" } },
{ 250f, new List<string> { "ActionPacify", "ActionSubspaceSwap" } },
{ 400f, new List<string> { /*"ActionDeployDecoy",*/"ActionMaxThrallCountUpdate1" } },
{ 600f, new List<string> { "ActionRallyThralls", "ActionMaxThrallCountUpdate2" } },
{ 800f, new List<string> { "ActionBloodBond" } },
{ 1000f, new List<string> { "ActionMassHysteria", "ActionMaxThrallCountUpdate3" } }
};
default:
return new Dictionary<float, List<string>>();
}
}
#endregion
#region Space Damage
private void DoSpaceDamage(Entity<VampireComponent> vampire)
{
_damage.TryChangeDamage(vampire.Owner, VampireComponent.SpaceDamage, true, origin: vampire);
_popup.PopupEntity(Loc.GetString("vampire-startlight-burning"), vampire, vampire, PopupType.LargeCaution);
}
private bool IsInSpace(EntityUid vampireUid)
{
var vampirePosition = _transform.GetMapCoordinates(Transform(vampireUid));
if (!_mapMan.TryFindGridAt(vampirePosition, out _, out _))
return true;
return false;
}
#endregion
#region Distribute Damage
private void OnDamageChanged(EntityUid uid, VampireComponent component, ref DamageChangedEvent args)
{
// Null Rode Damage
if (args.Origin.HasValue && HasComp<BibleUserComponent>(args.Origin.Value) && !component.TruePowerActive)
{
var heldEntity = _hands.GetActiveItem(uid);
if (TryComp<NullRodComponent>(heldEntity, out var nullRodComp))
{
var damageToApply = component.NullDamage > 0
? nullRodComp.NullDamage
: nullRodComp.FirstNullDamage;
component.NullDamage += damageToApply;
component.NullDamage = FixedPoint2.Clamp(component.NullDamage, FixedPoint2.Zero, 120);
}
}
// Distribute Damage
if (_isDamageBeingHandled || !component.IsDamageSharingActive
|| component.ThrallOwned.Count == 0 || args.DamageDelta is null
|| IsNegativeDamage(args.DamageDelta))
return;
_isDamageBeingHandled = true;
_damage.TryChangeDamage(uid, -args.DamageDelta, true);
DistributeDamage(uid, component, args.DamageDelta, ref args);
_isDamageBeingHandled = false;
}
private void OnDamageChanged(EntityUid uid, ThrallComponent component, ref DamageChangedEvent args)
{
if (_isDamageBeingHandled || !TryComp(component.VampireOwner, out VampireComponent? vampire)
|| !vampire.IsDamageSharingActive || args.DamageDelta is null
|| IsNegativeDamage(args.DamageDelta))
return;
_isDamageBeingHandled = true;
_damage.TryChangeDamage(uid, -args.DamageDelta, true);
DistributeDamage(component.VampireOwner.Value, vampire, args.DamageDelta, ref args);
_isDamageBeingHandled = false;
}
private void DistributeDamage(
EntityUid vampireUid,
VampireComponent vampireComponent,
DamageSpecifier damage,
ref DamageChangedEvent args,
EntityUid? excludedEntity = null)
{
if (damage == null)
return;
var participants = new List<EntityUid> { vampireUid };
participants.AddRange(vampireComponent.ThrallOwned.Where(thrall => Exists(thrall) && thrall != excludedEntity));
if (participants.Count == 0)
return;
var sharedDamage = damage / participants.Count;
foreach (var participant in participants)
{
if (!HasComp<DamageableComponent>(participant))
continue;
_damage.TryChangeDamage(participant, sharedDamage, true);
}
}
private bool IsNegativeDamage(DamageSpecifier damage)
{
var totalDamage = damage.DamageDict.Values.Aggregate(FixedPoint2.Zero, (sum, value) => sum + value);
return totalDamage < FixedPoint2.Zero;
}
#endregion
#region Thralls
private void MindShieldImplanted(EntityUid uid, MindShieldComponent comp, ComponentStartup init)
{
if (TryComp<ThrallComponent>(uid, out var thrall))
{
var stunTime = TimeSpan.FromSeconds(4);
var name = Identity.Entity(uid, EntityManager);
if (TryComp<VampireComponent>(thrall.VampireOwner, out var vampire))
{
vampire.ThrallOwned.Remove(uid);
vampire.ThrallCount--;
}
RemComp<ThrallComponent>(uid);
_stun.TryUpdateParalyzeDuration(uid, stunTime);
_popup.PopupEntity(Loc.GetString("thrall-break-control", ("name", name)), uid);
}
}
#endregion
#region True Power
private void MakeImmuneToHoly(EntityUid vampire, VampireComponent component)
{
if (TryComp<ReactiveComponent>(vampire, out var reactive))
{
if (reactive.ReactiveGroups == null)
return;
reactive.ReactiveGroups.Remove("Unholy");
}
component.TruePowerActive = true;
RemComp<UnholyComponent>(vampire);
_popup.PopupEntity(Loc.GetString("vampire-true-power"), vampire, vampire, PopupType.Medium);
}
#endregion
}

View File

@@ -17,9 +17,10 @@ namespace Content.Shared.Communications
public readonly bool CountdownStarted;
public List<string>? AlertLevels;
public string CurrentAlert;
public Color CurrentAlertColor;
public float CurrentAlertDelay;
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, Color currentAlertColor, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
{
CanAnnounce = canAnnounce;
CanCall = canCall;
@@ -27,6 +28,7 @@ namespace Content.Shared.Communications
CountdownStarted = expectedCountdownEnd != null;
AlertLevels = alertLevels;
CurrentAlert = currentAlert;
CurrentAlertColor = currentAlertColor;
CurrentAlertDelay = currentAlertDelay;
}
}

View File

@@ -452,6 +452,24 @@ public abstract partial class SharedStaminaSystem : EntitySystem
_movementMod.TryUpdateMovementStatus(ent.Owner, status.Value, ent.Comp.StunModifierThresholds[closest]);
}
// WyLab-Wega-Start
public void RemoveStaminaDamage(Entity<StaminaComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (ent.Comp.StaminaDamage >= ent.Comp.CritThreshold)
ExitStamCrit(ent);
ent.Comp.StaminaDamage = 0;
AdjustStatus(ent.Owner);
RemComp<ActiveStaminaComponent>(ent);
_status.TryRemoveStatusEffect(ent, StaminaLow);
UpdateStaminaVisuals((ent.Owner, ent.Comp));
Dirty(ent);
}
// WyLab-Wega-End
[Serializable, NetSerializable]
public sealed class StaminaAnimationEvent(NetEntity entity) : EntityEventArgs
{

View File

@@ -23,6 +23,7 @@ public sealed class MovementModStatusSystem : EntitySystem
public static readonly EntProtoId VomitingSlowdown = "VomitingSlowdownStatusEffect";
public static readonly EntProtoId TaserSlowdown = "TaserSlowdownStatusEffect";
public static readonly EntProtoId FlashSlowdown = "FlashSlowdownStatusEffect";
public static readonly EntProtoId Slowdown = "BasicSlowdownStatusEffect"; // WyLab-Wega
public static readonly EntProtoId StatusEffectFriction = "StatusEffectFriction";
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;

View File

@@ -0,0 +1,25 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Blood.Brother;
[RegisterComponent, NetworkedComponent]
public sealed partial class BloodBrotherComponent : Component
{
/// <summary>
/// Mind entity ID of your blood brother
/// </summary>
[DataField]
public EntityUid? BrotherMind;
/// <summary>
/// Whether both brothers must survive for victory
/// </summary>
[DataField]
public bool RequireBothAlive = true;
/// <summary>
/// Whether both brothers must escape for victory
/// </summary>
[DataField]
public bool RequireBothEscape = true;
}

View File

@@ -0,0 +1,121 @@
using Robust.Shared.Configuration;
namespace Content.Shared.CCVar;
[CVarDefs]
public sealed class WegaCVars
{
/*
Ghost Respawn CVars
*/
/// <summary>
/// Whether or not respawning is enabled.
/// </summary>
public static readonly CVarDef<bool> GhostRespawnEnabled =
CVarDef.Create("wega.respawn_enabled", false, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Respawn time, how long the player has to wait in seconds after death.
/// </summary>
public static readonly CVarDef<float> GhostRespawnTime =
CVarDef.Create("wega.respawn_time", 1200.0f, CVar.SERVER | CVar.REPLICATED);
/*
Barks CVars
*/
/// <summary>
/// Responsible for turning on and off the bark system.
/// </summary>
public static readonly CVarDef<bool> BarksEnabled =
CVarDef.Create("wega.barks_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
/// <summary>
/// Default volume setting of Barks sound.
/// </summary>
public static readonly CVarDef<float> BarksVolume =
CVarDef.Create("wega.barks_volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
Night Light System CVars
*/
/// <summary>
/// Responsible for switching the night light system.
/// </summary>
public static readonly CVarDef<bool> NightLightEnabled =
CVarDef.Create("wega.night_light_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
/// <summary>
/// Switching adjusts all the lamps to the holiday mode according to the logic of updating the night lighting.
/// </summary>
public static readonly CVarDef<bool> PartyEnabled =
CVarDef.Create("wega.party_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
/*
Sound insulation CVars
*/
/// <summary>
/// If you enable this mode, it will process the sound with sound isolation.
/// </summary>
public static readonly CVarDef<bool> SoundInsulationEnabled =
CVarDef.Create("wega.sound_insulation_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
/*
Vote CVars
*/
/// <summary>
/// If enabled forcibly, it will trigger a vote for the mode at the end of the round.
/// </summary>
public static readonly CVarDef<bool> VoteRoundEndEnabled =
CVarDef.Create("wega.roundend_vote_enabled", false, CVar.SERVERONLY);
/*
Ic Flavors
*/
/// <summary>
/// Sets the maximum length for OOC flavor text.
/// </summary>
public static readonly CVarDef<int> OOCMaxFlavorTextLength =
CVarDef.Create("ic.oocflavor_text_length", 2048, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Sets the maximum length for character description text.
/// </summary>
public static readonly CVarDef<int> CharacterDescriptionLength =
CVarDef.Create("ic.character_description_length", 2048, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Sets the maximum length for green preferences text.
/// </summary>
public static readonly CVarDef<int> GreenPreferencesLength =
CVarDef.Create("ic.green_preferences_length", 256, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Sets the maximum length for yellow preferences text.
/// </summary>
public static readonly CVarDef<int> YellowPreferencesLength =
CVarDef.Create("ic.yellow_preferences_length", 256, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Sets the maximum length for red preferences text.
/// </summary>
public static readonly CVarDef<int> RedPreferencesLength =
CVarDef.Create("ic.red_preferences_length", 256, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Sets the maximum length for tags text.
/// </summary>
public static readonly CVarDef<int> TagsLength =
CVarDef.Create("ic.tags_length", 128, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Sets the maximum length for links text.
/// </summary>
public static readonly CVarDef<int> LinksLength =
CVarDef.Create("ic.links_length", 512, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Sets the maximum length for NSFW preferences text.
/// </summary>
public static readonly CVarDef<int> NSFWPreferencesLength =
CVarDef.Create("ic.nsfw_preferences_length", 1024, CVar.SERVER | CVar.REPLICATED);
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared.Edible.Matter;
[RegisterComponent]
public sealed partial class EdibleMatterComponent : Component
{
[DataField("nutritionValue")]
public float NutritionValue = 5f;
[DataField("canBeEaten")]
public bool CanBeEaten = true;
}

View File

@@ -0,0 +1,11 @@
using Content.Shared.NPC.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Friendly.Faction;
[RegisterComponent]
public sealed partial class FriendlyFactionComponent : Component
{
[DataField]
public ProtoId<NpcFactionPrototype>? Faction;
}

View File

@@ -0,0 +1,4 @@
namespace Content.Shared.Genetics;
[RegisterComponent]
public sealed partial class DnaModifiedComponent : Component;

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Wega.Ghost.Respawn;
[Serializable, NetSerializable]
public sealed class GhostRespawnEvent(TimeSpan? time) : EntityEventArgs
{
public readonly TimeSpan? Time = time;
}

View File

@@ -0,0 +1,74 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Hallucinations;
[RegisterComponent, Serializable]
public sealed partial class HallucinationsComponent : Component
{
[DataField]
public TimeSpan NextSecond = TimeSpan.Zero;
/// <summary>
/// How far from humanoid can appear hallucination
/// </summary>
[DataField]
public float Range = 7f;
/// <summary>
/// How often (in seconds) hallucinations spawned
/// </summary>
[DataField]
public float SpawnRate = 15f;
/// <summary>
/// Minimum spawn chance per humanoid
/// </summary>
[DataField]
public float MinChance = 0.1f;
/// <summary>
/// Max spawn chance per humanoid
/// </summary>
[DataField]
public float MaxChance = 0.8f;
/// <summary>
/// How much chance increased per spawn
/// </summary>
[DataField]
public float IncreaseChance = 0.1f;
/// <summary>
/// Max spawned hallucinations count for one spawn
/// </summary>
[DataField]
public int MaxSpawns = 5;
/// <summary>
/// How much entities already spawned
/// </summary>
public int SpawnedCount = 0;
/// <summary>
/// Current spawn chance
/// </summary>
[DataField]
public float CurChance = 0.1f;
/// <summary>
/// List of prototypes that are spawned as a hallucination.
/// </summary>
[DataField]
public List<EntProtoId> Spawns = new();
/// <summary>
/// Hallucinations pack proto
/// </summary>
[DataField]
public HallucinationsPrototype? Proto;
/// <summary>
/// Currently selected for hallucinations layer
/// </summary>
public int Layer = 50;
}

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