| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663 |
- using Content.Server.AlertLevel;
- using Content.Server.Audio;
- using Content.Server.Chat.Systems;
- using Content.Server.Explosion.EntitySystems;
- using Content.Server.Pinpointer;
- using Content.Server.Popups;
- using Content.Server.Station.Systems;
- using Content.Shared.Audio;
- using Content.Shared.Containers.ItemSlots;
- using Content.Shared.Coordinates.Helpers;
- using Content.Shared.DoAfter;
- using Content.Shared.Examine;
- using Content.Shared.Maps;
- using Content.Shared.Nuke;
- using Content.Shared.Popups;
- using Robust.Server.GameObjects;
- using Robust.Shared.Audio;
- using Robust.Shared.Audio.Systems;
- using Robust.Shared.Containers;
- using Robust.Shared.Map;
- using Robust.Shared.Map.Components;
- using Robust.Shared.Player;
- using Robust.Shared.Random;
- using Robust.Shared.Utility;
- namespace Content.Server.Nuke;
- public sealed class NukeSystem : EntitySystem
- {
- [Dependency] private readonly AlertLevelSystem _alertLevel = default!;
- [Dependency] private readonly ChatSystem _chatSystem = default!;
- [Dependency] private readonly ExplosionSystem _explosions = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
- [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
- [Dependency] private readonly NavMapSystem _navMap = default!;
- [Dependency] private readonly PointLightSystem _pointLight = default!;
- [Dependency] private readonly PopupSystem _popups = default!;
- [Dependency] private readonly ServerGlobalSoundSystem _sound = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
- [Dependency] private readonly SharedTransformSystem _transform = default!;
- [Dependency] private readonly SharedMapSystem _map = default!;
- [Dependency] private readonly StationSystem _station = default!;
- [Dependency] private readonly UserInterfaceSystem _ui = default!;
- [Dependency] private readonly AppearanceSystem _appearance = default!;
- /// <summary>
- /// Used to calculate when the nuke song should start playing for maximum kino with the nuke sfx
- /// </summary>
- private float _nukeSongLength;
- private ResolvedSoundSpecifier _selectedNukeSong = String.Empty;
- /// <summary>
- /// Time to leave between the nuke song and the nuke alarm playing.
- /// </summary>
- private const float NukeSongBuffer = 1.5f;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<NukeComponent, ComponentInit>(OnInit);
- SubscribeLocalEvent<NukeComponent, ComponentRemove>(OnRemove);
- SubscribeLocalEvent<NukeComponent, MapInitEvent>(OnMapInit);
- SubscribeLocalEvent<NukeComponent, EntInsertedIntoContainerMessage>(OnItemSlotChanged);
- SubscribeLocalEvent<NukeComponent, EntRemovedFromContainerMessage>(OnItemSlotChanged);
- SubscribeLocalEvent<NukeComponent, ExaminedEvent>(OnExaminedEvent);
- // Shouldn't need re-anchoring.
- SubscribeLocalEvent<NukeComponent, AnchorStateChangedEvent>(OnAnchorChanged);
- // ui events
- SubscribeLocalEvent<NukeComponent, NukeAnchorMessage>(OnAnchorButtonPressed);
- SubscribeLocalEvent<NukeComponent, NukeArmedMessage>(OnArmButtonPressed);
- SubscribeLocalEvent<NukeComponent, NukeKeypadMessage>(OnKeypadButtonPressed);
- SubscribeLocalEvent<NukeComponent, NukeKeypadClearMessage>(OnClearButtonPressed);
- SubscribeLocalEvent<NukeComponent, NukeKeypadEnterMessage>(OnEnterButtonPressed);
- // Doafter events
- SubscribeLocalEvent<NukeComponent, NukeDisarmDoAfterEvent>(OnDoAfter);
- }
- private void OnInit(EntityUid uid, NukeComponent component, ComponentInit args)
- {
- component.RemainingTime = component.Timer;
- _itemSlots.AddItemSlot(uid, SharedNukeComponent.NukeDiskSlotId, component.DiskSlot);
- UpdateStatus(uid, component);
- UpdateUserInterface(uid, component);
- }
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- var query = EntityQueryEnumerator<NukeComponent>();
- while (query.MoveNext(out var uid, out var nuke))
- {
- switch (nuke.Status)
- {
- case NukeStatus.ARMED:
- TickTimer(uid, frameTime, nuke);
- break;
- case NukeStatus.COOLDOWN:
- TickCooldown(uid, frameTime, nuke);
- break;
- }
- }
- }
- private void OnMapInit(EntityUid uid, NukeComponent nuke, MapInitEvent args)
- {
- var originStation = _station.GetOwningStation(uid);
- if (originStation != null)
- nuke.OriginStation = originStation;
- else
- {
- var transform = Transform(uid);
- nuke.OriginMapGrid = (transform.MapID, transform.GridUid);
- }
- nuke.Code = GenerateRandomNumberString(nuke.CodeLength);
- }
- private void OnRemove(EntityUid uid, NukeComponent component, ComponentRemove args)
- {
- _itemSlots.RemoveItemSlot(uid, component.DiskSlot);
- }
- private void OnItemSlotChanged(EntityUid uid, NukeComponent component, ContainerModifiedMessage args)
- {
- if (!component.Initialized)
- return;
- if (args.Container.ID != component.DiskSlot.ID)
- return;
- UpdateStatus(uid, component);
- UpdateUserInterface(uid, component);
- }
- #region Anchor
- private void OnAnchorChanged(EntityUid uid, NukeComponent component, ref AnchorStateChangedEvent args)
- {
- UpdateUserInterface(uid, component);
- if (args.Anchored == false && component.Status == NukeStatus.ARMED && component.RemainingTime > component.DisarmDoafterLength)
- {
- // yes, this means technically if you can find a way to unanchor the nuke, you can disarm it
- // without the doafter. but that takes some effort, and it won't allow you to disarm a nuke that can't be disarmed by the doafter.
- DisarmBomb(uid, component);
- }
- UpdateAppearance(uid, component);
- }
- #endregion
- #region UI Events
- private async void OnAnchorButtonPressed(EntityUid uid, NukeComponent component, NukeAnchorMessage args)
- {
- // malicious client sanity check
- if (component.Status == NukeStatus.ARMED)
- return;
- // Nuke has to have the disk in it to be moved
- if (!component.DiskSlot.HasItem)
- {
- var msg = Loc.GetString("nuke-component-cant-anchor-toggle");
- _popups.PopupEntity(msg, uid, args.Actor, PopupType.MediumCaution);
- return;
- }
- // manually set transform anchor (bypassing anchorable)
- // todo: it will break pullable system
- var xform = Transform(uid);
- if (xform.Anchored)
- {
- _transform.Unanchor(uid, xform);
- _itemSlots.SetLock(uid, component.DiskSlot, true);
- }
- else
- {
- if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
- return;
- var worldPos = _transform.GetWorldPosition(xform);
- foreach (var tile in _map.GetTilesIntersecting(xform.GridUid.Value, grid, new Circle(worldPos, component.RequiredFloorRadius), false))
- {
- if (!tile.IsSpace(_tileDefManager))
- continue;
- var msg = Loc.GetString("nuke-component-cant-anchor-floor");
- _popups.PopupEntity(msg, uid, args.Actor, PopupType.MediumCaution);
- return;
- }
- _transform.SetCoordinates(uid, xform, xform.Coordinates.SnapToGrid());
- _transform.AnchorEntity(uid, xform);
- _itemSlots.SetLock(uid, component.DiskSlot, false);
- }
- UpdateUserInterface(uid, component);
- }
- private void OnEnterButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadEnterMessage args)
- {
- if (component.Status != NukeStatus.AWAIT_CODE)
- return;
- UpdateStatus(uid, component);
- UpdateUserInterface(uid, component);
- }
- private void OnKeypadButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadMessage args)
- {
- PlayNukeKeypadSound(uid, args.Value, component);
- if (component.Status != NukeStatus.AWAIT_CODE)
- return;
- if (component.EnteredCode.Length >= component.CodeLength)
- return;
- component.EnteredCode += args.Value.ToString();
- UpdateUserInterface(uid, component);
- }
- private void OnClearButtonPressed(EntityUid uid, NukeComponent component, NukeKeypadClearMessage args)
- {
- _audio.PlayPvs(component.KeypadPressSound, uid);
- if (component.Status != NukeStatus.AWAIT_CODE)
- return;
- component.EnteredCode = "";
- UpdateUserInterface(uid, component);
- }
- private void OnArmButtonPressed(EntityUid uid, NukeComponent component, NukeArmedMessage args)
- {
- if (!component.DiskSlot.HasItem)
- return;
- if (component.Status == NukeStatus.AWAIT_ARM && Transform(uid).Anchored)
- ArmBomb(uid, component);
- else
- {
- DisarmBombDoafter(uid, args.Actor, component);
- }
- }
- #endregion
- #region Doafter Events
- private void OnDoAfter(EntityUid uid, NukeComponent component, DoAfterEvent args)
- {
- if (args.Handled || args.Cancelled)
- return;
- DisarmBomb(uid, component);
- var ev = new NukeDisarmSuccessEvent();
- RaiseLocalEvent(ev);
- args.Handled = true;
- }
- #endregion
- private void TickCooldown(EntityUid uid, float frameTime, NukeComponent? nuke = null)
- {
- if (!Resolve(uid, ref nuke))
- return;
- nuke.CooldownTime -= frameTime;
- if (nuke.CooldownTime <= 0)
- {
- // reset nuke to default state
- nuke.CooldownTime = 0;
- nuke.Status = NukeStatus.AWAIT_ARM;
- UpdateStatus(uid, nuke);
- }
- UpdateUserInterface(uid, nuke);
- }
- private void TickTimer(EntityUid uid, float frameTime, NukeComponent? nuke = null)
- {
- if (!Resolve(uid, ref nuke))
- return;
- nuke.RemainingTime -= frameTime;
- // Start playing the nuke event song so that it ends a couple seconds before the alert sound
- // should play
- if (nuke.RemainingTime <= _nukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong && !ResolvedSoundSpecifier.IsNullOrEmpty(_selectedNukeSong))
- {
- _sound.DispatchStationEventMusic(uid, _selectedNukeSong, StationEventMusicType.Nuke);
- nuke.PlayedNukeSong = true;
- }
- // play alert sound if time is running out
- if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound)
- {
- _sound.PlayGlobalOnStation(uid, _audio.ResolveSound(nuke.AlertSound), new AudioParams{Volume = -5f});
- _sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
- nuke.PlayedAlertSound = true;
- UpdateAppearance(uid, nuke);
- }
- if (nuke.RemainingTime <= 0)
- {
- nuke.RemainingTime = 0;
- ActivateBomb(uid, nuke);
- }
- else
- UpdateUserInterface(uid, nuke);
- }
- private void UpdateStatus(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- switch (component.Status)
- {
- case NukeStatus.AWAIT_DISK:
- if (component.DiskSlot.HasItem)
- component.Status = NukeStatus.AWAIT_CODE;
- break;
- case NukeStatus.AWAIT_CODE:
- if (!component.DiskSlot.HasItem)
- {
- component.Status = NukeStatus.AWAIT_DISK;
- component.EnteredCode = "";
- break;
- }
- // var isValid = _codes.IsCodeValid(uid, component.EnteredCode);
- if (component.EnteredCode == component.Code)
- {
- component.Status = NukeStatus.AWAIT_ARM;
- component.RemainingTime = component.Timer;
- _audio.PlayPvs(component.AccessGrantedSound, uid);
- }
- else
- {
- component.EnteredCode = "";
- _audio.PlayPvs(component.AccessDeniedSound, uid);
- }
- break;
- case NukeStatus.AWAIT_ARM:
- // do nothing, wait for arm button to be pressed
- break;
- case NukeStatus.ARMED:
- // do nothing, wait for arm button to be unpressed
- break;
- }
- }
- private void UpdateUserInterface(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- if (!_ui.HasUi(uid, NukeUiKey.Key))
- return;
- var anchored = Transform(uid).Anchored;
- var allowArm = component.DiskSlot.HasItem &&
- (component.Status == NukeStatus.AWAIT_ARM ||
- component.Status == NukeStatus.ARMED);
- var state = new NukeUiState
- {
- Status = component.Status,
- RemainingTime = (int) component.RemainingTime,
- DiskInserted = component.DiskSlot.HasItem,
- IsAnchored = anchored,
- AllowArm = allowArm,
- EnteredCodeLength = component.EnteredCode.Length,
- MaxCodeLength = component.CodeLength,
- CooldownTime = (int) component.CooldownTime
- };
- _ui.SetUiState(uid, NukeUiKey.Key, state);
- }
- private void PlayNukeKeypadSound(EntityUid uid, int number, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- // This is a C mixolydian blues scale.
- // 1 2 3 C D Eb
- // 4 5 6 E F F#
- // 7 8 9 G A Bb
- var semitoneShift = number switch
- {
- 1 => 0,
- 2 => 2,
- 3 => 3,
- 4 => 4,
- 5 => 5,
- 6 => 6,
- 7 => 7,
- 8 => 9,
- 9 => 10,
- 0 => component.LastPlayedKeypadSemitones + 12,
- _ => 0
- };
- // Don't double-dip on the octave shifting
- component.LastPlayedKeypadSemitones = number == 0 ? component.LastPlayedKeypadSemitones : semitoneShift;
- var opts = component.KeypadPressSound.Params;
- opts = AudioHelpers.ShiftSemitone(opts, semitoneShift).AddVolume(-5f);
- _audio.PlayPvs(component.KeypadPressSound, uid, opts);
- }
- public string GenerateRandomNumberString(int length)
- {
- var ret = "";
- for (var i = 0; i < length; i++)
- {
- var c = (char) _random.Next('0', '9' + 1);
- ret += c;
- }
- return ret;
- }
- #region Public API
- /// <summary>
- /// Force a nuclear bomb to start a countdown timer
- /// </summary>
- public void ArmBomb(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- if (component.Status == NukeStatus.ARMED)
- return;
- var nukeXform = Transform(uid);
- var stationUid = _station.GetStationInMap(nukeXform.MapID);
- // The nuke may not be on a station, so it's more important to just
- // let people know that a nuclear bomb was armed in their vicinity instead.
- // Otherwise, you could set every station to whatever AlertLevelOnActivate is.
- if (stationUid != null)
- _alertLevel.SetLevel(stationUid.Value, component.AlertLevelOnActivate, true, true, true, true);
- var pos = _transform.GetMapCoordinates(uid, xform: nukeXform);
- var x = (int) pos.X;
- var y = (int) pos.Y;
- var posText = $"({x}, {y})";
- // We are collapsing the randomness here, otherwise we would get separate random song picks for checking duration and when actually playing the song afterwards
- _selectedNukeSong = _audio.ResolveSound(component.ArmMusic);
- // warn a crew
- var announcement = Loc.GetString("nuke-component-announcement-armed",
- ("time", (int) component.RemainingTime),
- ("location", FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString((uid, nukeXform)))));
- var sender = Loc.GetString("nuke-component-announcement-sender");
- _chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
- _sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.ArmSound));
- _nukeSongLength = (float) _audio.GetAudioLength(_selectedNukeSong).TotalSeconds;
- // turn on the spinny light
- _pointLight.SetEnabled(uid, true);
- // enable the navmap beacon for people to find it
- _navMap.SetBeaconEnabled(uid, true);
- _itemSlots.SetLock(uid, component.DiskSlot, true);
- if (!nukeXform.Anchored)
- {
- // Admin command shenanigans, just make sure.
- _transform.AnchorEntity(uid, nukeXform);
- }
- component.Status = NukeStatus.ARMED;
- UpdateUserInterface(uid, component);
- UpdateAppearance(uid, component);
- }
- /// <summary>
- /// Stop nuclear bomb timer
- /// </summary>
- public void DisarmBomb(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- if (component.Status != NukeStatus.ARMED)
- return;
- var stationUid = _station.GetOwningStation(uid);
- if (stationUid != null)
- _alertLevel.SetLevel(stationUid.Value, component.AlertLevelOnDeactivate, true, true, true);
- // warn a crew
- var announcement = Loc.GetString("nuke-component-announcement-unarmed");
- var sender = Loc.GetString("nuke-component-announcement-sender");
- _chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
- component.PlayedNukeSong = false;
- _sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.DisarmSound));
- _sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
- // reset nuke remaining time to either itself or the minimum time, whichever is higher
- component.RemainingTime = Math.Max(component.RemainingTime, component.MinimumTime);
- // disable sound and reset it
- component.PlayedAlertSound = false;
- component.AlertAudioStream = _audio.Stop(component.AlertAudioStream);
- // turn off the spinny light
- _pointLight.SetEnabled(uid, false);
- // disable the navmap beacon now that its disarmed
- _navMap.SetBeaconEnabled(uid, false);
- // start bomb cooldown
- _itemSlots.SetLock(uid, component.DiskSlot, false);
- component.Status = NukeStatus.COOLDOWN;
- component.CooldownTime = component.Cooldown;
- UpdateUserInterface(uid, component);
- UpdateAppearance(uid, component);
- }
- /// <summary>
- /// Toggle bomb arm button
- /// </summary>
- public void ToggleBomb(EntityUid uid, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- if (component.Status == NukeStatus.ARMED)
- DisarmBomb(uid, component);
- else
- ArmBomb(uid, component);
- }
- /// <summary>
- /// Force bomb to explode immediately
- /// </summary>
- public void ActivateBomb(EntityUid uid, NukeComponent? component = null,
- TransformComponent? transform = null)
- {
- if (!Resolve(uid, ref component, ref transform))
- return;
- if (component.Exploded)
- return;
- component.Exploded = true;
- _explosions.QueueExplosion(uid,
- component.ExplosionType,
- component.TotalIntensity,
- component.IntensitySlope,
- component.MaxIntensity);
- RaiseLocalEvent(new NukeExplodedEvent()
- {
- OwningStation = transform.GridUid,
- });
- _sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
- Del(uid);
- }
- /// <summary>
- /// Set remaining time value
- /// </summary>
- public void SetRemainingTime(EntityUid uid, float timer, NukeComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
- component.RemainingTime = timer;
- UpdateUserInterface(uid, component);
- }
- #endregion
- private void DisarmBombDoafter(EntityUid uid, EntityUid user, NukeComponent nuke)
- {
- var doAfter = new DoAfterArgs(EntityManager, user, nuke.DisarmDoafterLength, new NukeDisarmDoAfterEvent(), uid, target: uid)
- {
- BreakOnDamage = true,
- BreakOnMove = true,
- NeedHand = true,
- };
- if (!_doAfter.TryStartDoAfter(doAfter))
- return;
- _popups.PopupEntity(Loc.GetString("nuke-component-doafter-warning"), user,
- user, PopupType.LargeCaution);
- }
- private void UpdateAppearance(EntityUid uid, NukeComponent nuke)
- {
- var xform = Transform(uid);
- _appearance.SetData(uid, NukeVisuals.Deployed, xform.Anchored);
- NukeVisualState state;
- if (nuke.PlayedAlertSound)
- state = NukeVisualState.YoureFucked;
- else if (nuke.Status == NukeStatus.ARMED)
- state = NukeVisualState.Armed;
- else
- state = NukeVisualState.Idle;
- _appearance.SetData(uid, NukeVisuals.State, state);
- }
- private void OnExaminedEvent(EntityUid uid, NukeComponent component, ExaminedEvent args)
- {
- if (component.PlayedAlertSound)
- args.PushMarkup(Loc.GetString("nuke-examine-exploding"));
- else if (component.Status == NukeStatus.ARMED)
- args.PushMarkup(Loc.GetString("nuke-examine-armed"));
- if (Transform(uid).Anchored)
- args.PushMarkup(Loc.GetString("examinable-anchored"));
- else
- args.PushMarkup(Loc.GetString("examinable-unanchored"));
- }
- }
- public sealed class NukeExplodedEvent : EntityEventArgs
- {
- public EntityUid? OwningStation;
- }
- /// <summary>
- /// Raised directed on the nuke when its disarm doafter is successful.
- /// So the game knows not to end.
- /// </summary>
- public sealed class NukeDisarmSuccessEvent : EntityEventArgs
- {
- }
|