Files
Delta-v/Content.Server/Ghost/GhostSystem.cs
T
+33 2620eb48a8 upstream merge 2/11 (#2090)
* Fix multiplying colors of differing nullabilities (#32991)

* revert the temporary hack

* Make atmos alert computer colors private variables (#32992)

* Add flash reaction effect (#32377)

add flash reaction effect

* Automatic changelog update

* Fix loneop spawnrate by reverting it to not use the shuttle event system. (#32942)

Fix loneop spawnrate by reverting it to not use the custom shuttle event system.

* Automatic changelog update

* add atmosia to devmap (#32460)

change

* bowls now make drinking sounds (#32819)

* Automatic changelog update

* Add health analyzer unrevivability warning (#32636)

* Add health analyzer unrevivability warning

* Remove errornous comment

* Automatic changelog update

* Removed the name "Hujsak" (#32998)

removed hujsak

* fix pie throwing sound not playing (#33017)

* Automatic changelog update

* Fix TestSuicideByHeldItem and TestSuicideByHeldItemSpreadDamage (#33030)

* make ai speak robotically (#33025)

* Automatic changelog update

* Extends the minimum round time for meteor swarm events (#32876)

* adjusted minimum timers in meteorswarms.yml

* Updated timer minimum from 20 to 15 minutes

* Reduced minimum timer to 10 minutes as a result of other meteor changes

---------

Co-authored-by: august-sun <45527070+august.sun@users.noreply.github.com>

* Automatic changelog update

* add StartDelay bool to actions (#33026)

* add StartDelay bool to actions

* forgot summary

* Updated an incorrect sprite in the smite menu (#33043)

changed the synth sprite used in the icon for the instrumentify smite out for the more accurate supersynth sprite

* fix lava expeds (#33042)

Co-authored-by: deltanedas <@deltanedas:kde.org>

* Automatic changelog update

* Fix separated game screen bumping (#33046)

I don't really understand why RecordedSplitContainer exists but removing it looks identical and fixes the panel bumping occasionally.

* Automatic changelog update

* Fix: Make Plushie Damage Unexaminable (#33061)

Set hidden to true

* fix chameleon projector bot whitelist (#33055)

fix

* Various Vaugely Connected Sprite Updates™: Encryption Keys, Station Map, Brig Timer (#32786)

* various resprites (encryption keys + signal screens + station map)

* brig timer update

* fixes n shit

* Give proto-kinetic crushers, glaives, and daggers better inhands. Update the crusher and glaive icons. (#32212)

* inhands

* better inhands

* indent

* Automatic changelog update

* Make the security belt contain more useful items by default (#32291)

* replace tear gas and flashbang with holobarrier and sec radio

* holobarrier-belt-fix

* revert changes of 32291

* Automatic changelog update

* Add 3 bottle boxes to nanomed plus (#33018)

three bottle boxes in nanomed plus

* Automatic changelog update

* Nukie med bundle now costs 24 tc and contains a unique defibrillator (#32720)

* a-few-injectors

* comment

* defib-ops-when

* Automatic changelog update

* Pill Bottles can only store pills now (#33074)

* no longer absurd

* Update Resources/Prototypes/Entities/Objects/Specific/chemistry.yml

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Automatic changelog update

* Add a spare bible to PietyVend (#32363)

Update chapel.yml

* Automatic changelog update

* [#28722 fix] Add notification for dependent wearables being dropped (#33078)

* add notification for dependent wearables being dropped

* fix dropped item popup redundancy
- did a check to see if any item was dropped, instead of making a notification for each item being dropped.

* change popup to client-only variant

* fix redundant messages, add plural locale string

* fix conventions, fix locale input to be more intuitive

---------

Co-authored-by: Justin <justinbrick1@gmail.com>

* Automatic changelog update

* More pda space (#32601)

* Rebalance the max programs that a PDA can hold

* Give Caps PDA more programs too.

* Make the max programs a static 8

* I forgor sec and med

* CaseCase

* Empty commit to re-run checks

* The final change, I hope.

* Automatic changelog update

* Fixed spelling mistake in water blaster description. (#33087)

Fixed minor spelling mistake

* [#20285 fix] Carp Plush and Rehydratables can now be put into mop bucket (#33079)

* Make shark plush janitor-bucketable

* fix bucketed grey shark texture

* Make sprites less shiny and adapt copyright notice

* Made shark way way less shiny

* Allow carp plush and rehydratables in mop bucket.

* Remove old mop bucket shark sprites

* Fix post-merge bugs

* Fix errors

* Move ReactiveContainer stuff to shared

That should mean it is now predicted.

* Custom eject verb for the mop bucket

* Fixes OnSolutionChange, removes pop-up as there already is one.

* .ftl is not necessary as the custom pop-up was removed

* Review fixes

* Update Content.Shared/Chemistry/Components/ReactiveContainerComponent.cs

* Update Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs

---------

Co-authored-by: Psychpsyo <psychpsyo@gmail.com>
Co-authored-by: Psychpsyo <60073468+Psychpsyo@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Automatic changelog update

* remove now duplicated nyano PlushieCarp tag

* Fixes tailthump breaking positional audio by making it mono (#33092)

* Automatic changelog update

* ExaminableDamage now puts its message at the bottom and in color (#32820)

* the examineableDamage component now puts its messages at the bottom, and in color

* god help us if something is priority -100 :godo:

* Automatic changelog update

* Lower in-round votekick requirements (#32953)

Initial commit

* Allow votekicks to be initiated in the lobby (#32528)

Initial commit

* Automatic changelog update

* Fixes bolt lights of previously unpowered bolted doors (#33063)

fix

* Automatic changelog update

* Muffins (#29318)

* Update meta.json

* Add files via upload

* Update misc.yml

* Update meal_recipes.yml

* Update meta.json

* Add files via upload

* Update plate.yml

* Update food_baked_single.yml

* Update dinnerware.yml

* Update cooking.yml

* Update misc.yml

* Add files via upload

* Delete Resources/Textures/Objects/Consumable/Food/Baked/misc.rsi/muffin-cherry.png

* Add files via upload

* Update meta.json

* Update misc.yml

* Update meal_recipes.yml

* Update meta.json

* Fix meta.json

* Fix meta.json again

* Update misc.yml

* Update misc.yml

* Update misc.yml

* Update misc.yml

* Update meta.json

* Update meta.json

* Update misc.yml

* Update meal_recipes.yml

* Update Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Update dinnerware.yml

* Delete cherry

* Add files via upload

* Delete banana

* Add banana

* Delete chocolate

* Add chocolate

* lathe recipe fix

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Automatic changelog update

* Borgs can no longer see mindshield + AI can no longer toggle off seeing job icons (#33069)

* :(

* Removed the sprite + updated RSI

* Automatic changelog update

* Add CanLoad for biomes (#33050)

CPUJob to come later.

* Remove CargoPallet component from the cargo pallet (#33022)

* Change cargo shuttle pallets to catwalks.

* Remove CargoPallet component from the cargo pallet.

* Undo cargo shuttle changes.

* Automatic changelog update

* Minor antagonist guidebook changes (#32824)

* took a two month nap. accidentally pushed too many buttons. let's try this again. added thieves to antagonists.xml

* even after that nap, i don't feel well-rested at all.

* please don't kill me for using webedit

* capitalization, typo

* Apply suggestions from code review (more period moving)

Thanks Evan, very cool

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* guess you could say im not pro-proper noun

* typo

* Update Resources/ServerInfo/Guidebook/Antagonist/Nuclear Operatives.xml

* ok

Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>

* Give Nukies a Hand Labeler (#33053)

* Add hand-labeler to nukie planet

* Rearrange nukie chem table

* Fix `ItemSlotSystem` popup Logic (#28856)

* move popup call out of `CanInsert` into `OnInteractUsing`

* im stupid and `reason` is completely unnecessary

Signed-off-by: Brandon Li <sirbrandonthenerd@gmail.com>

* return early when `itemSlots.Slots.Count == 0`

* tweak logic for triggering popups

* change popup logic again

* Consolidate whitelist check

* Get any popup message not just last failed slot

* Apply suggestions from code review

Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>

* yoink

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>

---------

Signed-off-by: Brandon Li <sirbrandonthenerd@gmail.com>
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>

* Add cvars to votekick to customize requirements for the initiator. (#32490)

* Automatic changelog update

* Add on-call functionality for adminning (#30443)

* Add on-call functionality for adminning

The first time an ahelp gets SOS it gets relayed to the specified channel with the specified ping. Every time after that it doesn't until it gets a non-SOS response received.

* Remove redundant name

Pretty sure this already gets chucked on the name of the msg itself I think it just didn't show in screenshot because they were subsequent.

* Update Content.Server/Administration/Systems/BwoinkSystem.cs

Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>

---------

Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>
Co-authored-by: deathride58 <deathride58@users.noreply.github.com>

* Automatic changelog update

* Pills are explosion resistant (partially reverts #15851) (#32458)

* idk how to revert a pr so I just deleted some lines

* pill destructible with explosion resistance

* comment for explosion resist

* "and" to "but"

---------

Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>

* Automatic changelog update

* Hasten handcraft gauze recipe & decrease techfab gauze cost (#32744)

* med lathe gauze price reduction

* gauze craft doafter time

* 3 second doafter craft

* Automatic changelog update

* Service workers antagonist fix. (#31359)

* add

* Revert "add"

This reverts commit 25da34b0fead5812fe5800c9bf5dd7b10ef48d7d.

* antagonism allowed™️

* Automatic changelog update

* Adds a new AME sound effect! (#33097)

* Changes the AME sound effect to not be the default MetalThud.

* Was told on discord to make a minor change to autorerun the tests due to the Build & Test Debug failing!

* Attribution and licensing, as requsted by deathride58

* Fixes the high-pitched squeak audible to some people!

* Audio file tweaked by SlamBamActionMan to eliminate a weird squeak they were still able to hear. Thanks!

* Automatic changelog update

* Allow for the ai's laws to be changed from its core and eye (#32461)

* Allow for the ai's laws to be changed from its core and eye

* Address reviews

* Automatic changelog update

* clean up tools lathe recipes (#31521)

* clean up tools lathe recipes

* add medical and cooking tools

* add result

* add result to others

* review

* engine

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>

* Automatic changelog update

* Intercoms and Radios both pick up proximate speech (#32737)

* Deduping of recent messages should consider the channel it's being sent to

* rerun actions

* Automatic changelog update

* Adds headphones to loadouts (#33067)

added headphones to trinkets

* Automatic changelog update

* Cardboard Box Capacity 4 -> 5 (#32743)

* capacity upgrade

* comment update

* remove our duplicate pill whitelist

* fix

* remove broken crew monitor from dev map, how did it even get there

---------

Signed-off-by: Brandon Li <sirbrandonthenerd@gmail.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: deltanedas <@deltanedas:kde.org>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
Co-authored-by: Moomoobeef <62638182+Moomoobeef@users.noreply.github.com>
Co-authored-by: Saphire Lattice <lattice@saphi.re>
Co-authored-by: FluffMe <1780586+FluffMe@users.noreply.github.com>
Co-authored-by: Theodore Lukin <66275205+pheenty@users.noreply.github.com>
Co-authored-by: August Sun <45527070+august-sun@users.noreply.github.com>
Co-authored-by: august-sun <45527070+august.sun@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Preston Smith <92108534+thetolbean@users.noreply.github.com>
Co-authored-by: Milon <plmilonpl@gmail.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Alzore <140123969+Blackern5000@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: Jarmer123 <148848017+Jarmer123@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Justin <justinbrick1@gmail.com>
Co-authored-by: SpaceLizard <the.justice.league.of.canada@gmail.com>
Co-authored-by: Psychpsyo <psychpsyo@gmail.com>
Co-authored-by: Psychpsyo <60073468+Psychpsyo@users.noreply.github.com>
Co-authored-by: deathride58 <deathride58@users.noreply.github.com>
Co-authored-by: PopGamer46 <yt1popgamer@gmail.com>
Co-authored-by: RumiTiger <154005209+RumiTiger@users.noreply.github.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: UBlueberry <161545003+UBlueberry@users.noreply.github.com>
Co-authored-by: Brandon Li <48413902+aspiringLich@users.noreply.github.com>
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Co-authored-by: Vasilis <vasilis@pikachu.systems>
Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Ubaser <134914314+UbaserB@users.noreply.github.com>
Co-authored-by: AftrLite <61218133+AftrLite@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: Centronias <me@centronias.com>
Co-authored-by: joshepvodka <86210200+joshepvodka@users.noreply.github.com>
Co-authored-by: Scribbles0 <91828755+Scribbles0@users.noreply.github.com>
2024-11-07 20:09:25 +00:00

584 lines
24 KiB
C#

using Content.Server.Administration.Logs;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Ghost.Components;
using Content.Server.Mind;
using Content.Server.Roles.Jobs;
using Content.Server.Warps;
using Content.Shared.Actions;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Eye;
using Content.Shared.FixedPoint;
using Content.Shared.Follower;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Storage.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
using System.Numerics;
namespace Content.Server.Ghost
{
public sealed class GhostSystem : SharedGhostSystem
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] private readonly FollowerSystem _followerSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MindSystem _minds = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
private EntityQuery<GhostComponent> _ghostQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
public override void Initialize()
{
base.Initialize();
_ghostQuery = GetEntityQuery<GhostComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
SubscribeLocalEvent<GhostComponent, ComponentStartup>(OnGhostStartup);
SubscribeLocalEvent<GhostComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<GhostComponent, ComponentShutdown>(OnGhostShutdown);
SubscribeLocalEvent<GhostComponent, ExaminedEvent>(OnGhostExamine);
SubscribeLocalEvent<GhostComponent, MindRemovedMessage>(OnMindRemovedMessage);
SubscribeLocalEvent<GhostComponent, MindUnvisitedMessage>(OnMindUnvisitedMessage);
SubscribeLocalEvent<GhostComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<GhostOnMoveComponent, MoveInputEvent>(OnRelayMoveInput);
SubscribeNetworkEvent<GhostWarpsRequestEvent>(OnGhostWarpsRequest);
SubscribeNetworkEvent<GhostReturnToBodyRequest>(OnGhostReturnToBodyRequest);
SubscribeNetworkEvent<GhostWarpToTargetRequestEvent>(OnGhostWarpToTargetRequest);
SubscribeNetworkEvent<GhostnadoRequestEvent>(OnGhostnadoRequest);
SubscribeLocalEvent<GhostComponent, BooActionEvent>(OnActionPerform);
SubscribeLocalEvent<GhostComponent, ToggleGhostHearingActionEvent>(OnGhostHearingAction);
SubscribeLocalEvent<GhostComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
SubscribeLocalEvent<RoundEndTextAppendEvent>(_ => MakeVisible(true));
SubscribeLocalEvent<ToggleGhostVisibilityToAllEvent>(OnToggleGhostVisibilityToAll);
}
private void OnGhostHearingAction(EntityUid uid, GhostComponent component, ToggleGhostHearingActionEvent args)
{
args.Handled = true;
if (HasComp<GhostHearingComponent>(uid))
{
RemComp<GhostHearingComponent>(uid);
_actions.SetToggled(component.ToggleGhostHearingActionEntity, true);
}
else
{
AddComp<GhostHearingComponent>(uid);
_actions.SetToggled(component.ToggleGhostHearingActionEntity, false);
}
var str = HasComp<GhostHearingComponent>(uid)
? Loc.GetString("ghost-gui-toggle-hearing-popup-on")
: Loc.GetString("ghost-gui-toggle-hearing-popup-off");
Popup.PopupEntity(str, uid, uid);
Dirty(uid, component);
}
private void OnActionPerform(EntityUid uid, GhostComponent component, BooActionEvent args)
{
if (args.Handled)
return;
var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius);
var booCounter = 0;
foreach (var ent in entities)
{
var handled = DoGhostBooEvent(ent);
if (handled)
booCounter++;
if (booCounter >= component.BooMaxTargets)
break;
}
args.Handled = true;
}
private void OnRelayMoveInput(EntityUid uid, GhostOnMoveComponent component, ref MoveInputEvent args)
{
// If they haven't actually moved then ignore it.
if ((args.Entity.Comp.HeldMoveButtons &
(MoveButtons.Down | MoveButtons.Left | MoveButtons.Up | MoveButtons.Right)) == 0x0)
{
return;
}
// Let's not ghost if our mind is visiting...
if (HasComp<VisitingMindComponent>(uid))
return;
if (!_minds.TryGetMind(uid, out var mindId, out var mind) || mind.IsVisitingEntity)
return;
if (component.MustBeDead && (_mobState.IsAlive(uid) || _mobState.IsCritical(uid)))
return;
OnGhostAttempt(mindId, component.CanReturn, mind: mind);
}
private void OnGhostStartup(EntityUid uid, GhostComponent component, ComponentStartup args)
{
// Allow this entity to be seen by other ghosts.
var visibility = EnsureComp<VisibilityComponent>(uid);
if (_gameTicker.RunLevel != GameRunLevel.PostRound)
{
_visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Ghost, false);
_visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Normal, false);
_visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility);
}
SetCanSeeGhosts(uid, true);
var time = _gameTiming.CurTime;
component.TimeOfDeath = time;
}
private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args)
{
// Perf: If the entity is deleting itself, no reason to change these back.
if (Terminating(uid))
return;
// Entity can't be seen by ghosts anymore.
if (TryComp(uid, out VisibilityComponent? visibility))
{
_visibilitySystem.RemoveLayer((uid, visibility), (int) VisibilityFlags.Ghost, false);
_visibilitySystem.AddLayer((uid, visibility), (int) VisibilityFlags.Normal, false);
_visibilitySystem.RefreshVisibility(uid, visibilityComponent: visibility);
}
// Entity can't see ghosts anymore.
SetCanSeeGhosts(uid, false);
_actions.RemoveAction(uid, component.BooActionEntity);
}
private void SetCanSeeGhosts(EntityUid uid, bool canSee, EyeComponent? eyeComponent = null)
{
if (!Resolve(uid, ref eyeComponent, false))
return;
if (canSee)
_eye.SetVisibilityMask(uid, eyeComponent.VisibilityMask | (int) VisibilityFlags.Ghost, eyeComponent);
else
_eye.SetVisibilityMask(uid, eyeComponent.VisibilityMask & ~(int) VisibilityFlags.Ghost, eyeComponent);
}
private void OnMapInit(EntityUid uid, GhostComponent component, MapInitEvent args)
{
_actions.AddAction(uid, ref component.BooActionEntity, component.BooAction);
_actions.AddAction(uid, ref component.ToggleGhostHearingActionEntity, component.ToggleGhostHearingAction);
_actions.AddAction(uid, ref component.ToggleLightingActionEntity, component.ToggleLightingAction);
_actions.AddAction(uid, ref component.ToggleFoVActionEntity, component.ToggleFoVAction);
_actions.AddAction(uid, ref component.ToggleGhostsActionEntity, component.ToggleGhostsAction);
}
private void OnGhostExamine(EntityUid uid, GhostComponent component, ExaminedEvent args)
{
var timeSinceDeath = _gameTiming.RealTime.Subtract(component.TimeOfDeath);
var deathTimeInfo = timeSinceDeath.Minutes > 0
? Loc.GetString("comp-ghost-examine-time-minutes", ("minutes", timeSinceDeath.Minutes))
: Loc.GetString("comp-ghost-examine-time-seconds", ("seconds", timeSinceDeath.Seconds));
args.PushMarkup(deathTimeInfo);
}
#region Ghost Deletion
private void OnMindRemovedMessage(EntityUid uid, GhostComponent component, MindRemovedMessage args)
{
DeleteEntity(uid);
}
private void OnMindUnvisitedMessage(EntityUid uid, GhostComponent component, MindUnvisitedMessage args)
{
DeleteEntity(uid);
}
private void OnPlayerDetached(EntityUid uid, GhostComponent component, PlayerDetachedEvent args)
{
DeleteEntity(uid);
}
private void DeleteEntity(EntityUid uid)
{
if (Deleted(uid) || Terminating(uid))
return;
QueueDel(uid);
}
#endregion
private void OnGhostReturnToBodyRequest(GhostReturnToBodyRequest msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {Valid: true} attached
|| !_ghostQuery.TryComp(attached, out var ghost)
|| !ghost.CanReturnToBody
|| !TryComp(attached, out ActorComponent? actor))
{
Log.Warning($"User {args.SenderSession.Name} sent an invalid {nameof(GhostReturnToBodyRequest)}");
return;
}
_mind.UnVisit(actor.PlayerSession);
}
#region Warp
private void OnGhostWarpsRequest(GhostWarpsRequestEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {Valid: true} entity
|| !_ghostQuery.HasComp(entity))
{
Log.Warning($"User {args.SenderSession.Name} sent a {nameof(GhostWarpsRequestEvent)} without being a ghost.");
return;
}
var response = new GhostWarpsResponseEvent(GetPlayerWarps(entity).Concat(GetLocationWarps()).ToList());
RaiseNetworkEvent(response, args.SenderSession.Channel);
}
private void OnGhostWarpToTargetRequest(GhostWarpToTargetRequestEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {Valid: true} attached
|| !_ghostQuery.HasComp(attached))
{
Log.Warning($"User {args.SenderSession.Name} tried to warp to {msg.Target} without being a ghost.");
return;
}
var target = GetEntity(msg.Target);
if (!Exists(target))
{
Log.Warning($"User {args.SenderSession.Name} tried to warp to an invalid entity id: {msg.Target}");
return;
}
WarpTo(attached, target);
}
private void OnGhostnadoRequest(GhostnadoRequestEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {} uid
|| !_ghostQuery.HasComp(uid))
{
Log.Warning($"User {args.SenderSession.Name} tried to ghostnado without being a ghost.");
return;
}
if (_followerSystem.GetMostGhostFollowed() is not {} target)
return;
WarpTo(uid, target);
}
private void WarpTo(EntityUid uid, EntityUid target)
{
if ((TryComp(target, out WarpPointComponent? warp) && warp.Follow) || HasComp<MobStateComponent>(target))
{
_followerSystem.StartFollowingEntity(uid, target);
return;
}
var xform = Transform(uid);
_transformSystem.SetCoordinates(uid, xform, Transform(target).Coordinates);
_transformSystem.AttachToGridOrMap(uid, xform);
if (_physicsQuery.TryComp(uid, out var physics))
_physics.SetLinearVelocity(uid, Vector2.Zero, body: physics);
}
private IEnumerable<GhostWarp> GetLocationWarps()
{
var allQuery = AllEntityQuery<WarpPointComponent>();
while (allQuery.MoveNext(out var uid, out var warp))
{
yield return new GhostWarp(GetNetEntity(uid), warp.Location ?? Name(uid), true);
}
}
private IEnumerable<GhostWarp> GetPlayerWarps(EntityUid except)
{
foreach (var player in _playerManager.Sessions)
{
if (player.AttachedEntity is not {Valid: true} attached)
continue;
if (attached == except) continue;
TryComp<MindContainerComponent>(attached, out var mind);
var jobName = _jobs.MindTryGetJobName(mind?.Mind);
var playerInfo = $"{Comp<MetaDataComponent>(attached).EntityName} ({jobName})";
if (_mobState.IsAlive(attached) || _mobState.IsCritical(attached))
yield return new GhostWarp(GetNetEntity(attached), playerInfo, false);
}
}
#endregion
private void OnEntityStorageInsertAttempt(EntityUid uid, GhostComponent comp, ref InsertIntoEntityStorageAttemptEvent args)
{
args.Cancelled = true;
}
private void OnToggleGhostVisibilityToAll(ToggleGhostVisibilityToAllEvent ev)
{
if (ev.Handled)
return;
ev.Handled = true;
MakeVisible(true);
}
/// <summary>
/// When the round ends, make all players able to see ghosts.
/// </summary>
public void MakeVisible(bool visible)
{
var entityQuery = EntityQueryEnumerator<GhostComponent, VisibilityComponent>();
while (entityQuery.MoveNext(out var uid, out _, out var vis))
{
if (visible)
{
_visibilitySystem.AddLayer((uid, vis), (int) VisibilityFlags.Normal, false);
_visibilitySystem.RemoveLayer((uid, vis), (int) VisibilityFlags.Ghost, false);
}
else
{
_visibilitySystem.AddLayer((uid, vis), (int) VisibilityFlags.Ghost, false);
_visibilitySystem.RemoveLayer((uid, vis), (int) VisibilityFlags.Normal, false);
}
_visibilitySystem.RefreshVisibility(uid, visibilityComponent: vis);
}
}
public bool DoGhostBooEvent(EntityUid target)
{
var ghostBoo = new GhostBooEvent();
RaiseLocalEvent(target, ghostBoo, true);
return ghostBoo.Handled;
}
public EntityUid? SpawnGhost(Entity<MindComponent?> mind, EntityUid targetEntity,
bool canReturn = false)
{
_transformSystem.TryGetMapOrGridCoordinates(targetEntity, out var spawnPosition);
return SpawnGhost(mind, spawnPosition, canReturn);
}
private bool IsValidSpawnPosition(EntityCoordinates? spawnPosition)
{
if (spawnPosition?.IsValid(EntityManager) != true)
return false;
var mapUid = spawnPosition?.GetMapUid(EntityManager);
var gridUid = spawnPosition?.EntityId;
// Test if the map is being deleted
if (mapUid == null || TerminatingOrDeleted(mapUid.Value))
return false;
// Test if the grid is being deleted
if (gridUid != null && TerminatingOrDeleted(gridUid.Value))
return false;
return true;
}
public EntityUid? SpawnGhost(Entity<MindComponent?> mind, EntityCoordinates? spawnPosition = null,
bool canReturn = false)
{
if (!Resolve(mind, ref mind.Comp))
return null;
// Test if the map or grid is being deleted
if (!IsValidSpawnPosition(spawnPosition))
spawnPosition = null;
// If it's bad, look for a valid point to spawn
spawnPosition ??= _gameTicker.GetObserverSpawnPoint();
// Make sure the new point is valid too
if (!IsValidSpawnPosition(spawnPosition))
{
Log.Warning($"No spawn valid ghost spawn position found for {mind.Comp.CharacterName}"
+ $" \"{ToPrettyString(mind)}\"");
_minds.TransferTo(mind.Owner, null, createGhost: false, mind: mind.Comp);
return null;
}
var ghost = SpawnAtPosition(GameTicker.ObserverPrototypeName, spawnPosition.Value);
var ghostComponent = Comp<GhostComponent>(ghost);
// Try setting the ghost entity name to either the character name or the player name.
// If all else fails, it'll default to the default entity prototype name, "observer".
// However, that should rarely happen.
if (!string.IsNullOrWhiteSpace(mind.Comp.CharacterName))
_metaData.SetEntityName(ghost, mind.Comp.CharacterName);
else if (!string.IsNullOrWhiteSpace(mind.Comp.Session?.Name))
_metaData.SetEntityName(ghost, mind.Comp.Session.Name);
if (mind.Comp.TimeOfDeath.HasValue)
{
SetTimeOfDeath(ghost, mind.Comp.TimeOfDeath!.Value, ghostComponent);
}
SetCanReturnToBody(ghostComponent, canReturn);
if (canReturn)
_minds.Visit(mind.Owner, ghost, mind.Comp);
else
_minds.TransferTo(mind.Owner, ghost, mind: mind.Comp);
Log.Debug($"Spawned ghost \"{ToPrettyString(ghost)}\" for {mind.Comp.CharacterName}.");
return ghost;
}
public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null)
{
if (!Resolve(mindId, ref mind))
return false;
var playerEntity = mind.CurrentEntity;
if (playerEntity != null && viaCommand)
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
RaiseLocalEvent(handleEv);
// Something else has handled the ghost attempt for us! We return its result.
if (handleEv.Handled)
return handleEv.Result;
if (mind.PreventGhosting)
{
if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
{
_chatManager.DispatchServerMessage(mind.Session, Loc.GetString("comp-mind-ghosting-prevented"),
true);
}
return false;
}
if (TryComp<GhostComponent>(playerEntity, out var comp) && !comp.CanGhostInteract)
return false;
if (mind.VisitingEntity != default)
{
_mind.UnVisit(mindId, mind: mind);
}
var position = Exists(playerEntity)
? Transform(playerEntity.Value).Coordinates
: _gameTicker.GetObserverSpawnPoint();
if (position == default)
return false;
// Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning.
// There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved.
// + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing)
// Note that we could theoretically be ICly dead and still physically alive and vice versa.
// (For example, a zombie could be dead ICly, but may retain memories and is definitely physically active)
// + If we're in a mob that is critical, and we're supposed to be able to return if possible,
// we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK.
// (If the mob survives, that's a bug. Ghosting is kept regardless.)
var canReturn = canReturnGlobal && _mind.IsCharacterDeadPhysically(mind);
if (_configurationManager.GetCVar(CCVars.GhostKillCrit) &&
canReturnGlobal &&
TryComp(playerEntity, out MobStateComponent? mobState))
{
if (_mobState.IsCritical(playerEntity.Value, mobState))
{
canReturn = true;
//todo: what if they dont breathe lol
//cry deeply
FixedPoint2 dealtDamage = 200;
if (TryComp<DamageableComponent>(playerEntity, out var damageable)
&& TryComp<MobThresholdsComponent>(playerEntity, out var thresholds))
{
var playerDeadThreshold = _mobThresholdSystem.GetThresholdForState(playerEntity.Value, MobState.Dead, thresholds);
dealtDamage = playerDeadThreshold - damageable.TotalDamage;
}
DamageSpecifier damage = new(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), dealtDamage);
_damageable.TryChangeDamage(playerEntity, damage, true);
}
}
if (playerEntity != null)
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " (non-returnable)" : "")}");
var ghost = SpawnGhost((mindId, mind), position, canReturn);
if (ghost == null)
return false;
return true;
}
}
public sealed class GhostAttemptHandleEvent(MindComponent mind, bool canReturnGlobal) : HandledEntityEventArgs
{
public MindComponent Mind { get; } = mind;
public bool CanReturnGlobal { get; } = canReturnGlobal;
public bool Result { get; set; }
}
}