Compare commits

..
767 changed files with 114921 additions and 320299 deletions
+5 -6
View File
@@ -6,13 +6,12 @@ It is highly recommended to read the '''[[Standard Operating Procedure|Standard
* '''[[Standard Operating Procedure#Controlled Items|Controlled Items]]:''' If a member of station crew requires access to a Controlled Item for use in their job, or for regular function, they must obtain the signed and stamped approval of a relevant member of Station Command or the CO. The latter is able to override the decisions of Station Command in this regard, however.
* '''[[Standard Operating Procedure#Entity Definitions|Entity]]:''' Any thing with distinct, independent existence.
* '''[[Standard Operating Procedure#Entity Definitions|Employee]]:''' An entity who has an employment contract with a Company.
* '''[[Standard Operating Procedure#Entity Definitions|Pet]]:''' Any Entity that is provided to Company's Space Stations - either through the station's Logistics Department, or brought on board by the crew - for the purposes of emotional support.
* '''[[Standard Operating Procedure#Security Jurisdiction|Security Jurisdiction]]:''' Station law enforcement officers are bound by Space Law; they cannot operate outside of standard accessible areas, or perform searches, unless such action is allowed by either Alert Procedure or a valid warrant.
* '''[[Standard Operating Procedure#Arrest|Arrest]]:'''Any entity may be arrested, during which they are restrained, informed of their charges, and taken to the brig for search and processing under '''[[Standard Operating Procedure#Brig Procedure|Brig Procedure]]'''.
* '''[[Standard Operating Procedure#Detainment and Arrest|Detainment & Arrest]]:''' Members of law enforcement may detain entities under '''[[Standard Operating Procedure#Probable Cause|Probable Cause]]''' of criminal activity. These detainees must be informed of the crime they are suspected of committing, and may be moved to a secure location and/or restrained while law enforcement determine if a crime was committed. Such detention should last no longer than five minutes. Any entity may be arrested, during which they are restrained, informed of their charges, and taken to the brig for search and processing under '''[[Standard Operating Procedure#Brig Procedure|Brig Procedure]]'''.
* '''[[Standard Operating Procedure#Sentencing|Sentencing]]:''' Any member of law enforcement is allowed to pass sentence on Misdemeanor, Felony, and Grand Felony charges. The Chief Justice, or in their absence, the Head of Security are allowed to pass sentence on all Charges.
* '''[[Standard Operating Procedure#Treatment of Prisoners|Treatment of Prisoners]]:''' Prisoners must be provided with food, water, clothing, free movement within the brig, adequate medical care, access to the Common and Prison radio channels, and moral, spiritual, or legal counseling if requested and available. If the brig becomes uninhabitable, prisoners must be securely and safely relocated to another area until the brig is functional once more. Prisoners sentenced to Extended Confinement have additional rights and firmer restrictions that must be upheld by law enforcement officials.
* '''[[Standard Operating Procedure#Paroles and Pardons|Paroles & Pardons]]:''' Parole may be offered under the discretion of the Warden, Head of Security, or CO. A parole hearing requires only a Judge, as discussed within Trial Procedure. Pardons may be issued by the arresting officer, the Judge of a trial, by the Warden (to those within the permanent brig), or by the CO, when deemed in the best interest of the crew or vessel, or when the circumstances of the crime warrant suspension of sentence.
* '''[[Standard Operating Procedure#Pressing Charges|Pressing Charges]]:''' Any Employee may press charges against any other entity. Individuals may only be charged with a crime if it can be proven beyond reasonable doubt that they have committed the crime.
* '''[[Standard Operating Procedure#Pressing Charges|Pressing Charges]]:''' Any Employee may press charges against any other entity. Individuals may only be charged with a crime if it can be proveny beyond reasonable doubt that they have committed the crime.
* '''[[Standard Operating Procedure#Trial Procedure|Trial Procedure]]:''' A criminal trial’s Judge must be, in sequential order: The Chief Justice, the Court Clerk, an impartial member of Justice or Station Command, or the Warden. The Prosecution should ideally be the Prosecutor, or an available member of Station Command or Station Security in that order. The Defense should ideally be a member of the Justice department assigned to the defendant, though they are allowed to represent themselves if they wish. If insufficient crew are available, the Judge is fully authorized and strongly encouraged to pass a sentence of Extended Confinement until a proper trial can be held.
* '''[[Alert Procedure]]:''' Codes Blue, Red, Gamma, and Delta authorize Security to carry progressively more advanced equipment, and loosen '''[[Standard Operating Procedure#Rules of Engagement|Rules of Engagement]]''' regarding their use. The former two codes disallow openly carrying firearms by non-Security personnel; the latter two suspend many rights to privacy, and grant Security the power to do whatever neccessary to retake or scuttle the station.
@@ -211,7 +210,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|style= "border:1px solid black;"| [[File:SL_Kidnapping.png]]
|style= "border:1px solid black;"| Kidnapping
|style= "border:1px solid black;"| 16 minutes
|style= "border:1px solid black;"| To unlawfully confine or restrict the free movement of an Employee against its will.
|style= "border:1px solid black;"| To unlawfully confine or restrict the free movement of an entity against its will.
|-
| style="border:1px solid black;" | 303
| style="border:1px solid black;" | WIP
@@ -311,7 +310,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|style= "border:1px solid black;"| WIP
!style= "border:1px solid black;"| Identity Theft
|style= "border:1px solid black;"| 5 minutes
|style= "border:1px solid black;"| To maliciously assume the identity of an Employee, such as through writing, attire, and/or speech.
|style= "border:1px solid black;"| To maliciously assume the identity of an entity, such as through writing, attire, and/or speech.
|}
@@ -330,7 +329,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | [[File:SL_AnimalCruelty.png]]
! style="border: 1px solid black;" | {{anchor|Animal Cruelty}}Animal Cruelty
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To inflict unnecessary suffering on a Pet with malicious intent.
| style="border: 1px solid black;" | To inflict unnecessary suffering on a sapient being with malicious intent.
|-
|style= "border:1px solid black;"| 102
|style= "border:1px solid black;"| WIP
+10 -9
View File
@@ -8,6 +8,7 @@ The terms specified below are defined for further use within the procedures.
:* Command Staff/Station Command: Members of the Command department who lead a department aboard the station.
:* Crew: All Employees employed by Station Command, or by NT to reside on the station and perform an established function therein. This includes any Employee listed on the Crew Manifest or issued a functional NT identification and access card. Individuals imprisoned by NT, along with those imprisoned during the course of a shift for violations of Space Law, are considered Crew.
:* Judge: An individual legally appointed to preside over a hearing, as specified within Hearing Procedure.
:* Detainee: Any Employee whose free movement has been curtailed by law enforcement on suspicion of criminal activity. They have not, however, been officially charged with a crime.
:* Arrestee: Any Employee who has been formally charged with a crime.
:* Prisoner: Any Employee who has been sentenced for the commission of a crime, or awaiting a hearing for a capital offense.
:* Permanent Brig/Permabrig/Brig: A secure location for the long-term confinement of Prisoners. Typically outfitted to sustain the basic survival needs of the Prisoners within.
@@ -21,7 +22,7 @@ The following definitions are provided to clarify the legal protections- and obl
== Entity ==
Any individual with a distinct existence. Entities may also encompass a plurality of identities contained within a single physical form. NanoTrasen does not require Entities be offered the protections of Standard Operating Procedure, Space Law, or Rules of Engagement unless further defined below. Security Officers are encouraged to be reasonable with their use of force regarding circumstances, especially if Entity's termination would leave lawsuits or tarnish reputation for NanoTrasen.
== Pet ==
Any Entity that is provided to NanoTrasen Space Stations - either through the station's Logistics Department, or brought on board by the crew - for the purposes of emotional support. Any harm against such a Pet may render the offender liable to the crime of Animal Cruelty.
Any Entity that is provided to NanoTrasen Space Stations- either through the station's Logistics Department, or brought on board by the crew- for the purposes of emotional support. Any harm against such a Pet may render the offender liable to the crime of Animal Cruelty.
Examples of Pets include, but are not limited to:
:* Renault the Fox
:* Smile the Rainbow Slime
@@ -100,27 +101,27 @@ A list of such Substances follow:
== Controlled Armaments ==
Controlled Armaments are grouped into categories of increasing severity. If the offender possesses several categories of contraband, only the highest severity of said items should be charged.
Controlled Armaments are grouped into categories of increasing severity.
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 5min brig and further legal action as the situation demands.''
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 5min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
* Any object or instrument specifically designed, adapted, or used to cause physical harm in close combat
* Any object or equipment specifically designed or adapted to protect against physical harm caused by weapons, projectiles, or other forms of direct assault
* Any object or instrument specifically designed or adapted to incapacitate or disorient by using light or sound without causing permanent injury
* Any object or equipment specifically designed or adapted for explicitly training purposes that would otherwise fall under another category
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 10min brig and further legal action as the situation demands.''
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 10min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
* Any object or instrument specifically designed or adapted to cause physical harm in close combat, which employs a powered system to enhance performance
* Any manually-operated ballistic arms that possess an internal magazine or feeding system, or lack an internal magazine or feeding system altogether
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 15min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
* Any manually-operated ballistic arms that use an external magazine or feeding system
* Any semi-automatic ballistic arms not easily concealable within a pocket or coat
* Any object or instrument specifically designed or adapted for explicitly less-lethal purposes that would otherwise fall under another category
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 20min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
* Any semi-automatic ballistic arms that are easily concealable
* Any fully-automatic ballistic arms
@@ -128,7 +129,7 @@ Controlled Armaments are grouped into categories of increasing severity. If the
* Any object or device specifically designed or adapted to cause physical harm through detonation of an explosive charge
* Any object or equipment specifically designed or adapted to protect against both a space environment and physical harm caused by weapons, projectiles, or other forms of direct assault
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 25min brig and further legal action as the situation demands.''
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 25min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
* Any crew-served or emplaced weapon
* Any object or instrument specifically designed or adapted to launch large-calibre explosives or projectiles for offensive or defensive purposes
@@ -217,7 +218,7 @@ Should the mutiny be unsuccessful, only the head officer(s) who approved the mut
Should the mutiny be successful, the head officer(s) who approved the mutiny must select a new Commanding Officer per Line of Succession. The new CO will then sentence the previous heads of staff for their crimes per Space Law.
== Foreign Vessels ==
Should civilian and military vessels receive a docking request from another neutral vessel, they may approve or deny such a request. Any vessel may, with consent from the docking entity, be searched by law enforcement upon arrival, and its crew granted free movement accompanied by an escort at the discretion of the CO or law enforcement officials. Crew of vessels legally docked on NanoTrasen Stations are considered to be in contract with NanoTrasen and as such are protected by Space Law, Standard operating Procedure and the Rules of Engagement.
Should civilian and military vessels receive a docking request from another neutral vessel, they may approve or deny such a request. Any vessel may, with consent from the docking entity, be searched by law enforcement upon arrival, and its crew granted free movement accompanied by an escort at the discretion of the CO or law enforcement officials.
Crew of foreign vessels may be refused entry, at any time and for any reason, and must be returned to, or be escorted to, their own vessel where applicable. Failure to comply may constitute trespass. Docking vessels bearing a warrant or commission represent an exception, and may not be denied entry or be forced to submit to a search.
@@ -252,7 +253,7 @@ Martial Law may only be declared by Central Command. Under Martial Law Security
* authorised to take whatever actions deemed necessary to neutralise or repel station threats.
* authorized to summarily execute station threats.
* not bound to the Rules of Engagement.
* not obligated to provide any medical care to prisoners, including revival.
* not obligated to provide any medical care to prisoners and detainees, including revival.
== Armory Procedure ==
The armory and its usage are governed by the Warden. The Warden is responsible for stocking, distributing, and recording the collection of weapons used by the station during the shift. They are also afforded the authority to arm security at their discretion, so long as the weaponry distributed is in appropriate response to [[Alert Procedure]].
+24 -76
View File
@@ -22,57 +22,39 @@ Examples:
# Changes to upstream files
Follow a few guidelines when modifying non-DeltaV files, to help us manage our project. (files that are not in `_DV` or `Nyano` folders)
If you make a change to an upstream C# or YAML file **you must add comments on or around the changed lines**.
The comments should clarify what changed, to make conflict resolution simpler when a file is changed upstream.
Primarily, **add comments on or around all new or changed lines** in upstream files. Explain what was changed to make resolving merge conflicts easier; we regularly merge new upstream changes into our project.
For YAML specifically, if you add a new component to a prototype add the comment to the `type: ...` line.
If you just modify some fields of a component, comment the fields instead.
### Changing Upstream YAML .yml files
For C# files, if you are adding a lot of code try to put it in a partial class when it makes sense.
**Add comments on or around any changed lines.**
The exception to this is early merging commits that are going to be cherry picked in the future regardless, there's no harm in leaving them as-is.
If you add a new component to a prototype, add an explanation to the `type: ...` line. Example:
As an aside, fluent (.ftl) files **do not support comments on the same line** as a locale value, so be careful when changing them.
## Examples of comments in upstream files
A single line comment on a changed yml field:
```yml
- type: entity
parent: MobSiliconBase
id: MobSupplyBot
components:
- type: InteractionPopup # DeltaV - Make supplybots pettable
interactSuccessString: petting-success-supplybot
interactFailureString: petting-failure-supplybot
interactSuccessSound:
path: /Audio/Ambience/Objects/periodic_beep.ogg
parent: BasePDA
id: SciencePDA
name: epistemics PDA # DeltaV - Epistemics Department replacing Science
```
Whereas if you just modify some fields of a component, comment the fields instead, using inline or block comments. Examples:
A pair of comments enclosing a list of added items to starting gear:
```yml
- type: entityTable
id: FillLockerWarden
table: !type:AllSelector
children:
- id: ClothingHandsGlovesCombat
- id: ClothingShoesBootsSecurityMagboots # DeltaV - Added security magboots.
- id: ClothingShoesBootsJack
#- id: ClothingOuterCoatWarden # DeltaV - removed for incongruence
#- id: ClothingOuterWinterWarden # DeltaV - removed for incongruence
- id: RubberStampWarden
- id: DoorRemoteArmory
- id: HoloprojectorSecurity
storage:
back:
- EmergencyRollerBedSpawnFolded
# Begin DeltaV additions
- id: WeaponEnergyShotgun
- id: BoxPDAPrisoner
- id: LunchboxSecurityFilledRandom
prob: 0.3
- BodyBagFolded
- Portafib
# End DeltaV additions
```
### Changing Upstream C# .cs files
If you are adding a lot of C# code, then take advantage of partial classes. Put the new code in its own file in the `_DV` folder, if it makes sense.
Otherwise, **add comments on or around any changed lines.**
A comment on a new imported namespace:
```cs
using Content.Server.Psionics.Glimmer; // DeltaV
@@ -95,40 +77,6 @@ private EntityUid Slice(...)
}
```
### Changing Upstream Localization Fluent .ftl files
**Move all changed locale strings to a new DeltaV file** - use a `.ftl` file in the `_DV` folder. Comment out the old strings in the upstream file, and explain that they were moved.
Example:
Commented out old string in `Resources\Locale\en-US\xenoarchaeology\artifact-analyzer.ftl`
```
# DeltaV - moved to _DV file
#analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
#}[/color][/font]
```
The new version of the string in `Resources\Locale\en-US\_DV\xenoarchaeology\artifact-analyzer.ftl`
```
analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
[vagueandspecific] {$vagueInfo} ({$specificInfo})
[vagueonly] {$vagueInfo} (unable to detect details)
[simple] {$specificInfo}
[hidden] Unable to detect (unlock to discover)
*[noinfo] Unlock nodes to gain info
}[/color][/font]
```
Also keep in mind that fluent (.ftl) files **do not support comments on the same line** as a locale value, so be careful when commenting.
### Early merges
We mostly merge upstream changes in big chunks (e.g. a month of upstream PRs at a time), but urgent changes can be merged early, separately.
Early merges are an exception to the above rules - if cherry-picking a PR for an early merge, you don't need to add `#DeltaV` comments, since the code is coming directly from upstream without any changes.
# Mapping
If you want to make changes to a map, get in touch with its maintainer to make sure you don't both make changes at the same time.
@@ -144,11 +92,11 @@ Please limit changelogs on map PRs to **significant** map alterations or additio
Format for map PRs looks like:
```
:cl: Yourname
MAPS:
- add: Mapname: Added fun!
- remove: Mapname: Removed fun!
- tweak: Mapname: Changed fun!
- fix: Mapname: Fixed fun!
MAPS: Mapname
- add: Added fun!
- remove: Removed fun!
- tweak: Changed fun!
- fix: Fixed fun!
```
# Before you submit
@@ -59,9 +59,6 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
SendCartridgeUiReadyEvent(_activeProgram.Value);
}
if (_activeCartridgeUI is IDisposable disposableUi) // Funky: Handle cleanup for popups and such.
disposableUi.Dispose();
_activeCartridgeUI = ui;
_activeUiFragment?.Dispose();
_activeUiFragment = control;
@@ -124,13 +121,8 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
{
base.Dispose(disposing);
if (disposing) // Funky: Handle cleanup for popups and such.
{
if (_activeCartridgeUI is IDisposable disposableUi)
disposableUi.Dispose();
if (disposing)
_activeUiFragment?.Dispose();
}
}
protected CartridgeComponent? RetrieveCartridgeComponent(EntityUid? cartridgeUid)
@@ -1,11 +1,8 @@
using Content.Client.Message;
using Content.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
namespace Content.Client.CartridgeLoader.Cartridges;
@@ -17,18 +14,6 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
public event Action? OnNotificationSwithPressed;
// DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
private static readonly Type[] AllowedTags =
[
typeof(BoldItalicTag),
typeof(BoldTag),
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag),
typeof(MonoTag),
];
public NewsReaderUiFragment()
{
RobustXamlLoader.Load(this);
@@ -46,17 +31,16 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
Author.Visible = true;
PageName.Text = article.Title;
PageText.SetMessage(FormattedMessage.FromMarkupPermissive(article.Content), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
PageText.SetMarkupPermissive(article.Content);
PageNum.Text = $"{targetNum}/{totalNum}";
NotificationSwitch.Text = Loc.GetString(notificationOn ? "news-read-ui-notification-on" : "news-read-ui-notification-off");
var shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
string shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
ShareTime.SetMarkup(Loc.GetString("news-read-ui-time-prefix-text") + " " + shareTime);
var author = Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author ?? Loc.GetString("news-read-ui-no-author")); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
Author.SetMessage(FormattedMessage.FromMarkupPermissive(author), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
Author.SetMarkup(Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author != null ? article.Author : Loc.GetString("news-read-ui-no-author")));
Prev.Disabled = targetNum <= 1;
Next.Disabled = targetNum >= totalNum;
@@ -58,13 +58,6 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
{
_menu?.UpdateState(targets, st.SelectedId);
}
// Begin Delta-V: Override for the name of the chameleon stuff, so you can have items use the chameleon system without being illegal.
if (_menu is { } menu && EntMan.TryGetComponent<ChameleonClothingComponent>(Owner, out var chameleon) && chameleon.WindowTitleOverride is { } titleOverride)
{
_menu.Title = Loc.GetString(titleOverride);
}
// End Delta-V
}
private void OnIdSelected(string selectedId)
@@ -1,12 +1,10 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
@@ -20,18 +18,6 @@ public sealed partial class ArticleEditorPanel : Control
private bool _preview;
// DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
private static readonly Type[] AllowedTags =
[
typeof(BoldItalicTag),
typeof(BoldTag),
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag),
typeof(MonoTag),
];
public ArticleEditorPanel()
{
RobustXamlLoader.Load(this);
@@ -43,7 +29,7 @@ public sealed partial class ArticleEditorPanel : Control
// Customize scrollbar width and margin. This is not possible in xaml
var scrollbar = ContentField.GetChild(1);
scrollbar.SetWidth = 6f;
scrollbar.Margin = new Thickness(9, 0, 2, 0);
scrollbar.Margin = new Thickness(9, 0, 2 , 0);
RichTextInfoLabel.TooltipSupplier = sender =>
{
@@ -95,9 +81,7 @@ public sealed partial class ArticleEditorPanel : Control
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
var articleBody = Rope.Collapse(ContentField.TextRope); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
PreviewLabel.SetMessage(FormattedMessage.FromMarkupPermissive(articleBody), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope));
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
@@ -19,9 +19,5 @@
Align="Center"
Access="Public"/>
</BoxContainer>
<Label Name='WarningLabel'
Align="Center"
StyleClasses="WindowFooterText"
Text="{Loc 'war-declarator-standards-warning'}" /> <!-- DeltaV - warops LRP announcement warning -->
</BoxContainer>
</controls:FancyWindow>
+3 -3
View File
@@ -146,7 +146,7 @@ namespace Content.Client.PDA
if (state.PdaOwnerInfo.ActualOwnerName != null)
{
_pdaOwner = FormattedMessage.EscapeText(state.PdaOwnerInfo.ActualOwnerName); // DeltaV - Escape actual owner
_pdaOwner = state.PdaOwnerInfo.ActualOwnerName;
PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
("actualOwnerName", _pdaOwner)));
PdaOwnerLabel.Visible = true;
@@ -159,8 +159,8 @@ namespace Content.Client.PDA
if (state.PdaOwnerInfo.IdOwner != null || state.PdaOwnerInfo.JobTitle != null)
{
_owner = FormattedMessage.EscapeText(state.PdaOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")); // DeltaV - Escape owner and job
_jobTitle = FormattedMessage.EscapeText(state.PdaOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned")); // DeltaV - Escape owner and job
_owner = state.PdaOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown");
_jobTitle = state.PdaOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned");
IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
("owner", _owner),
("jobTitle", _jobTitle)));
@@ -714,13 +714,6 @@ public sealed partial class ChatUIController : UIController
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null);
else
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel);
// DeltaV begin - refresh indicator change
if (CurrentChannel != prefixChannel)
_typingIndicator?.ClientSubmittedChatText();
CurrentChannel = prefixChannel;
// DeltaV end
}
public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel) SplitInputContents(string text)
@@ -238,7 +238,6 @@ public partial class ChatBox : UIWidget
return;
ChatInput.ChannelSelector.Select(toSelect);
_controller.CurrentChannel = toSelect; // DeltaV - Alt Chat Indicators
}
private void OnInputKeyBindDown(GUIBoundKeyEventArgs args)
@@ -271,14 +270,12 @@ public partial class ChatBox : UIWidget
_controller.UpdateSelectedChannel(this);
// Warn typing indicator about change
// _controller.NotifyChatTextChange(); // DeltaV
_controller.NotifySpecificChatTextChange(SelectedChannel); // DeltaV - Alt Chat Indicators
_controller.NotifyChatTextChange();
}
private void OnFocusEnter(LineEditEventArgs args)
{
// Warn typing indicator about focus
_controller.CurrentChannel = SelectedChannel; // DeltaV - Alt Chat Indicators
_controller.NotifyChatFocus(true);
}
@@ -3,10 +3,12 @@ using Content.Client.Ghost;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Client.UserInterface.Systems.Ghost.Widgets;
using Content.Shared.Ghost;
using Robust.Client.Console; // Frontier
using Robust.Shared.Console; // Frontier
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Content.Shared._Corvax.Respawn; // Frontier
using Content.Client._Corvax.Respawn; // Frontier
using Content.Shared._NF.CCVar; // Frontier
using Robust.Shared.Configuration; // Frontier
namespace Content.Client.UserInterface.Systems.Ghost;
@@ -19,9 +21,7 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
[Dependency] private readonly IConfigurationManager _cfg = default!; // Frontier
[UISystemDependency] private readonly GhostSystem? _system = default;
// Updated when get death time from the server.
private TimeSpan? DeathTime;
[UISystemDependency] private readonly RespawnSystem? _respawn = default; // Frontier
private GhostGui? Gui => UIManager.GetActiveUIWidgetOrNull<GhostGui>();
@@ -32,9 +32,6 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
gameplayStateLoad.OnScreenUnload += OnScreenUnload;
// DeltaV
SubscribeNetworkEvent<RespawnResetEvent>(OnRespawnReseted);
}
private void OnScreenLoad()
@@ -67,14 +64,23 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
system.GhostRoleCountUpdated -= OnRoleCountUpdated;
}
// Begin DeltaV
private void OnRespawnReseted(RespawnResetEvent ev, EntitySessionEventArgs args)
// Begin Frontier
public void OnSystemLoaded(RespawnSystem system)
{
DeathTime = ev.Time;
UpdateGui();
UpdateRespawn();
system.RespawnReseted += OnRespawnReseted;
}
// End DeltaV
public void OnSystemUnloaded(RespawnSystem system)
{
system.RespawnReseted -= OnRespawnReseted;
}
private void OnRespawnReseted()
{
UpdateGui();
UpdateRespawn(_respawn?.RespawnResetTime);
}
// End Frontier
public void UpdateGui()
{
@@ -88,9 +94,9 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
}
// Begin Frontier
private void UpdateRespawn()
private void UpdateRespawn(TimeSpan? timeOfDeath)
{
Gui?.UpdateRespawn(DeathTime);
Gui?.UpdateRespawn(timeOfDeath);
}
// End Frontier
@@ -110,7 +116,7 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
return;
Gui.Visible = true;
UpdateRespawn(); // Frontier, DeltaV
UpdateRespawn(_respawn?.RespawnResetTime); // Frontier
UpdateGui();
}
@@ -197,4 +203,11 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
{
_system?.OpenGhostRoles();
}
// Begin Frontier
private void RespawnPressed()
{
IoCManager.Resolve<IClientConsoleHost>().RemoteExecuteCommand(null, "ghostrespawn");
}
// End Frontier
}
@@ -1,8 +1,7 @@
using Content.Shared.Research.Components;
using Content.Shared._DV.Xenoarchaeology.BUI;// DeltaV
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.UserInterface;
using JetBrains.Annotations;
using Robust.Client.UserInterface; // DeltaV
namespace Content.Client.Xenoarchaeology.Ui;
@@ -55,15 +54,5 @@ public sealed class AnalysisConsoleBoundUserInterface(EntityUid owner, Enum uiKe
_consoleMenu?.Dispose();
}
// DeltaV
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not AnalysisConsoleBoundUserInterfaceState consoleState)
return;
_consoleMenu?.UpdateState(consoleState.Mult, consoleState.Ratio);
}
}
@@ -29,9 +29,7 @@
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<RichTextLabel Name="GlimmerSumLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/> <!-- DeltaV -->
<RichTextLabel Name="ExtractionSumLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<RichTextLabel Name="MultLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/> <!-- DeltaV -->
</BoxContainer>
</PanelContainer>
</BoxContainer>
@@ -1,4 +1,3 @@
using System.Numerics; // DeltaV
using System.Text;
using Content.Client.Message;
using Content.Client.Resources;
@@ -22,7 +21,7 @@ namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class AnalysisConsoleMenu : FancyWindow
{
private static readonly TimeSpan ExtractInfoDisplayForDuration = TimeSpan.FromSeconds(5);// DeltaV
private static readonly TimeSpan ExtractInfoDisplayForDuration = TimeSpan.FromSeconds(3);
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
@@ -37,14 +36,8 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
private Entity<XenoArtifactNodeComponent>? _currentNode;
private TimeSpan? _hideExtractInfoIn;
// Begin DeltaV - Variables for extraction calculation display
private int _extractionSum;
private int _glimmerSum;
private float _multValue;
private float _ratioValue;
private bool _pressed;
// End DeltaV
public event Action? OnServerSelectionButtonPressed;
public event Action? OnExtractButtonPressed;
@@ -72,12 +65,7 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
OnServerSelectionButtonPressed?.Invoke();
};
// DeltaV
ExtractButton.OnPressed += _ =>
{
_pressed = true;
OnExtractButtonPressed?.Invoke();
};
ExtractButton.OnPressed += StartExtract;
}
/// <summary>
@@ -96,20 +84,7 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
Update(_owner);
}
// DeltaV
public void UpdateState(float mult, float ratio)
{
_multValue = mult;
_ratioValue = ratio;
if (_pressed)
{
_pressed = false;
StartExtract();
return;
}
}
private void StartExtract() // DeltaV - args changed
private void StartExtract(BaseButton.ButtonEventArgs obj)
{
if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
return;
@@ -117,8 +92,7 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ExtractContainer.Visible = true;
NodeViewContainer.Visible = false;
_extractionSum = 0;// DeltaV
_glimmerSum = 0;// DeltaV
_extractionSum = 0;
var extractionMessage = new FormattedMessage();
var nodes = _xenoArtifact.GetAllNodes(artifact.Value);
@@ -126,21 +100,16 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
var count = 0;
foreach (var node in nodes)
{
var pointValue = _xenoArtifact.GetResearchValue(node) * _multValue;// DeltaV
var glimmerValue = (pointValue / _ratioValue / _multValue);// DeltaV
var pointValue = _xenoArtifact.GetResearchValue(node);
if (pointValue <= 0)
continue;
count++;
var nodeId = _xenoArtifact.GetNodeId(node);
_extractionSum += (int)pointValue;// DeltaV
_glimmerSum += (int)(glimmerValue);// DeltaV
var textPoints = Loc.GetString("analysis-console-extract-value", ("id", nodeId), ("value", (int)(pointValue)));
var textGlimmer = Loc.GetString("analysis-console-glimmer-value", ("id", nodeId), ("value", (int)(glimmerValue)));// DeltaV
extractionMessage.AddMarkupOrThrow(textPoints);
extractionMessage.PushNewline();
extractionMessage.AddMarkupOrThrow(textGlimmer);// DeltaV
var text = Loc.GetString("analysis-console-extract-value", ("id", nodeId), ("value", pointValue));
extractionMessage.AddMarkupOrThrow(text);
extractionMessage.PushNewline();
}
@@ -151,12 +120,10 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ExtractionResearchLabel.SetMessage(extractionMessage);
ExtractionSumLabel.SetMarkup(Loc.GetString("analysis-console-extract-sum", ("value", _extractionSum)));// DeltaV
GlimmerSumLabel.SetMarkup(Loc.GetString("analysis-console-glimmer-sum", ("value", _glimmerSum)));// DeltaV
MultLabel.SetMarkup(Loc.GetString("analysis-console-glimmer-mult", ("value", _multValue.ToString("F2"))));// DeltaV
ExtractionSumLabel.SetMarkup(Loc.GetString("analysis-console-extract-sum", ("value", _extractionSum)));
_audio.PlayGlobal(_owner.Comp.ScanFinishedSound, _owner, AudioParams.Default.WithVolume(1f));
OnExtractButtonPressed?.Invoke();
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -235,34 +202,14 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
var hasInfo = _xenoArtifact.HasUnlockedPredecessor(artifact.Value, node.Value);
// DeltaV - start of hide locked node effects
var specificInfo = node.Value.Comp.EffectTipSpecific ?? "xenoarch-effect-tip-unknown";
if (!hasInfo || (node.Value.Comp.LockedEffectTipHidden && node.Value.Comp.Locked))
{
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", hasInfo ? XenoArtifactEffectVisibility.Hidden : XenoArtifactEffectVisibility.NoInfo)));
}
else if (!node.Value.Comp.LockedEffectTipHidden && node.Value.Comp.LockedEffectTipVague && node.Value.Comp.EffectTipVague != null)
{
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", node.Value.Comp.Locked ? XenoArtifactEffectVisibility.VagueOnly : XenoArtifactEffectVisibility.VagueAndSpecific),
("specificInfo", Loc.GetString(specificInfo)),
("vagueInfo", Loc.GetString(node.Value.Comp.EffectTipVague))));
}
else
{
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", XenoArtifactEffectVisibility.Simple),
("specificInfo", Loc.GetString(specificInfo))));
}
// DeltaV - end of hide locked node effects
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", hasInfo),
("info", _ent.GetComponentOrNull<MetaDataComponent>(node.Value)?.EntityDescription ?? string.Empty)));
var predecessorNodes = _xenoArtifact.GetPredecessorNodes(artifact.Value.Owner, node.Value);
if (!hasInfo)
{
// DeltaV - start of hide locked node effects
TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value", ("state", XenoArtifactEffectVisibility.NoInfo)));
// DeltaV - end of hide locked node effects
TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value", ("state", false)));
}
else
{
@@ -282,16 +229,5 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ClassValueLabel.SetMarkup(Loc.GetString("analysis-console-info-class-value",
("class", Loc.GetString($"artifact-node-class-{Math.Min(6, predecessorNodes.Count + 1)}"))));
}
// DeltaV - start of hide locked node effects
public enum XenoArtifactEffectVisibility : sbyte
{
NoInfo = 0,
Hidden = 1,
Simple = 2,
VagueOnly = 3,
VagueAndSpecific = 4,
}
// DeltaV - end of hide locked node effects
}
@@ -1,4 +1,3 @@
using Content.Client._DV.Xenoarchaeology.Ui;
using Robust.Client.UserInterface;
namespace Content.Client.Xenoarchaeology.Ui;
@@ -8,19 +7,15 @@ namespace Content.Client.Xenoarchaeology.Ui;
/// </summary>
public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
// DeltaV - start of node scanner overhaul
[ViewVariables]
private DVNodeScannerDisplay? _scannerDisplay;
// DeltaV - end of node scanner overhaul
private NodeScannerDisplay? _scannerDisplay;
/// <inheritdoc />
protected override void Open()
{
base.Open();
// DeltaV - start of node scanner overhaul
_scannerDisplay = this.CreateWindow<DVNodeScannerDisplay>();
// DeltaV - end of node scanner overhaul
_scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
_scannerDisplay.SetOwner(Owner);
}
@@ -0,0 +1,20 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'node-scan-display-title'}"
MinSize="305 180"
SetSize="305 180"
Resizable="False"
>
<BoxContainer Orientation="Vertical" >
<controls:StripeBack>
<Label Name="NodeScannerState" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
<BoxContainer Orientation="Horizontal">
<Label Name="NoActiveNodeDataLabel" Text="{Loc 'node-scan-no-data'}" Margin="45 25 0 0" MinHeight="47" />
<GridContainer Name="ActiveNodesList" Columns="4" Rows="2" Visible="True" MinHeight="72" />
</BoxContainer>
<controls:StripeBack>
<Label Name="ArtifactStateLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>
@@ -0,0 +1,150 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.NameIdentifier;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class NodeScannerDisplay : FancyWindow
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IGameTiming _timing= default!;
private readonly SharedXenoArtifactSystem _artifact;
private TimeSpan? _nextUpdate;
private EntityUid _owner;
private TimeSpan _updateFromAttachedFrequency;
private readonly HashSet<string> _triggeredNodeNames = new();
public NodeScannerDisplay()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_artifact = _ent.System<SharedXenoArtifactSystem>();
}
/// <summary>
/// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
/// Closes window if <see cref="NodeScannerComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid scannerEntityUid)
{
if (!_ent.TryGetComponent<NodeScannerComponent>(scannerEntityUid, out var scannerComponent))
{
Close();
return;
}
_updateFromAttachedFrequency = scannerComponent.DisplayDataUpdateInterval;
_owner = scannerEntityUid;
}
/// <inheritdoc />
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if(_nextUpdate != null && _timing.CurTime < _nextUpdate)
return;
_nextUpdate = _timing.CurTime + _updateFromAttachedFrequency;
if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
{
Update(false, ArtifactState.None);
return;
}
var attachedArtifactEnt = connectedScanner.AttachedTo;
if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
return;
_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
_triggeredNodeNames.Clear();
ArtifactState artifactState;
if (unlockingComponent == null)
{
var timeToUnlockAvailable = artifactComponent.NextUnlockTime - _timing.CurTime;
artifactState = timeToUnlockAvailable > TimeSpan.Zero
? ArtifactState.Cooldown
: ArtifactState.Ready;
}
else
{
var triggeredIndexes = unlockingComponent.TriggeredNodeIndexes;
foreach (var triggeredIndex in triggeredIndexes)
{
var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
var triggeredNodeName = (_ent.GetComponentOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
_triggeredNodeNames.Add(triggeredNodeName);
}
artifactState = ArtifactState.Unlocking;
}
Update(true, artifactState, _triggeredNodeNames);
}
/// <summary>
/// Updates labels with scanned artifact data and list of triggered nodes from component.
/// </summary>
private void Update(bool isConnected, ArtifactState artifactState, HashSet<string>? triggeredNodeNames = null)
{
ArtifactStateLabel.Text = GetStateText(artifactState);
NodeScannerState.Text = isConnected
? Loc.GetString("node-scanner-artifact-connected")
: Loc.GetString("node-scanner-artifact-non-connected");
ActiveNodesList.Children.Clear();
if (triggeredNodeNames == null)
return;
if (triggeredNodeNames.Count > 0)
{
// show list of triggered nodes instead of 'no data' placeholder
NoActiveNodeDataLabel.Visible = false;
ActiveNodesList.Visible = true;
foreach (var nodeId in triggeredNodeNames)
{
var nodeLabel = new Button
{
Text = nodeId,
Margin = new Thickness(15, 5, 0, 0),
MaxHeight = 40,
Disabled = true
};
ActiveNodesList.Children.Add(nodeLabel);
}
}
else
{
// clear list of activated nodes (done previously), show 'no data' placeholder
NoActiveNodeDataLabel.Visible = true;
ActiveNodesList.Visible = false;
}
}
private string GetStateText(ArtifactState state)
{
return state switch
{
ArtifactState.None => "\u2800", // placeholder for line to not be squeezed
ArtifactState.Ready => Loc.GetString("node-scanner-artifact-state-ready"),
ArtifactState.Unlocking => Loc.GetString("node-scanner-artifact-state-unlocking"),
ArtifactState.Cooldown => Loc.GetString("node-scanner-artifact-state-cooldown"),
_ => throw new ArgumentException("Invalid state"),
};
}
}
@@ -0,0 +1,22 @@
using Content.Shared._Corvax.Respawn;
namespace Content.Client._Corvax.Respawn;
public sealed class RespawnSystem : EntitySystem
{
public TimeSpan? RespawnResetTime { get; private set; }
public event Action? RespawnReseted;
public override void Initialize()
{
SubscribeNetworkEvent<RespawnResetEvent>(OnRespawnReset);
}
private void OnRespawnReset(RespawnResetEvent e)
{
RespawnResetTime = e.Time;
RespawnReseted?.Invoke();
}
}
+1 -2
View File
@@ -6,7 +6,6 @@ namespace Content.Client._DV.Biscuit;
public sealed class BiscuitSystem : VisualizerSystem<BiscuitVisualsComponent>
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
protected override void OnAppearanceChange(EntityUid uid, BiscuitVisualsComponent component,
ref AppearanceChangeEvent args)
@@ -16,7 +15,7 @@ public sealed class BiscuitSystem : VisualizerSystem<BiscuitVisualsComponent>
_appearance.TryGetData(uid, BiscuitStatus.Cracked, out bool cracked);
_sprite.LayerSetVisible((uid, args.Sprite), BiscuitVisualLayers.Top, !cracked);
args.Sprite.LayerSetVisible(BiscuitVisualLayers.Top, !cracked);
}
}
@@ -26,16 +26,6 @@
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Group chat icon -->
<TextureRect Name="GroupIcon"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
MinSize="16 16"
MaxSize="16 16"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
Margin="4 0 6 0"
Visible="False" />
<!-- Text container -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
@@ -4,7 +4,6 @@ using Content.Shared.Access.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
@@ -39,12 +38,10 @@ public sealed partial class NanoChatEntry : BoxContainer
_pressHandler = _ => OnPressed?.Invoke(_number);
ChatButton.OnPressed += _pressHandler;
// FIXME: Truncate and Escape can lead to stray `\`, if the truncate cuts a markup tag in half
NameLabel.Text = FormattedMessage.EscapeText(SharedNanoChatSystem.Truncate(recipient.Name, _maxNameLength));
JobLabel.Text = FormattedMessage.EscapeText(SharedNanoChatSystem.Truncate(recipient.JobTitle ?? string.Empty, _maxIdJobLength));
JobLabel.Visible = !string.IsNullOrWhiteSpace(JobLabel.Text);
NameLabel.Text = SharedNanoChatSystem.Truncate(recipient.Name, _maxNameLength);
JobLabel.Text = SharedNanoChatSystem.Truncate(recipient.JobTitle ?? "", _maxIdJobLength);
JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle);
UnreadIndicator.Visible = recipient.HasUnread;
GroupIcon.Visible = recipient.IsGroup;
ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null;
}
@@ -1,12 +1,9 @@
using System.Linq;
using System.Numerics;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
@@ -29,67 +26,12 @@ public sealed partial class NanoChatLookupView : PanelContainer
return;
}
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var departmentGroups = new Dictionary<DepartmentPrototype, List<NanoChatRecipient>>();
var noDepartmentContacts = new List<NanoChatRecipient>();
foreach (var contact in contacts)
for (var idx = 0; idx < contacts.Count; idx++)
{
if (!string.IsNullOrWhiteSpace(contact.Department) &&
prototypeManager.TryIndex<DepartmentPrototype>(contact.Department, out var dept))
{
departmentGroups.GetOrNew(dept).Add(contact);
}
else
{
noDepartmentContacts.Add(contact);
}
}
var rowIndex = 0;
foreach (var (department, departmentContacts) in departmentGroups.OrderBy(entry => entry.Key, DepartmentUIComparer.Instance))
{
departmentContacts.Sort();
// Add department header
ContactsList.AddChild(new Label
{
Text = Loc.GetString(department.Name),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, rowIndex == 0 ? 0 : 8, 0, 2)
});
foreach (var contact in departmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
ContactsList.AddChild(contactControl);
rowIndex++;
}
}
// Add contacts without departments at the end
if (noDepartmentContacts.Count > 0)
{
noDepartmentContacts.Sort();
if (departmentGroups.Count > 0)
{
ContactsList.AddChild(new Label
{
Text = Loc.GetString("nano-chat-other-department"),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, 8, 0, 2)
});
}
foreach (var contact in noDepartmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
ContactsList.AddChild(contactControl);
rowIndex++;
}
var contact = contacts[idx];
var isEvenRow = idx % 2 == 0;
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
ContactsList.AddChild(contactControl);
}
}
@@ -100,13 +42,9 @@ public sealed partial class NanoChatLookupView : PanelContainer
HorizontalExpand = true;
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
var displayName = contact.Name;
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
displayName = $"{contact.Name} ({contact.JobTitle})";
var nameLabel = new Label()
{
Text = displayName,
Text = contact.Name,
HorizontalAlignment = HAlignment.Left,
HorizontalExpand = true
};
@@ -1,6 +1,5 @@
<cartridges:NanoChatMessageBubble
xmlns="https://spacestation14.io"
xmlns:nanochatui="clr-namespace:Content.Client._DV.NanoChat.UI"
xmlns:cartridges="clr-namespace:Content.Client._DV.CartridgeLoader.Cartridges"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
HorizontalExpand="True">
@@ -8,34 +7,31 @@
<BoxContainer Name="MessageContainer"
Orientation="Horizontal"
HorizontalExpand="True">
<!-- Left spacer for other's messages -->
<Control MinSize="12 0" Name="LeftSpacer" />
<!-- Left spacer for other's messages -->
<Control Name="LeftSpacer"
MinSize="12 0" />
<!-- Message panel -->
<BoxContainer Name="MessageBox"
Orientation="Vertical"
MaxWidth="320"
HorizontalExpand="True">
<!-- Sender name -->
<Label HorizontalAlignment="Left"
Margin="10,0,10,2"
Name="SenderNameLabel"
StyleClasses="LabelSmall"
Visible="False" />
<PanelContainer Name="MessagePanel"
MaxWidth="320"
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BorderThickness="1">
<graphics:StyleBoxFlat
ContentMarginLeftOverride="10"
ContentMarginRightOverride="10"
ContentMarginTopOverride="6"
ContentMarginBottomOverride="6"
BorderThickness="1">
<!-- Colors set in code based on message sender -->
</graphics:StyleBoxFlat>
</PanelContainer.PanelOverride>
<RichTextLabel Name="MessageText"
HorizontalExpand="True"
Margin="10,6,10,6" />
HorizontalExpand="True" />
</PanelContainer>
<!-- Delivery failed text -->
@@ -1,19 +1,14 @@
using System.Text.RegularExpressions;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
using Content.Client._Funkystation.NanoChat;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatMessageBubble : BoxContainer
{
private static readonly Regex EmojiRegex = new(@":(\w+):", RegexOptions.Compiled);
public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green
public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray
public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border
@@ -25,7 +20,7 @@ public sealed partial class NanoChatMessageBubble : BoxContainer
RobustXamlLoader.Load(this);
}
public void SetMessage(NanoChatMessage message, bool isOwnMessage, string? senderName = null, bool showSenderName = false)
public void SetMessage(NanoChatMessage message, bool isOwnMessage)
{
if (MessagePanel.PanelOverride is not StyleBoxFlat)
return;
@@ -35,20 +30,8 @@ public sealed partial class NanoChatMessageBubble : BoxContainer
style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor;
style.BorderColor = BorderColor;
var contentWithEmoji = EmojiRegex.Replace(FormattedMessage.EscapeText(message.Content), "[emoji=\"$1\"]");
MessageText.SetMessage(
FormattedMessage.FromMarkupPermissive(contentWithEmoji),
new[] { typeof(EmojiTag) }
);
SenderNameLabel.Visible = showSenderName && !string.IsNullOrEmpty(senderName);
if (SenderNameLabel.Visible)
{
SenderNameLabel.Text = senderName;
SenderNameLabel.Modulate = TextColor;
}
// Set message content
MessageText.Text = message.Content;
MessageText.Modulate = TextColor;
// Show delivery failed text if needed (only for own messages)
@@ -5,7 +5,7 @@ using Robust.Client.UserInterface;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
public sealed partial class NanoChatUi : UIFragment, IDisposable
public sealed partial class NanoChatUi : UIFragment
{
private NanoChatUiFragment? _fragment;
@@ -30,12 +30,6 @@ public sealed partial class NanoChatUi : UIFragment, IDisposable
_fragment?.UpdateState(cast);
}
public void Dispose()
{
// Close all popups when switching apps or closing the PDA
_fragment?.CloseAllPopups();
}
private static void SendNanoChatUiMessage(NanoChatUiMessageType type,
uint? number,
string? content,
@@ -60,17 +60,6 @@
Margin="0 0 4 0"
StyleClasses="OpenBoth"
ToolTip="{Loc nano-chat-new-chat}" />
<Button Name="CreateGroupChatButton"
MinSize="32 32"
MaxSize="32 32"
Margin="0 0 4 0"
StyleClasses="OpenBoth"
ToolTip="{Loc nano-chat-create-group}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<Button Name="LookupButton"
MaxSize="32 32"
StyleClasses="OpenBoth"
@@ -95,20 +84,12 @@
</BoxContainer>
</controls:StripeBack>
<Label Name="NoIdLabel"
Text="{Loc 'nano-chat-no-id-card'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalExpand="True"
HorizontalExpand="True"
Visible="True" />
<!-- Main content split -->
<BoxContainer Name="ChatView"
Orientation="Horizontal"
VerticalExpand="True"
HorizontalExpand="True"
Margin="0 5 0 0"
Visible="False">
Margin="0 5 0 0">
<!-- Left panel: Chat list -->
<PanelContainer StyleClasses="AngleRect"
VerticalExpand="True"
@@ -186,18 +167,6 @@
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<!-- View Members should take place of edit button in group chats, to avoid layout shift when moving between normal and group -->
<Button Name="ViewMembersButton"
MaxSize="32 32"
Visible="False"
StyleClasses="OpenBoth"
Margin="0 0 4 0"
ToolTip="{Loc nano-chat-view-members}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<Button Name="DeleteChatButton"
MaxSize="32 32"
Visible="False"
@@ -228,34 +197,11 @@
MinSize="18 18" />
</Control>
</Button>
<Button Name="InviteToGroupButton"
MaxSize="32 32"
Visible="False"
StyleClasses="OpenBoth"
Margin="0 0 4 0"
ToolTip="{Loc nano-chat-invite-to-group}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/invite.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<!-- Input row -->
<LineEdit Name="MessageInput"
PlaceHolder="{Loc nano-chat-message-placeholder}"
HorizontalExpand="True"
StyleClasses="OpenRight" />
<!-- Emoji Picker -->
<Button Name="EmojiButton"
MinSize="32 32"
MaxSize="32 32"
StyleClasses="OpenBoth"
Margin="4 0 0 0"
ToolTip="{Loc nano-chat-emoji-picker}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/Interface/Emotes/laugh.png"
Stretch="KeepAspectCentered"
MinSize="24 24" />
</Button>
<Button Name="SendButton"
MinSize="32 32"
Disabled="True"
@@ -1,18 +1,7 @@
// SPDX-FileCopyrightText: 2024 Skubman <ba.fallaria@gmail.com>
// SPDX-FileCopyrightText: 2024 Tadeo <td12233a@gmail.com>
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
// SPDX-FileCopyrightText: 2025 Icepick <122653407+Icepicked@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 corresp0nd <46357632+corresp0nd@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 taydeo <td12233a@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using System.Numerics;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared.CCVar;
using Content.Client._Funkystation.NanoChat;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -29,14 +18,10 @@ public sealed partial class NanoChatUiFragment : BoxContainer
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly int _maxNameLength;
private readonly int _maxIdJobLength;
private int _maxNameLength;
private int _maxIdJobLength;
private readonly NewChatPopup _newChatPopup;
private readonly EditChatPopup _editChatPopup;
private readonly CreateGroupChatPopup _createGroupChatPopup;
private readonly InviteToGroupPopup _inviteToGroupPopup;
private readonly GroupMembersPopup _groupMembersPopup;
private readonly EmojiPickerPopup _emojiPickerPopup;
private uint? _currentChat;
private uint? _pendingChat;
private uint _ownNumber;
@@ -45,7 +30,6 @@ public sealed partial class NanoChatUiFragment : BoxContainer
private Dictionary<uint, NanoChatRecipient> _recipients = [];
private Dictionary<uint, List<NanoChatMessage>> _messages = [];
private HashSet<uint> _mutedChats = [];
private List<NanoChatRecipient>? _contacts; // Used in order to show names for people not in recipients
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
@@ -59,10 +43,6 @@ public sealed partial class NanoChatUiFragment : BoxContainer
_newChatPopup = new(_maxNameLength, _maxIdJobLength);
_editChatPopup = new(_maxNameLength, _maxIdJobLength);
_createGroupChatPopup = new(_maxNameLength);
_inviteToGroupPopup = new();
_groupMembersPopup = new();
_emojiPickerPopup = new();
SetupEventHandlers();
}
@@ -78,57 +58,12 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.EditChat, number, name, job);
};
_createGroupChatPopup.OnGroupCreated += (name) =>
{
OnMessageSent?.Invoke(NanoChatUiMessageType.CreateGroupChat, null, name, null);
};
_inviteToGroupPopup.OnInvite += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.InviteToGroup, currentChat, number.ToString(), null);
};
_groupMembersPopup.OnKick += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.KickFromGroup, currentChat, number.ToString(), null);
};
_groupMembersPopup.OnAdmin += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.AdminUser, currentChat, number.ToString(), null);
};
_groupMembersPopup.OnDeadmin += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.DeadminUser, currentChat, number.ToString(), null);
};
_emojiPickerPopup.OnEmojiSelected += InsertEmoji;
NewChatButton.OnPressed += _ =>
{
_newChatPopup.ClearInputs();
_newChatPopup.OpenCentered();
};
CreateGroupChatButton.OnPressed += _ =>
{
_createGroupChatPopup.ClearInputs();
_createGroupChatPopup.OpenCentered();
};
MuteChatButton.OnPressed += _ =>
{
if (_currentChat is not uint currentChat)
@@ -199,39 +134,8 @@ public sealed partial class NanoChatUiFragment : BoxContainer
MessageInput.OnTextEntered += _ => SendMessage();
SendButton.OnPressed += _ => SendMessage();
EmojiButton.OnPressed += _ => OpenEmojiPicker();
EditChatButton.OnPressed += _ => BeginEditChat();
DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
InviteToGroupButton.OnPressed += _ => OpenInvitePopup();
ViewMembersButton.OnPressed += _ => OpenMembersPopup();
}
private void OpenEmojiPicker()
{
// Close any existing emoji picker to prevent duplicates (shouldn't be happening anyway but it sure did)
if (_emojiPickerPopup.IsOpen)
_emojiPickerPopup.Close();
_emojiPickerPopup.OpenCentered();
}
private void InsertEmoji(string emoji)
{
MessageInput.InsertAtCursor(emoji);
MessageInput.GrabKeyboardFocus();
}
/// <summary>
/// Closes all open popups. Called when switching apps or closing the PDA.
/// </summary>
public void CloseAllPopups()
{
_emojiPickerPopup.Close();
_inviteToGroupPopup.Close();
_groupMembersPopup.Close();
_newChatPopup.Close();
_editChatPopup.Close();
_createGroupChatPopup.Close();
}
private void ToggleView()
@@ -252,7 +156,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
if (_recipients.Count == 0)
return;
var orderedRecipients = _recipients.OrderBy(r => r.Value).Select(r => r.Key).ToArray();
var orderedRecipients = _recipients.OrderBy(r => r.Value.Name).Select(r => r.Key).ToArray();
var currentChatIndex = (direction, _currentChat) switch
{
(CycleDirection.Up, null) => _recipients.Count,
@@ -348,89 +252,6 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
}
private void OpenInvitePopup()
{
if (_currentChat is not uint currentChat ||
!_recipients.TryGetValue(currentChat, out var recipient) ||
!recipient.IsGroup)
return;
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
var members = recipient.Members ?? new HashSet<uint>();
_inviteToGroupPopup.SetContacts(availableContacts, members, _ownNumber);
_inviteToGroupPopup.ClearSearch();
_inviteToGroupPopup.OpenCentered();
}
private void OpenMembersPopup()
{
if (_currentChat is not uint currentChat ||
!_recipients.TryGetValue(currentChat, out var recipient) ||
!recipient.IsGroup)
return;
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
var members = recipient.Members ?? new HashSet<uint>();
var admins = recipient.Admins ?? new HashSet<uint>();
_groupMembersPopup.SetMembers(availableContacts, members, _ownNumber, recipient.CreatorId, admins);
_groupMembersPopup.OpenCentered();
}
private void RefreshOpenPopups()
{
// If popup is open, refresh it or close if no longer a member
if (_inviteToGroupPopup.IsOpen)
{
if (_currentChat is uint currentChat &&
_recipients.TryGetValue(currentChat, out var recipient) &&
recipient.IsGroup)
{
var members = recipient.Members ?? new HashSet<uint>();
if (!members.Contains(_ownNumber))
{
_inviteToGroupPopup.Close();
}
else
{
// Use the contacts list from the server (same as Look Up Numbers)
// Don't fall back to recipients as they contain outdated cached information
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
_inviteToGroupPopup.SetContacts(availableContacts, members, _ownNumber);
}
}
else
{
_inviteToGroupPopup.Close();
}
}
if (_groupMembersPopup.IsOpen)
{
if (_currentChat is uint currentChat &&
_recipients.TryGetValue(currentChat, out var recipient) &&
recipient.IsGroup)
{
var members = recipient.Members ?? new HashSet<uint>();
if (!members.Contains(_ownNumber))
{
_groupMembersPopup.Close();
}
else
{
// Use the contacts list from the server (same as Look Up Numbers)
// Don't fall back to recipients as they contain outdated cached information
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
var admins = recipient.Admins ?? new HashSet<uint>();
_groupMembersPopup.SetMembers(availableContacts, members, _ownNumber, recipient.CreatorId, admins);
}
}
else
{
_groupMembersPopup.Close();
}
}
}
private void BeginEditChat()
{
if (_currentChat is not uint currentChat)
@@ -473,40 +294,17 @@ public sealed partial class NanoChatUiFragment : BoxContainer
var activeChat = _pendingChat ?? _currentChat;
var hasActiveChat = activeChat != null;
var isGroupChat = false;
var canManageGroup = false;
NanoChatRecipient recipient = default;
var hasRecipient = false;
if (hasActiveChat && activeChat != null && _recipients.TryGetValue(activeChat.Value, out recipient))
{
hasRecipient = true;
isGroupChat = recipient.IsGroup;
canManageGroup = isGroupChat && (recipient.CreatorId == _ownNumber || recipient.Admins?.Contains(_ownNumber) == true);
}
// Update UI state
MessagesScroll.Visible = hasActiveChat;
CurrentChatName.Visible = !hasActiveChat;
MessageInputContainer.Visible = hasActiveChat;
DeleteChatButton.Visible = hasActiveChat;
EditChatButton.Visible = hasActiveChat && !isGroupChat;
EditChatButton.Visible = hasActiveChat;
DeleteChatButton.Disabled = !hasActiveChat;
InviteToGroupButton.Visible = canManageGroup;
ViewMembersButton.Visible = isGroupChat;
if (hasRecipient)
if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
{
if (isGroupChat)
{
CurrentChatName.Text = recipient.Name;
}
else
{
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
}
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
}
else
{
@@ -523,44 +321,14 @@ public sealed partial class NanoChatUiFragment : BoxContainer
if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages))
return;
var isGroupChat = _recipients.TryGetValue(activeChat.Value, out var recipient) && recipient.IsGroup;
foreach (var message in chatMessages)
{
var messageBubble = new NanoChatMessageBubble();
var isOwnMessage = message.SenderId == _ownNumber;
// For group chats, show sender name for other people's messages
string? senderName = null;
if (isGroupChat && !isOwnMessage)
{
// Try to get sender name from contacts first, then recipients
if (_recipients.TryGetValue(message.SenderId, out var senderRecipient))
{
senderName = senderRecipient.Name;
}
// If not in recipients, try contacts list (station-wide directory)
if (senderName == null && _contacts != null)
{
var contact = _contacts.FirstOrDefault(c => c.Number == message.SenderId);
if (contact.Number != 0)
senderName = contact.Name;
}
// Fallback to "Unknown" with number if name not found
if (senderName == null)
{
senderName = $"{Loc.GetString("nano-chat-unknown-sender")}#{message.SenderId:D4}";
}
}
messageBubble.SetMessage(message, isOwnMessage, senderName, isGroupChat && !isOwnMessage);
messageBubble.SetMessage(message, message.SenderId == _ownNumber);
MessageList.AddChild(messageBubble);
// Add spacing between messages
if (MessageList.ChildCount > 0)
MessageList.AddChild(new Control { Name = "MessagePadding", MinSize = new Vector2(0, 4) });
MessageList.AddChild(messageBubble);
MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) });
}
MessageList.InvalidateMeasure();
@@ -589,41 +357,13 @@ public sealed partial class NanoChatUiFragment : BoxContainer
ListNumberButton.Pressed = _listNumber;
}
private void CheckValidNumber()
{
if (_ownNumber == 0)
{
NoIdLabel.Visible = true;
ChatView.Visible = false;
LookupView.Visible = false;
MuteButton.Disabled = true;
NewChatButton.Disabled = true;
CreateGroupChatButton.Disabled = true;
LookupButton.Disabled = true;
ListNumberButton.Disabled = true;
}
else if (NoIdLabel.Visible)
{
NoIdLabel.Visible = false;
ToggleView();
MuteButton.Disabled = false;
NewChatButton.Disabled = false;
CreateGroupChatButton.Disabled = false;
LookupButton.Disabled = false;
ListNumberButton.Disabled = false;
}
}
public void UpdateState(NanoChatUiState state)
{
_ownNumber = state.OwnNumber;
_notificationsMuted = state.NotificationsMuted;
_listNumber = state.ListNumber;
_mutedChats = state.MutedChats;
_contacts = state.Contacts;
OwnNumberLabel.Text = $"#{_ownNumber:D4}";
OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
UpdateMuteButton();
UpdateListNumber();
@@ -647,15 +387,10 @@ public sealed partial class NanoChatUiFragment : BoxContainer
if (_pendingChat == null)
_currentChat = state.CurrentChat;
// Update recipients first, then update UI that depends on it
UpdateChatList(state.Recipients);
UpdateMessages(state.Messages);
UpdateCurrentChat();
UpdateMuteChatButton();
UpdateChatList(state.Recipients);
UpdateMessages(state.Messages);
LookupView.UpdateContactList(state);
CheckValidNumber();
RefreshOpenPopups();
}
}
-130
View File
@@ -1,130 +0,0 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
namespace Content.Client._DV.NanoChat.UI;
/// <summary>
/// Extremely specialized version of <see cref="Robust.Client.UserInterface.Controls.TextureRect" /> designed for drawing Textures at fixed size with an additional offset
/// Circumvents the problem of not having "negative mergins" on TextureRect for manual alignment
/// This is fucking evil, please do not use this :3
/// </summary>
public sealed class EmojiControl : Control
{
public const string StylePropertyTexture = "texture";
public const string StylePropertyOffset = "offset";
private Texture? _texture;
private Vector2? _offset;
private string? _texturePath;
public string TexturePath
{
set
{
Texture = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(value);
_texturePath = value;
}
}
protected override void OnThemeUpdated()
{
if (_texturePath != null) Texture = Theme.ResolveTexture(_texturePath);
base.OnThemeUpdated();
}
/// <summary>
/// The texture to draw.
/// </summary>
public Texture? Texture
{
get
{
if (_texture is null)
{
if (TryGetStyleProperty(StylePropertyTexture, out Texture? texture))
{
return texture;
}
}
return _texture;
}
set
{
var oldSize = _texture?.Size;
_texture = value;
if (value?.Size != oldSize)
{
InvalidateMeasure();
}
}
}
/// <summary>
/// Vertical offset of the texture
/// </summary>
public Vector2 Offset
{
get
{
if (_offset is null)
{
if (TryGetStyleProperty(StylePropertyOffset, out Vector2 offset))
return offset;
}
return _offset ?? Vector2.Zero;
}
set
{
var oldOffset = _offset;
_offset = value;
if (oldOffset != value)
{
InvalidateArrange();
}
}
}
protected override void ArrangeCore(UIBox2 finalRect)
{
finalRect.Left += Offset.X;
finalRect.Right += Offset.X;
finalRect.Top += Offset.Y;
finalRect.Bottom += Offset.Y;
base.ArrangeCore(finalRect);
}
protected override void Draw(DrawingHandleScreen handle)
{
var texture = Texture;
if (texture is null)
return;
handle.DrawTextureRect(texture, GetDrawDimensions());
}
private UIBox2 GetDrawDimensions()
{
var (texWidth, texHeight) = Texture?.Size ?? Vector2i.Zero;
var width = texWidth * (PixelSize.Y / texHeight);
var height = (float)PixelSize.Y;
if (width > PixelSize.X)
{
width = PixelSize.X;
height = texHeight * (PixelSize.X / texWidth);
}
var size = new Vector2(width, height);
var position = (PixelSize - size) / 2 + Offset;
return UIBox2.FromDimensions(position, size);
}
}
@@ -1,187 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Client.Stealth;
using Content.Shared._DV.Overlays.Components;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Stealth.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client._Goobstation.Overlays;
public sealed class SharkVisionOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private readonly TransformSystem _transform;
private readonly StealthSystem _stealth;
private readonly ContainerSystem _container;
private readonly SharedSolutionContainerSystem _solution;
private readonly SpriteSystem _sprite;
public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly List<SharkVisionRenderEntry> _entries = [];
private EntityUid? _lightEntity;
public float LightRadius;
public SharkVisionComponent? Comp;
public SharkVisionOverlay()
{
IoCManager.InjectDependencies(this);
_container = _entity.System<ContainerSystem>();
_transform = _entity.System<TransformSystem>();
_stealth = _entity.System<StealthSystem>();
_solution = _entity.System<SharedSolutionContainerSystem>();
_sprite = _entity.System<SpriteSystem>();
ZIndex = -1;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture is null || Comp is null)
return;
var worldHandle = args.WorldHandle;
var eye = args.Viewport.Eye;
if (eye == null)
return;
var player = _player.LocalEntity;
if (!_entity.TryGetComponent(player, out TransformComponent? playerXform))
return;
var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime);
var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime);
var mapId = eye.Position.MapId;
var eyeRot = eye.Rotation;
GetVisionEntities(Comp.BloodPrototypes, mapId, eyeRot);
foreach (var entry in _entries)
{
Render(entry.Ent, entry.Map, worldHandle, entry.EyeRot, Comp.Color, alpha);
}
worldHandle.SetTransform(Matrix3x2.Identity);
}
private void GetVisionEntities(ProtoId<ReagentPrototype>[] bloodPrototypes, MapId mapId, Angle eyeRot)
{
_entries.Clear();
var entities = _entity.EntityQueryEnumerator<SolutionContainerManagerComponent, SpriteComponent, TransformComponent>();
while (entities.MoveNext(out var uid, out var body, out var sprite, out var xform))
{
if (!CanSee(uid, sprite))
continue;
// Luckily, players are also SolutionContainerManagerComponent, so let's check for a bloodstream and check for bleeds
if (_entity.TryGetComponent<BloodstreamComponent>(uid, out var bloodstream) && bloodstream.BleedAmount <= 0)
continue;
// Should always be true but I'm just simplifying a future if statement
if (!_entity.TryGetComponent<SolutionContainerManagerComponent>(uid, out var solutionContainer))
continue;
if (solutionContainer.Containers == null)
continue;
var bloodFound = false;
// Someone calculate the Big O notation of this lmao
foreach (var individualContainer in solutionContainer.Containers)
{
if (_solution.TryGetSolution((uid, solutionContainer), individualContainer, out var _, out var solution))
{
var reagentsInContainer = solution.GetReagentPrototypes(_proto).ToDictionary();
bloodFound = reagentsInContainer.Keys.Any(reagentKey => bloodPrototypes.Any(x => reagentKey == x));
}
if (bloodFound)
break; // We don't need to keep searching if the entity has blood in at least one container
}
if (!bloodFound)
continue;
var entity = uid;
// Parent container check
if (_container.TryGetOuterContainer(uid, xform, out var container))
{
var owner = container.Owner;
if (_entity.TryGetComponent<SpriteComponent>(owner, out var ownerSprite)
&& _entity.TryGetComponent<TransformComponent>(owner, out var ownerXform))
{
entity = owner;
sprite = ownerSprite;
xform = ownerXform;
}
}
if (_entries.Any(e => e.Ent.Owner == entity))
continue;
_entries.Add(new SharkVisionRenderEntry((entity, sprite, xform), mapId, eyeRot));
}
}
private void Render(Entity<SpriteComponent, TransformComponent> ent,
MapId? map,
DrawingHandleWorld handle,
Angle eyeRot,
Color color,
float alpha)
{
var (uid, sprite, xform) = ent;
if (xform.MapID != map || !CanSee(uid, sprite))
return;
var position = _transform.GetWorldPosition(xform);
var rotation = _transform.GetWorldRotation(xform);
var originalColor = sprite.Color;
_sprite.SetColor((ent, ent.Comp1), color.WithAlpha(alpha));
_sprite.RenderSprite((ent, ent.Comp1), handle, eyeRot, rotation, position);
_sprite.SetColor((ent, ent.Comp1), originalColor);
}
private bool CanSee(EntityUid uid, SpriteComponent sprite)
{
return sprite.Visible && (!_entity.TryGetComponent(uid, out StealthComponent? stealth) ||
_stealth.GetVisibility(uid, stealth) > 0.5f);
}
public void ResetLight(bool checkFirstTimePredicted = true)
{
if (_lightEntity == null || checkFirstTimePredicted && !_timing.IsFirstTimePredicted)
return;
_entity.DeleteEntity(_lightEntity);
_lightEntity = null;
}
}
public record struct SharkVisionRenderEntry(
Entity<SpriteComponent, TransformComponent> Ent,
MapId? Map,
Angle EyeRot);
@@ -1,113 +0,0 @@
using Content.Client.Overlays;
using Content.Shared._DV.Overlays.Components;
using Content.Shared._Goobstation.Overlays;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Robust.Client.Graphics;
namespace Content.Client._Goobstation.Overlays;
public sealed class SharkVisionSystem : EquipmentHudSystem<SharkVisionComponent>
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private SharkVisionOverlay _sharkOverlay = default!;
private BaseSwitchableOverlay<SharkVisionComponent> _overlay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharkVisionComponent, SwitchableOverlayToggledEvent>(OnToggle);
_sharkOverlay = new SharkVisionOverlay();
_overlay = new BaseSwitchableOverlay<SharkVisionComponent>();
}
protected override void OnRefreshComponentHud(Entity<SharkVisionComponent> ent,
ref RefreshEquipmentHudEvent<SharkVisionComponent> args)
{
if (!ent.Comp.IsEquipment)
base.OnRefreshComponentHud(ent, ref args);
}
protected override void OnRefreshEquipmentHud(Entity<SharkVisionComponent> ent,
ref InventoryRelayedEvent<RefreshEquipmentHudEvent<SharkVisionComponent>> args)
{
if (ent.Comp.IsEquipment)
base.OnRefreshEquipmentHud(ent, ref args);
}
private void OnToggle(Entity<SharkVisionComponent> ent, ref SwitchableOverlayToggledEvent args)
{
RefreshOverlay();
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<SharkVisionComponent> args)
{
base.UpdateInternal(args);
SharkVisionComponent? tvComp = null;
var lightRadius = 0f;
foreach (var comp in args.Components)
{
if (!comp.IsActive && (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime))
continue;
if (tvComp == null)
tvComp = comp;
else if (!tvComp.DrawOverlay && comp.DrawOverlay)
tvComp = comp;
else if (tvComp.DrawOverlay == comp.DrawOverlay && tvComp.PulseTime > 0f && comp.PulseTime <= 0f)
tvComp = comp;
lightRadius = MathF.Max(lightRadius, 1);
}
UpdateSharkOverlay(tvComp, lightRadius);
UpdateOverlay(tvComp);
}
protected override void DeactivateInternal()
{
base.DeactivateInternal();
_sharkOverlay.ResetLight(false);
UpdateOverlay(null);
UpdateSharkOverlay(null, 0f);
}
private void UpdateSharkOverlay(SharkVisionComponent? comp, float lightRadius)
{
_sharkOverlay.LightRadius = lightRadius;
_sharkOverlay.Comp = comp;
switch (comp)
{
case not null when !_overlayMan.HasOverlay<SharkVisionOverlay>():
_overlayMan.AddOverlay(_sharkOverlay);
break;
case null:
_overlayMan.RemoveOverlay(_sharkOverlay);
_sharkOverlay.ResetLight();
break;
}
}
private void UpdateOverlay(SharkVisionComponent? tvComp)
{
_overlay.Comp = tvComp;
switch (tvComp)
{
case { DrawOverlay: true } when !_overlayMan.HasOverlay<BaseSwitchableOverlay<SharkVisionComponent>>():
_overlayMan.AddOverlay(_overlay);
break;
case null or { DrawOverlay: false }:
_overlayMan.RemoveOverlay(_overlay);
break;
}
// Night vision overlay is prioritized
_overlay.IsActive = !_overlayMan.HasOverlay<BaseSwitchableOverlay<NightVisionComponent>>();
}
}
@@ -4,9 +4,6 @@ using Content.Client.CharacterInfo;
using Content.Shared.CCVar;
using Content.Shared._DV.CCVars;
using Content.Shared.Dataset;
using Content.Shared.Chat;
using Content.Shared.Chat.TypingIndicator;
using Robust.Shared.Prototypes;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using static Content.Client.CharacterInfo.CharacterInfoSystem;
@@ -15,12 +12,6 @@ namespace Content.Client.UserInterface.Systems.Chat;
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
{
public ChatSelectChannel CurrentChannel = ChatSelectChannel.None;
private static readonly ProtoId<TypingIndicatorPrototype> WhisperID = "whisper";
private static readonly ProtoId<TypingIndicatorPrototype> EmoteID = "emote";
private static readonly ProtoId<TypingIndicatorPrototype> OocID = "ooc";
private static readonly ProtoId<TypingIndicatorPrototype> RadioID = "radio";
/// <summary>
/// Gets Invoked whenever the autofilled highlights have changed.
/// Used to populate the preview in the channel selector window.
@@ -107,38 +98,4 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
if (shouldSave)
_config.SaveToFile();
}
/// <summary>
/// Notifies and sets what type of typing indicator should be put.
/// </summary>
public void NotifySpecificChatTextChange(ChatSelectChannel selectedChannel)
{
var channel = CurrentChannel;
if (CurrentChannel == ChatSelectChannel.None)
channel = selectedChannel;
switch (channel)
{
case ChatSelectChannel.Whisper:
_typingIndicator?.ClientAlternateTyping(WhisperID);
break;
case ChatSelectChannel.Radio:
_typingIndicator?.ClientAlternateTyping(RadioID);
break;
case ChatSelectChannel.Emotes:
_typingIndicator?.ClientAlternateTyping(EmoteID);
break;
case ChatSelectChannel.LOOC:
case ChatSelectChannel.OOC:
_typingIndicator?.ClientAlternateTyping(OocID);
break;
default:
_typingIndicator?.ClientChangedChatText();
break;
}
}
}
@@ -1,39 +0,0 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'node-scan-display-title'}"
MinSize="350 400"
SetSize="350 400"
Resizable="False"
>
<BoxContainer Orientation="Vertical" >
<controls:StripeBack>
<Label Name="NodeScannerState" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
<BoxContainer Name="ProgressBarContainer" Orientation="Horizontal" Margin="0 5 0 0" HorizontalExpand="True" Visible="False" >
<Label Name="ProgressBarLabel" Text="{Loc 'node-scan-timer'}" Margin="20 0 0 0" />
<ProgressBar
Name="ProgressBar"
MinValue="0"
MaxValue="1"
SetHeight="20"
Margin="10 0 20 0"
HorizontalExpand="True" >
</ProgressBar>
</BoxContainer>
<BoxContainer Orientation="Horizontal" VerticalExpand="True" >
<Label Name="NoActiveNodeDataLabel" Text="{Loc 'node-scan-no-data'}" Margin="15 15 0 0" MinHeight="47" HorizontalExpand="True" HorizontalAlignment="Center" />
<BoxContainer Name="ActiveNodesList" Orientation="Vertical" HorizontalExpand="True" Visible="False" MinHeight="72" />
</BoxContainer>
<PanelContainer Name="SystemWarningPanel"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
HorizontalExpand="True"
Margin="10 0 10 5"
Visible="False" >
<BoxContainer Orientation="Vertical" HorizontalExpand="True" >
<RichTextLabel Name="SystemWarningLabel" Margin="5 8 5 0" HorizontalAlignment="Center" />
<RichTextLabel Name="SystemWarningSublabel" Margin="5 3 5 8" HorizontalAlignment="Center" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -1,250 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.NameIdentifier;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client._DV.Xenoarchaeology.Ui;
/// <summary>
/// DeltaV replacement for NodeScannerDisplay, the UI of the artifact node scanner.
/// Displays extra information about the artifact unlocking phase.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DVNodeScannerDisplay : FancyWindow
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IGameTiming _timing= default!;
private readonly SharedXenoArtifactSystem _artifact;
private readonly SpriteSystem _spriteSystem;
private Texture? _blipTexture;
private EntityUid _owner;
private float animatedTimeRemaining = 0;
private float animatedTimeRemainingVelocity = 0;
public DVNodeScannerDisplay()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_artifact = _ent.System<SharedXenoArtifactSystem>();
_spriteSystem = _ent.System<SpriteSystem>();
}
/// <summary>
/// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
/// Closes window if <see cref="NodeScannerComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid scannerEntityUid)
{
if (!_ent.TryGetComponent<NodeScannerComponent>(scannerEntityUid, out var scannerComponent))
{
Close();
return;
}
_owner = scannerEntityUid;
_blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
}
/// <inheritdoc />
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// reset
NoActiveNodeDataLabel.Visible = true;
NodeScannerState.Text = Loc.GetString("node-scanner-artifact-non-connected");
ProgressBarContainer.Visible = false;
ActiveNodesList.Visible = false;
SystemWarningPanel.Visible = false;
if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
return;
var attachedArtifactEnt = connectedScanner.AttachedTo;
if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
return;
_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
// header banner
NodeScannerState.Text =
unlockingComponent != null
? Loc.GetString("node-scanner-artifact-state-unlocking")
: _timing.CurTime < artifactComponent.NextUnlockTime
? Loc.GetString("node-scanner-artifact-state-cooldown")
: Loc.GetString("node-scanner-artifact-state-ready");
// time remaining progress bar
var unlockTimeRemaining =
unlockingComponent != null
? MathF.Max(0f, (float)(unlockingComponent.EndTime - _timing.CurTime).TotalSeconds)
: 0f;
// damped-oscillator-style system for animating sudden jumps in time remaining
animatedTimeRemainingVelocity += 0.001f * (unlockTimeRemaining - animatedTimeRemaining) - 0.1f * animatedTimeRemainingVelocity;
animatedTimeRemaining = MathF.Max(0f, animatedTimeRemaining + animatedTimeRemainingVelocity);
// sigmoid-style function to make sure progress bar never goes over 100%
ProgressBar.Value = 2f / (1f + MathF.Exp(-0.08f * animatedTimeRemaining)) - 1f;
// node data and warning panel
(List<int> triggeredIndexesOrdered, HashSet<int> triggeredIndexesRelated)? unlockingDataToShow = null;
if (unlockingComponent != null)
{
unlockingDataToShow = (
unlockingComponent.TriggeredNodeIndexesOrdered,
unlockingComponent.TriggeredNodeIndexesRelated
);
}
else if (
artifactComponent.LastUnlockingEndTime != null
&& _timing.CurTime - artifactComponent.LastUnlockingEndTime < TimeSpan.FromSeconds(60)
)
{
unlockingDataToShow = (
artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered,
artifactComponent.LastUnlockingTriggeredNodeIndexesRelated
);
}
if (
unlockingDataToShow is (List<int> triggeredIndexesOrdered, HashSet<int> triggeredIndexesRelated)
&& triggeredIndexesOrdered.Count > 0
)
{
// show triggered node data instead of 'no data' placeholder
NoActiveNodeDataLabel.Visible = false;
ProgressBarContainer.Visible = true;
ActiveNodesList.Visible = true;
// node list
ActiveNodesList.Children.Clear();
foreach (var triggeredIndex in triggeredIndexesOrdered)
{
var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
var nodeControl = new Button
{
Margin = new Thickness(15, 5, 15, 0),
MaxHeight = 40,
Disabled = true,
HorizontalExpand = true
};
ActiveNodesList.AddChild(nodeControl);
var mainContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
nodeControl.AddChild(mainContainer);
// mimics suitCoordsIndicator in CrewMonitoringWindow
var relatednessIndicator = new TextureRect()
{
Texture = _blipTexture,
TextureScale = new Vector2(0.25f, 0.25f),
Modulate = triggeredIndexesRelated.Contains(triggeredIndex) ? Color.LimeGreen : Color.DarkRed,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
};
mainContainer.AddChild(relatednessIndicator);
var triggerTip = Loc.GetString(node.Comp.TriggerTip!);
var triggeredNodeName = (_ent.GetComponentOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
var descriptionLabel = new Label()
{
Text = $"{triggerTip} ({triggeredNodeName})",
HorizontalExpand = true,
ClipText = true,
Margin = new Thickness(8, 0, 0, 0),
};
mainContainer.AddChild(descriptionLabel);
}
// warning panel (mimics that of PowerMonitoringWindow)
(Color background, Color border, string message, string subtext)? warningDataToShow = null;
if (unlockingComponent == null)
{
if (artifactComponent.LastUnlockingSuccessful)
{
warningDataToShow = (
background: Color.Green,
border: Color.DarkGreen,
message: Loc.GetString("node-scan-warning-unlock-complete"),
subtext: Loc.GetString("node-scan-warning-unlock-complete-subtext")
);
}
else if (triggeredIndexesOrdered.Count == triggeredIndexesRelated.Count)
{
warningDataToShow = (
background: Color.Orange,
border: Color.DarkOrange,
message: Loc.GetString("node-scan-warning-partial-complete"),
subtext: Loc.GetString("node-scan-warning-partial-complete-subtext")
);
}
else
{
warningDataToShow = (
background: Color.Red,
border: Color.DarkRed,
message: Loc.GetString("node-scan-warning-failure-complete"),
subtext: Loc.GetString("node-scan-warning-failure-complete-subtext")
);
}
}
else if (triggeredIndexesOrdered.Count != triggeredIndexesRelated.Count)
{
warningDataToShow = (
background: Color.Red,
border: Color.DarkRed,
message: Loc.GetString("node-scan-warning-failure-imminent"),
subtext: Loc.GetString("node-scan-warning-failure-imminent-subtext")
);
}
if (warningDataToShow is (Color background, Color border, string message, string subtext))
{
SystemWarningPanel.Visible = true;
SystemWarningPanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = background,
BorderColor = border,
BorderThickness = new Thickness(2),
};
// warning panel pulses during unlocking phase
var lit = unlockingComponent != null && _timing.RealTime.TotalSeconds % 1f > 0.5f;
SystemWarningPanel.Modulate = lit ? Color.White : new Color(178, 178, 178);
SystemWarningLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(message));
SystemWarningSublabel.SetMessage(FormattedMessage.FromMarkupOrThrow(subtext));
}
}
}
}
@@ -1,36 +0,0 @@
using Robust.Client.Graphics;
namespace Content.Client._EE.Flight.Components;
[RegisterComponent]
public sealed partial class FlightVisualsComponent : Component
{
/// <summary>
/// The speed of the shader animation.
/// </summary>
[DataField]
public float Speed;
/// <summary>
/// How far it goes in any direction.
/// </summary>
[DataField]
public float Multiplier;
/// <summary>
/// How much the limbs (if there are any) rotate.
/// </summary>
[DataField]
public float Offset;
/// <summary>
/// Are we animating layers or the entire sprite?
/// </summary>
public bool AnimateLayer = false;
public int? TargetLayer;
[DataField]
public string AnimationKey = "default";
[ViewVariables(VVAccess.ReadWrite)]
public ShaderInstance Shader = default!;
}
-65
View File
@@ -1,65 +0,0 @@
using Robust.Client.GameObjects;
using Content.Shared._EE.Flight;
using Content.Shared._EE.Flight.Events;
using Content.Client._EE.Flight.Components;
namespace Content.Client._EE.Flight;
public sealed class FlightSystem : SharedFlightSystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<FlightEvent>(OnFlight);
}
private void OnFlight(FlightEvent args)
{
var uid = GetEntity(args.Uid);
if (!_entityManager.TryGetComponent(uid, out SpriteComponent? sprite)
|| !args.IsAnimated
|| !_entityManager.TryGetComponent(uid, out FlightComponent? flight))
return;
int? targetLayer = null;
if (flight.IsLayerAnimated && flight.Layer is not null)
{
targetLayer = GetAnimatedLayer(uid, flight.Layer, sprite);
if (targetLayer == null)
return;
}
if (args.IsFlying && args.IsAnimated && flight.AnimationKey != "default")
{
var comp = new FlightVisualsComponent
{
AnimateLayer = flight.IsLayerAnimated,
AnimationKey = flight.AnimationKey,
Multiplier = flight.ShaderMultiplier,
Offset = flight.ShaderOffset,
Speed = flight.ShaderSpeed,
TargetLayer = targetLayer,
};
AddComp(uid, comp);
}
if (!args.IsFlying)
RemComp<FlightVisualsComponent>(uid);
}
public int? GetAnimatedLayer(EntityUid uid, string targetLayer, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite))
return null;
var index = 0;
foreach (var layer in sprite.AllLayers)
{
// This feels like absolute shitcode, isn't there a better way to check for it?
if (layer.Rsi?.Path.ToString() == targetLayer)
return index;
index++;
}
return null;
}
}
@@ -1,62 +0,0 @@
using Content.Client._EE.Flight.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
namespace Content.Client._EE.Flight;
/// <summary>
/// Handles offsetting an entity while flying
/// </summary>
public sealed class FlyingVisualizerSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FlightVisualsComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<FlightVisualsComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<FlightVisualsComponent, BeforePostShaderRenderEvent>(OnBeforeShaderPost);
}
private void OnStartup(EntityUid uid, FlightVisualsComponent comp, ComponentStartup args)
{
comp.Shader = _protoMan.Index<ShaderPrototype>(comp.AnimationKey).InstanceUnique();
AddShader(uid, comp.Shader, comp.AnimateLayer, comp.TargetLayer);
SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
}
private void OnShutdown(EntityUid uid, FlightVisualsComponent comp, ComponentShutdown args)
{
AddShader(uid, null, comp.AnimateLayer, comp.TargetLayer);
}
private void AddShader(Entity<SpriteComponent?> entity, ShaderInstance? shader, bool animateLayer, int? layer)
{
if (!Resolve(entity, ref entity.Comp, false))
return;
entity.Comp.PostShader = shader;
if (animateLayer && layer is not null)
entity.Comp.LayerSetShader(layer.Value, shader);
entity.Comp.GetScreenTexture = shader is not null;
entity.Comp.RaiseShaderEvent = shader is not null;
}
/// <summary>
/// This function can be used to modify the shader's values while its running.
/// </summary>
private void OnBeforeShaderPost(EntityUid uid, FlightVisualsComponent comp, ref BeforePostShaderRenderEvent args)
{
SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
}
private void SetValues(FlightVisualsComponent comp, float speed, float offset, float multiplier)
{
comp.Shader.SetParameter("Speed", speed);
comp.Shader.SetParameter("Offset", offset);
comp.Shader.SetParameter("Multiplier", multiplier);
}
}
@@ -1,38 +0,0 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-chat-create-group-title}"
MinSize="300 150">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Group Name input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-chat-group-name-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="GroupNameInput"
PlaceHolder="{Loc nano-chat-group-name-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Action buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="CancelButton"
Text="{Loc nano-chat-cancel}"
StyleClasses="OpenRight"
MinSize="80 0" />
<Button Name="CreateButton"
Text="{Loc nano-chat-create}"
StyleClasses="OpenLeft"
MinSize="80 0"
Disabled="True" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -1,59 +0,0 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class CreateGroupChatPopup : DefaultWindow
{
public event Action<string>? OnGroupCreated;
public CreateGroupChatPopup(int maxFullNameLength)
{
RobustXamlLoader.Load(this);
ContentsContainer.Margin = new Thickness(3);
// Button handlers
CancelButton.OnPressed += _ => Close();
CreateButton.OnPressed += _ => CreateGroup();
// Input validation
GroupNameInput.OnTextEntered += _ => CreateGroup();
GroupNameInput.OnTextChanged += args =>
{
if (args.Text.Length > maxFullNameLength)
GroupNameInput.Text = args.Text[..maxFullNameLength];
ValidateInputs();
};
}
private void ValidateInputs()
{
var isValid = !string.IsNullOrWhiteSpace(GroupNameInput.Text);
CreateButton.Disabled = !isValid;
}
private void CreateGroup()
{
if (CreateButton.Disabled)
return;
var name = GroupNameInput.Text.Trim();
OnGroupCreated?.Invoke(name);
Close();
}
public void ClearInputs()
{
GroupNameInput.Text = string.Empty;
ValidateInputs();
}
}
@@ -1,39 +0,0 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-chat-emoji-picker}"
MinSize="300 400">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Search box -->
<LineEdit Name="SearchInput"
PlaceHolder="{Loc nano-chat-emoji-search}"
HorizontalExpand="True"
Margin="0 0 0 4" />
<!-- Emoji grid -->
<ScrollContainer VerticalExpand="True"
HorizontalExpand="True"
Margin="0 4"
HScroll="0"
VScroll="1">
<GridContainer Name="EmojiGrid"
Columns="6"
HorizontalExpand="True" />
</ScrollContainer>
<!-- Close button -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="EmojiCloseButton"
Text="{Loc nano-chat-close}"
MinSize="80 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -1,150 +0,0 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using Content.Shared._Funkystation.NanoChat;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.ResourceManagement;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
using System.Numerics;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class EmojiPickerPopup : DefaultWindow
{
private List<(string name, SpriteSpecifier specifier)> _filteredEmojis;
private SpriteSystem? _spriteSystem;
public event Action<string>? OnEmojiSelected;
public EmojiPickerPopup()
{
RobustXamlLoader.Load(this);
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers.Select(kv => (kv.Key, kv.Value)).ToList();
ContentsContainer.Margin = new Thickness(3);
EmojiCloseButton.OnPressed += _ => Close();
SearchInput.OnTextChanged += OnSearchChanged;
OnResized += UpdateGridColumns;
PopulateEmojiGrid();
UpdateGridColumns();
}
private void UpdateGridColumns()
{
const int buttonSize = 48;
const int scrollbarWidth = 12;
var scrollContainer = EmojiGrid.Parent as ScrollContainer;
if (scrollContainer == null)
return;
var availableWidth = scrollContainer.PixelSize.X - scrollbarWidth;
if (availableWidth <= 0)
return;
var columns = Math.Max(1, (int) (availableWidth / buttonSize));
if (EmojiGrid.Columns != columns)
{
EmojiGrid.Columns = columns;
}
}
private void OnSearchChanged(LineEdit.LineEditEventArgs args)
{
var search = args.Text.ToLower().Trim();
if (string.IsNullOrEmpty(search))
{
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers.Select(kv => (kv.Key, kv.Value)).ToList();
}
else
{
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers
.Where(kv => kv.Key.Contains(search, StringComparison.OrdinalIgnoreCase))
.Select(kv => (kv.Key, kv.Value))
.ToList();
}
PopulateEmojiGrid();
}
private void PopulateEmojiGrid()
{
EmojiGrid.RemoveAllChildren();
var resCache = IoCManager.Resolve<IResourceCache>();
_spriteSystem ??= IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
foreach (var (name, specifier) in _filteredEmojis)
{
var button = new Button
{
MinSize = new Vector2(48, 48),
MaxSize = new Vector2(48, 48),
ToolTip = $":{name}:",
};
Texture texture;
switch (specifier)
{
case SpriteSpecifier.Texture texSpecifier:
if (!resCache.TryGetResource<TextureResource>(texSpecifier.TexturePath, out var textureResource))
continue;
texture = textureResource.Texture;
break;
case SpriteSpecifier.Rsi rsiSpecifier:
var state = _spriteSystem.GetState(rsiSpecifier);
texture = state.Frame0;
break;
default:
continue;
}
var textureRect = new TextureRect
{
Texture = texture,
Stretch = TextureRect.StretchMode.KeepAspect,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
};
button.AddChild(textureRect);
button.OnPressed += _ =>
{
OnEmojiSelected?.Invoke($":{name}:");
Close();
};
EmojiGrid.AddChild(button);
}
// Recalculate columns after populating
UpdateGridColumns();
}
public static SpriteSpecifier? GetEmojiSpecifier(string emojiName)
{
return NanoChatEmojis.EmojiSpecifiers.TryGetValue(emojiName, out var specifier) ? specifier : null;
}
public static Dictionary<string, SpriteSpecifier> GetEmojis() => new Dictionary<string, SpriteSpecifier>(NanoChatEmojis.EmojiSpecifiers);
}
@@ -1,115 +0,0 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Content.Shared._Funkystation.NanoChat;
using Robust.Shared.Utility;
using Content.Client._DV.NanoChat.UI; // DeltaV - Fix emoji alignment
using Robust.Shared.Prototypes; // DeltaV - Emoji id for debugging
namespace Content.Client._Funkystation.NanoChat;
/// <summary>
/// Markup tag for rendering emoji images inline in RichTextLabel.
/// </summary>
public sealed class EmojiTag : IMarkupTagHandler
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
private SpriteSystem? _spriteSystem;
public EmojiTag()
{
IoCManager.InjectDependencies(this);
}
public string Name => "emoji";
// Begin DeltaV - Render invalid emoji as text
private static ProtoId<NanoChatEmojiPrototype>? GetEmoji(MarkupNode node, out string text, out SpriteSpecifier? emoji)
{
if (!node.Value.TryGetString(out var emojiName))
{
// If we don't have a value, then render nothing
emoji = null;
text = string.Empty;
return null;
}
else if (NanoChatEmojis.EmojiSpecifiers.TryGetValue(emojiName, out emoji))
{
// If we have a valid emoji, render the emoji, but no text
text = string.Empty;
return emojiName;
}
else
{
// If we have an invalid emoji, render only text
text = $":{emojiName}:";
return emojiName;
}
}
string IMarkupTagHandler.TextBefore(MarkupNode node)
{
GetEmoji(node, out var text, out _);
return text;
}
// End DeltaV - Render invalid emoji as text
public bool TryCreateControl(MarkupNode node, out Control control)
{
control = default!;
var emojiId = GetEmoji(node, out _, out var maybeSpecifier);
if (maybeSpecifier is not { } specifier)
{
return false;
}
_spriteSystem ??= _entitySystemManager.GetEntitySystem<SpriteSystem>();
Texture texture;
switch (specifier)
{
case SpriteSpecifier.Texture texSpecifier:
if (!_resourceCache.TryGetResource<TextureResource>(texSpecifier.TexturePath, out var textureResource))
{
return false;
}
texture = textureResource.Texture;
break;
case SpriteSpecifier.Rsi rsiSpecifier:
var state = _spriteSystem.GetState(rsiSpecifier);
texture = state.Frame0;
break;
default:
return false;
}
// Begin DeltaV - Fix emoji alignment, replace TextureRect with custom EmojiControl
control = new EmojiControl
{
Texture = texture,
SetWidth = 32f,
SetHeight = 32f,
Name = $"Emoji[{emojiId}]",
// Evilly hard-coded Margin and Offset, don't ask me where they come from please
Margin = new(5f, 0f, 2f, 0),
Offset = new(5f, -5f),
};
// End DeltaV
return true;
}
}
@@ -1,32 +0,0 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-chat-group-members-title}"
MinSize="400 300">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Member list -->
<ScrollContainer VerticalExpand="True"
HorizontalExpand="True"
Margin="0 4">
<BoxContainer Name="MembersList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
<!-- Action buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="MembersCloseButton"
Text="{Loc nano-chat-close}"
MinSize="80 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -1,201 +0,0 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using System.Numerics;
using Content.Client.Resources;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Client.ResourceManagement;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class GroupMembersPopup : DefaultWindow
{
private List<NanoChatRecipient> _allContacts = new();
private HashSet<uint> _members = new();
private HashSet<uint> _admins = new();
private uint _ownNumber;
private uint? _creatorId;
public event Action<uint>? OnKick;
public event Action<uint>? OnAdmin;
public event Action<uint>? OnDeadmin;
public GroupMembersPopup()
{
RobustXamlLoader.Load(this);
ContentsContainer.Margin = new Thickness(3);
MembersCloseButton.OnPressed += _ => Close();
}
public void SetMembers(List<NanoChatRecipient> allContacts, HashSet<uint> members, uint ownNumber, uint? creatorId, HashSet<uint>? admins = null)
{
_allContacts = allContacts;
_members = members;
_ownNumber = ownNumber;
_creatorId = creatorId;
_admins = admins ?? new HashSet<uint>();
UpdateMembersList();
}
private void UpdateMembersList()
{
MembersList.RemoveAllChildren();
if (_members.Count == 0)
{
MembersList.AddChild(new Label { Text = Loc.GetString("nano-chat-no-members") });
return;
}
var isCreator = _creatorId == _ownNumber;
var isAdmin = _admins.Contains(_ownNumber);
var canManage = isCreator || isAdmin;
var memberContacts = _allContacts
.Where(c => _members.Contains(c.Number))
.OrderBy(c => c.Name)
.ToList();
for (var idx = 0; idx < memberContacts.Count; idx++)
{
var contact = memberContacts[idx];
var isEvenRow = idx % 2 == 0;
var isMemberAdmin = _admins.Contains(contact.Number);
var isMemberCreator = contact.Number == _creatorId;
var isSelf = contact.Number == _ownNumber;
var canKick = false;
if (isSelf) // can always leave yourself.
{
canKick = true;
}
else if (!isSelf && canManage && !isMemberCreator)
{
if (isCreator)
canKick = true;
else if (isAdmin && !isMemberAdmin)
canKick = true;
}
var canAdmin = canManage && !isSelf && !isMemberCreator && !isMemberAdmin;
var canDeadmin = canManage && !isSelf && !isMemberCreator && isMemberAdmin;
if (!isCreator && canDeadmin)
canDeadmin = false;
var contactControl = new MemberContainer(contact, isEvenRow, isMemberCreator, isMemberAdmin, isSelf, canKick, canAdmin, canDeadmin, OnKick, OnAdmin, OnDeadmin);
MembersList.AddChild(contactControl);
}
}
private sealed class MemberContainer : PanelContainer
{
public MemberContainer(NanoChatRecipient contact, bool isEvenRow, bool isCreator, bool isAdmin, bool isSelf, bool canKick, bool canMakeAdmin, bool canDeadmin, Action<uint>? onKick, Action<uint>? onAdmin, Action<uint>? onDeadmin)
{
HorizontalExpand = true;
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
var resCache = IoCManager.Resolve<IResourceCache>();
// Show name with job title if available
var displayName = contact.Name;
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
displayName = $"{contact.Name} ({contact.JobTitle})";
var nameLabel = new Label
{
Text = displayName + (isCreator ? $" {Loc.GetString("nano-chat-owner-suffix")}" : isAdmin ? $" {Loc.GetString("nano-chat-admin-suffix")}" : ""),
HorizontalAlignment = HAlignment.Left,
HorizontalExpand = true
};
var numberLabel = new Label
{
Text = $"#{contact.Number:D4}",
HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(0, 0, (canKick || canMakeAdmin || canDeadmin) ? 108 : 4, 0),
};
AddChild(nameLabel);
AddChild(numberLabel);
// Admin/Deadmin button
if (canMakeAdmin)
{
var adminButton = new Button
{
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
ToolTip = Loc.GetString("nano-chat-admin"),
Margin = new Thickness(0, 0, canKick ? 72 : 36, 0),
};
adminButton.AddStyleClass("OpenBoth");
adminButton.AddChild(new TextureRect
{
Texture = resCache.GetTexture("/Textures/_Funkystation/Interface/NanoChat/admin.png"),
Stretch = TextureRect.StretchMode.KeepAspectCentered,
MinSize = new Vector2(28, 28),
MaxSize = new Vector2(28, 28),
});
adminButton.OnPressed += _ => onAdmin?.Invoke(contact.Number);
AddChild(adminButton);
}
else if (canDeadmin)
{
var deadminButton = new Button
{
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
ToolTip = Loc.GetString("nano-chat-deadmin"),
Margin = new Thickness(0, 0, canKick ? 72 : 36, 0),
};
deadminButton.AddStyleClass("OpenBoth");
deadminButton.AddChild(new TextureRect
{
Texture = resCache.GetTexture("/Textures/_Funkystation/Interface/NanoChat/deadmin.png"),
Stretch = TextureRect.StretchMode.KeepAspectCentered,
MinSize = new Vector2(28, 28),
MaxSize = new Vector2(28, 28),
});
deadminButton.OnPressed += _ => onDeadmin?.Invoke(contact.Number);
AddChild(deadminButton);
}
// Kick/Leave button
if (canKick)
{
var kickButton = new Button
{
Text = isSelf ? Loc.GetString("nano-chat-leave") : "X",
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(isSelf ? 64 : 32, 32),
MaxSize = new Vector2(isSelf ? 64 : 32, 32),
ToolTip = isSelf ? Loc.GetString("nano-chat-leave") : Loc.GetString("nano-chat-kick"),
Margin = new Thickness(0, 0, 36, 0),
};
kickButton.AddStyleClass("OpenBoth");
kickButton.OnPressed += _ => onKick?.Invoke(contact.Number);
AddChild(kickButton);
}
}
}
}
@@ -1,43 +0,0 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc nano-chat-invite-to-group-title}"
MinSize="400 300">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Search input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-chat-search-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="SearchInput"
PlaceHolder="{Loc nano-chat-search-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Contact list -->
<ScrollContainer VerticalExpand="True"
HorizontalExpand="True"
Margin="0 4">
<BoxContainer Name="ContactsList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
<!-- Action buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="InviteCloseButton"
Text="{Loc nano-chat-close}"
MinSize="80 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -1,191 +0,0 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using System.Numerics;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class InviteToGroupPopup : DefaultWindow
{
private List<NanoChatRecipient> _allContacts = new();
private HashSet<uint> _existingMembers = new();
private uint _ownNumber;
public event Action<uint>? OnInvite;
public InviteToGroupPopup()
{
RobustXamlLoader.Load(this);
ContentsContainer.Margin = new Thickness(3);
InviteCloseButton.OnPressed += _ => Close();
SearchInput.OnTextChanged += _ => UpdateContactList();
}
public void SetContacts(List<NanoChatRecipient> contacts, HashSet<uint> existingMembers, uint ownNumber)
{
_allContacts = contacts;
_existingMembers = existingMembers;
_ownNumber = ownNumber;
UpdateContactList();
}
private void UpdateContactList()
{
ContactsList.RemoveAllChildren();
if (_allContacts.Count == 0)
{
ContactsList.AddChild(new Label { Text = Loc.GetString("nano-chat-look-up-no-server") });
return;
}
var searchText = SearchInput.Text.ToLower();
var filteredContacts = _allContacts
.Where(c => c.Number != _ownNumber &&
!_existingMembers.Contains(c.Number) &&
(string.IsNullOrWhiteSpace(searchText) ||
c.Name.ToLower().Contains(searchText) ||
c.Number.ToString().Contains(searchText) ||
(!string.IsNullOrWhiteSpace(c.JobTitle) && c.JobTitle.ToLower().Contains(searchText))))
.ToList();
if (filteredContacts.Count == 0)
{
ContactsList.AddChild(new Label { Text = Loc.GetString("nano-chat-no-results") });
return;
}
// Group contacts by department like crew manifest
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var departmentGroups = new Dictionary<DepartmentPrototype, List<NanoChatRecipient>>();
var noDepartmentContacts = new List<NanoChatRecipient>();
foreach (var contact in filteredContacts)
{
if (!string.IsNullOrWhiteSpace(contact.Department) &&
prototypeManager.TryIndex<DepartmentPrototype>(contact.Department, out var dept))
{
departmentGroups.GetOrNew(dept).Add(contact);
}
else
{
noDepartmentContacts.Add(contact);
}
}
// Sort departments by weight
var sortedDepartments = departmentGroups.Keys.ToList();
sortedDepartments.Sort(DepartmentUIComparer.Instance);
var rowIndex = 0;
foreach (var department in sortedDepartments)
{
var departmentContacts = departmentGroups[department];
departmentContacts.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
// Add department header
ContactsList.AddChild(new Label
{
Text = Loc.GetString(department.Name),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, rowIndex == 0 ? 0 : 8, 0, 2)
});
foreach (var contact in departmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, isEvenRow, OnInvite);
ContactsList.AddChild(contactControl);
rowIndex++;
}
}
// Add contacts without departments at the end
if (noDepartmentContacts.Count > 0)
{
noDepartmentContacts.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
if (sortedDepartments.Count > 0)
{
ContactsList.AddChild(new Label
{
Text = Loc.GetString("nano-chat-other-department"),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, 8, 0, 2)
});
}
foreach (var contact in noDepartmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, isEvenRow, OnInvite);
ContactsList.AddChild(contactControl);
rowIndex++;
}
}
}
public void ClearSearch()
{
SearchInput.Text = string.Empty;
}
private sealed class ContactContainer : PanelContainer
{
public ContactContainer(NanoChatRecipient contact, bool isEvenRow, Action<uint>? onInvite)
{
HorizontalExpand = true;
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
// Show name with job title if available
var displayName = contact.Name;
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
displayName = $"{contact.Name} ({contact.JobTitle})";
var nameLabel = new Label
{
Text = displayName,
HorizontalAlignment = HAlignment.Left,
HorizontalExpand = true
};
var numberLabel = new Label
{
Text = $"#{contact.Number:D4}",
HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(0, 0, 36, 0),
};
var inviteButton = new Button
{
Text = "+",
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
ToolTip = Loc.GetString("nano-chat-invite"),
};
inviteButton.AddStyleClass("OpenBoth");
inviteButton.OnPressed += _ => onInvite?.Invoke(contact.Number);
AddChild(nameLabel);
AddChild(numberLabel);
AddChild(inviteButton);
}
}
}
+1 -1
View File
@@ -242,7 +242,7 @@ namespace Content.IntegrationTests.Tests
"MapGrid",
"StationEvent",
"TimedDespawn",
"TimedDespawnDetailed", // DeltaV
// makes an announcement on mapInit.
"AnnounceOnSpawn",
};
@@ -76,7 +76,6 @@ namespace Content.IntegrationTests.Tests
"Hammurabi", // DeltaV
"Lighthouse", // DeltaV
"Micro", // DeltaV
"Ovni", //DeltaV
"Pebble", // DeltaV
"Shoukou", // DeltaV
"Submarine", //DeltaV
@@ -9,7 +9,6 @@ using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Content.Shared.Roles;
using System.Diagnostics.CodeAnalysis;
using Content.Shared._DV.Access.Components; // DeltaV
using Content.Shared._DV.NanoChat; // DeltaV
using Content.Server.Clothing.Systems;
using Content.Server.Implants;
@@ -102,16 +101,11 @@ namespace Content.Server.Access.Systems
if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))
return;
// Begin DeltaV Additions
// Prevent certain types of IDs from being copied (i.e. Borg ID chips)
if (TryComp<PreventAgentIdComponent>(args.Target, out var preventable))
{
if (preventable.PopupText is { } popupText)
_popupSystem.PopupEntity(Loc.GetString(popupText), args.Target.Value, args.User);
return;
}
var beforeLength = access.Tags.Count;
access.Tags.UnionWith(targetAccess.Tags);
var addedLength = access.Tags.Count - beforeLength;
// Copy NanoChat data if available
// Begin DeltaV Additions - Copy NanoChat data if available
if (TryComp<NanoChatCardComponent>(args.Target, out var targetNanoChat) &&
TryComp<NanoChatCardComponent>(uid, out var agentNanoChat))
{
@@ -138,10 +132,6 @@ namespace Content.Server.Access.Systems
}
}
// End DeltaV Additions
var beforeLength = access.Tags.Count;
access.Tags.UnionWith(targetAccess.Tags);
var addedLength = access.Tags.Count - beforeLength;
_popupSystem.PopupEntity(Loc.GetString("agent-id-new", ("number", addedLength), ("card", args.Target)), args.Target.Value, args.User);
if (addedLength > 0)
Dirty(uid, access);
@@ -1,7 +1,6 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Components;
@@ -83,7 +82,6 @@ public sealed partial class DeltaPressureComponent : Component
/// The minimum difference in pressure between any side required for the entity to start taking damage.
/// </summary>
[DataField]
[GuidebookData]
public float MinPressureDelta = 7500;
/// <summary>
@@ -37,26 +37,4 @@ public sealed partial class DragonRiftComponent : SharedDragonRiftComponent
[ViewVariables(VVAccess.ReadWrite), DataField("spawn", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SpawnPrototype = "MobCarpDragon";
//Begin DeltaV - Elite spawns on dragon rifts
[DataField]
public bool SpawnElites = true;
[DataField("spawnElite")]
public EntProtoId? SpawnElitePrototype = "MobSharkminnowDragon";
/// <summary>
/// Every N-th spawn is the elite where N is Elite frequency
/// </summary>
[DataField]
public int SpawnEliteFrequency = 5;
/// <summary>
/// Accumulation of elite spawns
/// Starts at the same value as Frequency to guarantee the first spawn to be elite
/// </summary>
[DataField]
public int SpawnEliteAccumulator = 5;
//End DeltaV - Elite spawns on dragon rifts
}
+1 -10
View File
@@ -90,16 +90,7 @@ public sealed class DragonRiftSystem : EntitySystem
if (comp.SpawnAccumulator > comp.SpawnCooldown)
{
comp.SpawnAccumulator -= comp.SpawnCooldown;
//Begin DeltaV - Elite spawns on dragon rifts
comp.SpawnEliteAccumulator += 1;
var entSpawnPrototype = comp.SpawnPrototype;
if (comp.SpawnElites && comp.SpawnEliteAccumulator >= comp.SpawnEliteFrequency)
{
comp.SpawnEliteAccumulator -= comp.SpawnEliteFrequency;
entSpawnPrototype = comp.SpawnElitePrototype;
}
//End DeltaV - Elite spawns on dragon rifts
var ent = Spawn(entSpawnPrototype, xform.Coordinates); //Delta-v change: comp.SpawnPrototype -> entSpawnPrototype
var ent = Spawn(comp.SpawnPrototype, xform.Coordinates);
// Update their look to match the leader.
if (TryComp<RandomSpriteComponent>(comp.Dragon, out var randomSprite))
@@ -8,7 +8,6 @@ using Content.Server.Stack;
using Content.Shared.ActionBlocker;
using Content.Shared.Construction;
using Content.Shared.Database;
using Content.Shared._NF.Storage.Components; // Frontier
using JetBrains.Annotations;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
@@ -82,14 +81,6 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (volume <= 0 || !TryChangeMaterialAmount(uid, msg.Material, -volume))
return;
// Frontier
// If we made it this far, turn off the magnet before spawning materials
if (TryComp<MaterialStorageMagnetPickupComponent>(uid, out var magnet))
{
magnet.MagnetEnabled = false;
}
// end Frontier
var mats = SpawnMultipleFromMaterial(volume, material, Transform(uid).Coordinates, out _);
foreach (var mat in mats.Where(mat => !TerminatingOrDeleted(mat)))
{
@@ -111,12 +102,14 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (!base.TryInsertMaterialEntity(user, toInsert, receiver, storage, material, composition))
return false;
_audio.PlayPvs(storage.InsertingSound, receiver);
_popup.PopupEntity(Loc.GetString("machine-insert-item",
("user", user),
("machine", receiver),
("item", toInsert)),
receiver);
if (user != receiver) // Goobstation - for automation to not spam popups
_popup.PopupEntity(Loc.GetString("machine-insert-item",
("user", user),
("machine", receiver),
("item", toInsert)),
receiver);
_popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver),
("item", toInsert)), receiver);
QueueDel(toInsert);
// Logging
@@ -50,7 +50,7 @@ public sealed partial class WarDeclaratorComponent : Component
/// Time allowed for declaration of war
/// </summary>
[DataField]
public float WarDeclarationDelay = 8.0f; // DeltaV - was 6.0
public float WarDeclarationDelay = 6.0f;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan DisableAt;
@@ -51,9 +51,9 @@ public sealed partial class RadiationSystem : EntitySystem
_accumulator = 0f;
}
public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time, EntityUid? origin = null)
public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time)
{
var msg = new OnIrradiatedEvent(time, radsPerSecond, origin);
var msg = new OnIrradiatedEvent(time, radsPerSecond, uid);
RaiseLocalEvent(uid, msg);
}
@@ -55,13 +55,6 @@ public sealed partial class XenoArtifactSystem
var trigger = PrototypeManager.Index<XenoArchTriggerPrototype>(triggerId);
if (_entityWhitelist.IsWhitelistFail(trigger.Whitelist, ent))
continue;
// DeltaV - start of TriggerBlacklist
if (
(trigger.TriggerBlacklist != null && triggerPool.Any(otherTrigger => trigger.TriggerBlacklist.Contains(otherTrigger.ID)))
|| triggerPool.Any(otherTrigger => otherTrigger.TriggerBlacklist != null && otherTrigger.TriggerBlacklist.Contains(triggerId))
)
continue;
// DeltaV - end of TriggerBlacklist
triggerPool.Add(trigger);
}
@@ -1,16 +1,10 @@
using System; // DeltaV
using Content.Server.Research.Systems;
using Content.Server.Xenoarchaeology.Artifact;
using Content.Shared.Popups;
using Content.Shared.Psionics.Glimmer;// DeltaV
using Content.Shared._DV.Xenoarchaeology.BUI;// DeltaV
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Server.GameObjects; // DeltaV
using Robust.Shared.Audio.Systems;
namespace Content.Server.Xenoarchaeology.Equipment;
/// <inheritdoc />
@@ -20,8 +14,6 @@ public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly XenoArtifactSystem _xenoArtifact = default!;
[Dependency] private readonly GlimmerSystem _glimmerSystem = default!; // DeltaV
[Dependency] private readonly ArtifactAnalyzerSystem _analyzerSystem = default!; //DeltaV
/// <inheritdoc/>
public override void Initialize()
@@ -39,52 +31,21 @@ public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
if (!_research.TryGetClientServer(ent, out var server, out var serverComponent))
return;
// Begin DeltaV - Variables for extraction totals
var sumResearch = 0;
var sumGlimmer = 0;
if (!_analyzerSystem.TryGetAnalyzer(ent, out var analyzer))
return;
// End DeltaV
foreach (var node in _xenoArtifact.GetAllNodes(artifact.Value))
{
var research = _xenoArtifact.GetResearchValue(node);
_xenoArtifact.SetConsumedResearchValue(node, node.Comp.ConsumedResearchValue + research);
// Begin DeltaV - Only run if we have an artifact ready for extraction
if (analyzer != null)
{
sumGlimmer += (int)(research / (float)analyzer.Value.Comp.ExtractRatio);
research = (int)(research * GetGlimmerMultiplier(analyzer.Value.Comp));
}
// End DeltaV
sumResearch += research;
}
UpdateClientUI(ent, analyzer!.Value); // DeltaV
// 4-16-25: It's a sad day when a scientist makes negative 5k research
if (sumResearch <= 0)
return;
_glimmerSystem.Glimmer += sumGlimmer; // DeltaV - Add glimmer based on extracted points.
_research.ModifyServerPoints(server.Value, sumResearch, serverComponent);
_audio.PlayPvs(ent.Comp.ExtractSound, artifact.Value);
_popup.PopupEntity(Loc.GetString("analyzer-artifact-extract-popup"), artifact.Value, PopupType.Large);
}
// DeltaV
private void UpdateClientUI(EntityUid console, ArtifactAnalyzerComponent analyzer)
{
var uiSystem = EntityManager.System<UserInterfaceSystem>();
uiSystem.SetUiState(console, ArtifactAnalyzerUiKey.Key,
new AnalysisConsoleBoundUserInterfaceState(GetGlimmerMultiplier(analyzer), (float)analyzer.ExtractRatio));
}
// DeltaV
private float GetGlimmerMultiplier(ArtifactAnalyzerComponent comp)
{
return 1 + (MathF.Pow(_glimmerSystem.Glimmer / 1000f, 2f) * comp.PointGlimmerMultiplier);
}
}
@@ -1,105 +0,0 @@
using Content.Shared._DEN.TimedDespawn;
using Content.Shared.Examine;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
namespace Content.Server._DEN.TimedDespawn;
/// <summary>
/// This handles the <see cref="TimedDespawnDetailedComponent"/>
/// </summary>
public sealed class TimedDespawnDetailedSystem : EntitySystem
{
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
private readonly HashSet<EntityUid> _timedDespawns = new();
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<TimedDespawnDetailedComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TimedDespawnDetailedComponent, ExaminedEvent>(OnExamine);
}
public override void Update(float frameTime)
{
foreach (var entity in _timedDespawns)
{
if (!Exists(entity) || !TryComp<TimedDespawnDetailedComponent>(entity, out var timedDespawn))
continue;
TryDelete((entity, timedDespawn));
}
}
private void OnExamine(Entity<TimedDespawnDetailedComponent> ent, ref ExaminedEvent args)
{
var timeLeft = GetTimeRemaining(ent);
if (timeLeft == null || ent.Comp.ExamineLocId == null)
return;
var stringTime = double.Round(timeLeft.Value.TotalSeconds, 1);
var examineText = Loc.GetString(ent.Comp.ExamineLocId, ("remaining", stringTime));
args.PushMarkup(examineText, 1);
}
public void StartTimer(Entity<TimedDespawnDetailedComponent> ent)
{
ent.Comp.StartTime = _gameTiming.CurTime;
_timedDespawns.Add(ent);
if (ent.Comp.StartSound != null)
{
var entCoords = _transformSystem.GetMoverCoordinates(ent);
_audioSystem.PlayPredicted(ent.Comp.StartSound, entCoords, null, ent.Comp.StartSoundParams);
}
}
public void StopTimer(Entity<TimedDespawnDetailedComponent> ent)
{
ent.Comp.StartTime = TimeSpan.Zero;
_timedDespawns.Remove(ent);
}
public TimeSpan? GetTimeRemaining(Entity<TimedDespawnDetailedComponent> ent)
{
if (!_timedDespawns.Contains(ent))
return null;
var despawnAfterAsSpan = TimeSpan.FromSeconds(ent.Comp.Lifetime);
var timeLeft = (ent.Comp.StartTime + despawnAfterAsSpan) - _gameTiming.CurTime;
return timeLeft;
}
public void TryDelete(Entity<TimedDespawnDetailedComponent> ent)
{
var remaining = GetTimeRemaining(ent);
if (remaining == null || remaining.Value.TotalSeconds > 0)
return;
if (ent.Comp.EndSound != null)
{
var entCoords = _transformSystem.GetMoverCoordinates(ent);
_audioSystem.PlayPredicted(ent.Comp.EndSound, entCoords, null, ent.Comp.EndSoundParams);
}
StopTimer(ent);
EntityManager.QueueDeleteEntity(ent);
}
private void OnMapInit(Entity<TimedDespawnDetailedComponent> ent, ref MapInitEvent args)
{
var entCoords = _transformSystem.GetMoverCoordinates(ent);
if (ent.Comp.StartSound != null)
_audioSystem.PlayPredicted(ent.Comp.StartSound, entCoords, null, ent.Comp.StartSoundParams);
StartTimer(ent);
}
}
@@ -3,7 +3,6 @@ using Content.Server.Chat.Systems;
using Content.Server.Cloning;
using Content.Server.DoAfter;
using Content.Server.Mind;
using Content.Server.Psionics;
using Content.Server.Station.Systems;
using Content.Shared._DV.Abilities.Psionics;
using Content.Shared.Abilities.Psionics;
@@ -72,8 +71,7 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
component.NextSwap = _timing.CurTime + TimeSpan.FromSeconds(_random.Next(300, 1200));
if (HasComp<FracturedFormBodyComponent>(entity)) return; // Don't generate a new body if we're already part of a network.
var bodyComp = AddComp<FracturedFormBodyComponent>(entity);
bodyComp.ControllingForm = entity.Owner;
AddComp<FracturedFormBodyComponent>(entity);
component.Bodies.Add(entity);
GenerateForm(entity);
}
@@ -115,9 +113,9 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
}
var bodies = EntityQueryEnumerator<FracturedFormBodyComponent>();
while (bodies.MoveNext(out var uid, out var comp))
while (bodies.MoveNext(out var uid, out var _))
{
if (!HasComp<SleepingComponent>(uid) && !_mind.GetMind(uid).HasValue)
if (!HasComp<SleepingComponent>(uid) && !HasComp<FracturedFormPowerComponent>(uid))
{
_sleeping.TrySleeping(uid);
}
@@ -127,10 +125,6 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
// Ensure the body isn't forcesleep'd by the SSD system.
ssd.IsSSD = false;
}
// Cleanup the component from any out-of-network bodies. (See Mindbreaking)
if (!comp.ControllingForm.IsValid() || Deleted(comp.ControllingForm) || !HasComp<FracturedFormPowerComponent>(comp.ControllingForm))
RemCompDeferred<FracturedFormBodyComponent>(uid);
}
}
@@ -155,18 +149,8 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
var speciesPrototypes = _prototype.EnumeratePrototypes<SpeciesPrototype>();
foreach (var proto in speciesPrototypes)
{
var speciesEntityPrototype = _prototype.Index<EntityPrototype>(proto.Prototype);
if (proto.RoundStart && speciesEntityPrototype.TryGetComponent<PotentialPsionicComponent>(out var canBePsionic, Factory))
{
var chance = canBePsionic.Chance;
if (speciesEntityPrototype.TryGetComponent<PsionicBonusChanceComponent>(out var bonusChance, Factory))
chance = (chance * bonusChance.Multiplier) + bonusChance.FlatBonus;
if (chance > 0)
validSpecies.Add(proto.ID);
}
if (proto.RoundStart)
validSpecies.Add(proto.ID);
}
var species = _random.Pick(validSpecies);
var character = HumanoidCharacterProfile.RandomWithSpecies(species);
@@ -180,9 +164,8 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
if (newBody is { } body && !Deleted(body))
{
var bodyComp = AddComp<FracturedFormBodyComponent>(body);
AddComp<FracturedFormBodyComponent>(body);
original.Comp.Bodies.Add(body);
bodyComp.ControllingForm = original.Owner;
return body;
}
@@ -243,14 +226,6 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
var duplicate = AddComp<FracturedFormPowerComponent>(targetBody);
duplicate.Bodies = entity.Comp.Bodies;
// Pass the controlling form to the new component's bodies.
foreach (var body in duplicate.Bodies)
{
if (TryComp<FracturedFormBodyComponent>(body, out var bodyComp))
{
bodyComp.ControllingForm = targetBody;
}
}
RemCompDeferred(entity, entity.Comp);
return true;
}
@@ -11,7 +11,6 @@ public sealed partial class PsychokineticScreamPowerSystem : EntitySystem
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly ShatterLightsAbilitySystem _shatterLights = default!;
[Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
public override void Initialize()
{
@@ -55,8 +54,6 @@ public sealed partial class PsychokineticScreamPowerSystem : EntitySystem
SpawnAttachedTo(entity.Comp.Effect, entity.Owner.ToCoordinates());
_psionics.LogPowerUsed(entity.Owner, "psychokinetic scream", 3, 6);
args.Handled = true;
}
@@ -46,17 +46,11 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage);
SubscribeLocalEvent<NanoChatCartridgeComponent, MapInitEvent>(OnMapInit);
Subs.CVar(_cfg, CCVars.MaxNameLength, x => _maxNameLength = x, true);
Subs.CVar(_cfg, CCVars.MaxIdJobLength, x => _maxIdJobLength = x, true);
}
private void OnMapInit(Entity<NanoChatCartridgeComponent> ent, ref MapInitEvent args)
{
UpdateUIForAllCards();
}
private void UpdateClosed(Entity<NanoChatCartridgeComponent> ent)
{
if (!TryComp<CartridgeComponent>(ent, out var cartridge) ||
@@ -130,7 +124,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
HandleCloseChat(card);
break;
case NanoChatUiMessageType.ToggleMute:
HandleToggleMute(card, msg);
HandleToggleMute(card);
break;
case NanoChatUiMessageType.ToggleMuteChat:
HandleToggleMuteChat(card, msg);
@@ -142,22 +136,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
HandleSendMessage(ent, card, msg);
break;
case NanoChatUiMessageType.ToggleListNumber:
HandleToggleListNumber(card, msg);
break;
case NanoChatUiMessageType.CreateGroupChat:
HandleCreateGroupChat(card, msg);
break;
case NanoChatUiMessageType.InviteToGroup:
HandleInviteToGroup(card, msg);
break;
case NanoChatUiMessageType.KickFromGroup:
HandleKickFromGroup(card, msg);
break;
case NanoChatUiMessageType.AdminUser:
HandleAdminUser(card, msg);
break;
case NanoChatUiMessageType.DeadminUser:
HandleDeadminUser(card, msg);
HandleToggleListNumber(card);
break;
}
@@ -211,24 +190,16 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
}
// Add new recipient
var recipient = new NanoChatRecipient(
msg.RecipientNumber.Value,
var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
name,
jobTitle,
null,
false,
false,
null,
null,
null
);
jobTitle);
// Initialize or update recipient
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})");
$"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
@@ -306,65 +277,15 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (msg.RecipientNumber == null || card.Comp.Number == null)
return;
var chatNumber = msg.RecipientNumber.Value;
var recipient = _nanoChat.GetRecipient((card, card.Comp), chatNumber);
if (recipient is { IsGroup: true } groupChat)
{
var members = groupChat.Members ?? new HashSet<uint>();
if (members.Remove(card.Comp.Number.Value))
{
var admins = groupChat.Admins ?? new HashSet<uint>();
admins.Remove(card.Comp.Number.Value);
// If the creator is leaving, transfer ownership
uint? newCreatorId = groupChat.CreatorId;
var isCreatorLeaving = card.Comp.Number.Value == groupChat.CreatorId;
if (isCreatorLeaving)
{
if (admins.Count > 0)
{
newCreatorId = admins.First();
}
else if (members.Count > 0)
{
newCreatorId = members.First();
}
else
{
newCreatorId = null;
}
}
// Update the group for all remaining members
var updatedRecipient = newCreatorId != null
? groupChat with { Members = members, Admins = admins, CreatorId = newCreatorId }
: groupChat with { Members = members, Admins = admins };
foreach (var memberNumber in members)
{
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var memberCardUid, out var memberCard))
{
if (memberCard.Number == memberNumber)
{
_nanoChat.SetRecipient((memberCardUid, memberCard), chatNumber, updatedRecipient);
UpdateUIForCard(memberCardUid);
}
}
}
}
}
// Delete chat but keep the messages
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), chatNumber, true);
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true);
if (!deleted)
return;
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} deleted NanoChat conversation with #{chatNumber:D4}");
$"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}");
UpdateUIForCard(card);
}
@@ -372,15 +293,9 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
/// <summary>
/// Handles toggling notification mute state.
/// </summary>
private void HandleToggleMute(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
private void HandleToggleMute(Entity<NanoChatCardComponent> card)
{
var muted = _nanoChat.GetNotificationsMuted((card, card.Comp));
_nanoChat.SetNotificationsMuted((card, card.Comp), !muted);
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} {(muted ? "un" : "")}muted the NanoChat on {ToPrettyString(card)}");
_nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp)));
UpdateUIForCard(card);
}
@@ -388,25 +303,13 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{
if (msg.RecipientNumber is not uint chat)
return;
_nanoChat.ToggleChatMuted((card, card.Comp), chat);
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} toggled NanoChat notifcations for chat #{chat:D4} on {ToPrettyString(card)}");
UpdateUIForCard(card);
}
private void HandleToggleListNumber(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
private void HandleToggleListNumber(Entity<NanoChatCardComponent> card)
{
var numberListed = _nanoChat.GetListNumber((card, card.Comp));
_nanoChat.SetListNumber((card, card.Comp), !numberListed);
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} toggled the NanoChat listing of {ToPrettyString(card)} {(numberListed ? "off" : "on")}");
_nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp)));
UpdateUIForAllCards();
}
@@ -420,11 +323,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value);
var isGroupChat = recipient?.IsGroup ?? false;
// Only ensure recipient exists for non-group chats
if (!isGroupChat && !EnsureRecipientExists(card, msg.RecipientNumber.Value))
if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
return;
var content = msg.Content;
@@ -442,19 +341,8 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
(uint)card.Comp.Number
);
List<Entity<NanoChatCardComponent>> recipients;
bool deliveryFailed;
if (isGroupChat && recipient != null)
{
// For group chats, deliver to all members
(deliveryFailed, recipients) = AttemptGroupMessageDelivery(cartridge, recipient.Value, card.Comp.Number.Value);
}
else
{
// For regular chats, deliver to single recipient
(deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
}
// Attempt delivery
var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
// Update delivery status
message = message with { DeliveryFailed = deliveryFailed };
@@ -469,7 +357,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
$"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
var msgEv = new NanoChatMessageReceivedEvent(card);
RaiseLocalEvent(ref msgEv);
@@ -477,9 +365,9 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (deliveryFailed)
return;
foreach (var recipientCard in recipients)
foreach (var recipient in recipients)
{
DeliverMessageToRecipient(card, recipientCard, message, msg.RecipientNumber.Value, recipient);
DeliverMessageToRecipient(card, recipient, message);
}
}
@@ -592,88 +480,27 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
/// <param name="sender">The sender's card entity</param>
/// <param name="recipient">The recipient's card entity</param>
/// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param>
/// <param name="chatNumber">The chat number (for group chats, this is the group number)</param>
/// <param name="groupRecipient">Optional group recipient info if this is a group chat</param>
private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender,
Entity<NanoChatCardComponent> recipient,
NanoChatMessage message,
uint chatNumber,
NanoChatRecipient? groupRecipient = null)
NanoChatMessage message)
{
if (sender.Comp.Number is not uint senderNumber)
return;
// For group chats, use the group number. For regular chats, use the sender's number
var recipientNumber = (groupRecipient != null && groupRecipient.Value.IsGroup) ? chatNumber : senderNumber;
if (groupRecipient != null && groupRecipient.Value.IsGroup)
{
var existingRecipient = _nanoChat.GetRecipient((recipient, recipient.Comp), recipientNumber);
if (existingRecipient == null || !existingRecipient.Value.IsGroup)
{
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
}
else if (groupRecipient.Value.Members != null)
{
var existingMembers = existingRecipient.Value.Members ?? new HashSet<uint>();
if (!existingMembers.SetEquals(groupRecipient.Value.Members))
{
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
}
else
{
var existingAdmins = existingRecipient.Value.Admins ?? new HashSet<uint>();
var groupAdmins = groupRecipient.Value.Admins ?? new HashSet<uint>();
if (!existingAdmins.SetEquals(groupAdmins))
{
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
}
}
}
}
else if (!EnsureRecipientExists(recipient, recipientNumber))
{
// Always try to get and add sender info to recipient's contacts
if (!EnsureRecipientExists(recipient, senderNumber))
return;
}
_nanoChat.AddMessage((recipient, recipient.Comp), recipientNumber, message with { DeliveryFailed = false });
_nanoChat.AddMessage((recipient, recipient.Comp), senderNumber, message with { DeliveryFailed = false });
if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != recipientNumber)
HandleUnreadNotification(recipient, message, recipientNumber);
if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
HandleUnreadNotification(recipient, message, senderNumber);
var msgEv = new NanoChatMessageReceivedEvent(recipient);
RaiseLocalEvent(ref msgEv);
UpdateUIForCard(recipient);
}
/// <summary>
/// Attempts to deliver a message to all members of a group chat.
/// </summary>
private (bool failed, List<Entity<NanoChatCardComponent>> recipients) AttemptGroupMessageDelivery(
Entity<NanoChatCartridgeComponent> sender,
NanoChatRecipient groupRecipient,
uint senderNumber)
{
if (groupRecipient.Members == null)
return (true, new List<Entity<NanoChatCardComponent>>());
var deliverableRecipients = new List<Entity<NanoChatCardComponent>>();
foreach (var memberNumber in groupRecipient.Members)
{
if (memberNumber == senderNumber)
continue;
var (failed, memberCards) = AttemptMessageDelivery(sender, memberNumber);
if (!failed)
deliverableRecipients.AddRange(memberCards);
}
// Delivery failed if no recipients could receive the message
return (deliverableRecipients.Count == 0, deliverableRecipients);
}
/// <summary>
/// Handles unread message notifications and updates unread status.
/// </summary>
@@ -683,21 +510,21 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{
// Get sender name from contacts or fall back to number
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
var senderName = recipients.TryGetValue(senderNumber, out var senderRecipient)
var senderName = recipients.TryGetValue(message.SenderId, out var senderRecipient)
? senderRecipient.Name
: $"#{senderNumber:D4}";
: $"#{message.SenderId:D4}";
var hasSelectedCurrentChat = _nanoChat.GetCurrentChat((recipient, recipient.Comp)) == senderNumber;
// Update unread status
if (!hasSelectedCurrentChat)
_nanoChat.SetRecipient((recipient, recipient.Comp),
senderNumber,
message.SenderId,
senderRecipient with { HasUnread = true });
// Temporary local to avoid trouble with read-only access; Contains doesn't modify the collection
HashSet<uint> mutedChats = recipient.Comp.MutedChats;
if (recipient.Comp.NotificationsMuted ||
mutedChats.Contains(senderNumber) ||
mutedChats.Contains(message.SenderId) ||
recipient.Comp.PdaUid is not { } pdaUid ||
!TryComp<CartridgeLoaderComponent>(pdaUid, out var loader) ||
// Don't notify if the recipient has the NanoChat program open with this chat selected.
@@ -766,27 +593,14 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
// Try to get job title from ID card if possible
string? jobTitle = null;
string? department = null;
var name = "Unknown";
if (TryComp<IdCardComponent>(uid, out var idCard))
{
jobTitle = idCard.LocalizedJobTitle;
name = idCard.FullName ?? name;
if (idCard.JobDepartments is { } depts && depts.Count > 0)
department = depts[0].ToString();
}
return new NanoChatRecipient(
number,
name,
jobTitle,
department,
false,
false,
null,
null,
null
);
return new NanoChatRecipient(number, name, jobTitle);
}
return null;
@@ -810,34 +624,12 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var query = AllEntityQuery<NanoChatCardComponent, IdCardComponent>();
while (query.MoveNext(out var entityId, out var nanoChatCard, out var idCardComponent))
{
if (!nanoChatCard.ListNumber)
continue;
if (nanoChatCard.PdaUid is null)
continue;
if (nanoChatCard.Number is not { } nanoChatNumber)
continue;
if (idCardComponent.FullName is not { } fullName)
continue;
if (_station.GetOwningStation(entityId) != station)
continue;
string? department = null;
if (idCardComponent.JobDepartments is { } depts && depts.Count > 0)
department = depts[0].ToString();
contacts.Add(new NanoChatRecipient(
nanoChatNumber,
fullName,
idCardComponent.LocalizedJobTitle,
department,
false,
false,
null,
null,
null
));
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName && _station.GetOwningStation(entityId) == station)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName));
}
}
contacts.Sort();
contacts.Sort((contactA, contactB) => string.CompareOrdinal(contactA.Name, contactB.Name));
}
else
{
@@ -876,314 +668,4 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
listNumber);
_cartridge.UpdateCartridgeUiState(loader, state);
}
/// <summary>
/// Handles creation of a new group chat.
/// </summary>
private void HandleCreateGroupChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.Content == null || card.Comp.Number == null)
return;
var name = msg.Content;
if (!string.IsNullOrWhiteSpace(name))
{
name = name.Trim();
if (name.Length > _maxNameLength)
name = name[.._maxNameLength];
}
// Generate a unique group number (surely unique I actually have no idea how to generate good unique numbers.)
var groupNumber = (uint)(HashCode.Combine(card.Comp.Number.Value, _timing.CurTime.Ticks) & 0x7FFFFFFF);
// This fucking sucks (fire emoji)
while (_nanoChat.GetRecipient((card, card.Comp), groupNumber) != null)
{
groupNumber++;
}
// Create group chat recipient
var recipient = new NanoChatRecipient(
groupNumber,
name,
null,
null,
false,
true,
[card.Comp.Number.Value],
card.Comp.Number.Value,
null
);
_nanoChat.SetRecipient((card, card.Comp), groupNumber, recipient);
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} created group chat '{name}' (#{groupNumber:D4})");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles inviting a user to a group chat.
/// </summary>
private void HandleInviteToGroup(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var inviteeNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
// Only the creator or admins can invite
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
if (!isCreator && !isAdmin)
return;
var members = recipient.Value.Members ?? new HashSet<uint>();
if (members.Contains(inviteeNumber))
return;
// Add member to group
members.Add(inviteeNumber);
var updatedRecipient = recipient.Value with { Members = members };
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
// Update member lists for all members
var memberCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
{
if (memberCard.Number != null && members.Contains(memberCard.Number.Value))
{
memberCards.Add((cardUid, memberCard));
}
}
foreach (var memberCard in memberCards)
{
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
UpdateUIForCard(memberCard);
}
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} invited #{inviteeNumber:D4} to group chat #{groupNumber:D4}");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles kicking a user from a group chat.
/// </summary>
private void HandleKickFromGroup(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var kickeeNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
var isSelfKick = kickeeNumber == card.Comp.Number.Value;
if (!isSelfKick && !isCreator && !isAdmin)
return;
// Allow creator to leave the group but not be kicked by other members.
var isCreatorLeaving = kickeeNumber == recipient.Value.CreatorId && isCreator;
if (kickeeNumber == recipient.Value.CreatorId && !isCreatorLeaving)
return;
// Find all cards belonging to the kickee
var kickeeCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var kickeeCard))
{
if (kickeeCard.Number == kickeeNumber)
{
kickeeCards.Add((cardUid, kickeeCard));
}
}
foreach (var kickeeCard in kickeeCards)
{
var deleteMsg = new NanoChatUiMessageEvent(NanoChatUiMessageType.DeleteChat, groupNumber, null, null)
{
Actor = msg.Actor
};
HandleDeleteChat(kickeeCard, deleteMsg);
}
if (isCreatorLeaving)
{
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} left group chat #{groupNumber:D4}");
}
else
{
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} kicked #{kickeeNumber:D4} from group chat #{groupNumber:D4}");
}
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles promoting a user to admin in a group chat.
/// </summary>
private void HandleAdminUser(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var targetNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
// Only the creator or admins can make admins
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
if (!isCreator && !isAdmin)
return;
var members = recipient.Value.Members ?? new HashSet<uint>();
if (!members.Contains(targetNumber))
return; // how tf
if (targetNumber == recipient.Value.CreatorId)
return;
if (!isCreator && admins.Contains(targetNumber))
return;
if (!admins.Add(targetNumber))
return;
var updatedRecipient = recipient.Value with { Admins = admins };
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
// Sync to all members
foreach (var memberNumber in members)
{
var memberCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
{
if (memberCard.Number == memberNumber)
{
memberCards.Add((cardUid, memberCard));
}
}
foreach (var memberCard in memberCards)
{
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
UpdateUIForCard(memberCard);
}
}
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} promoted #{targetNumber:D4} to admin in group chat #{groupNumber:D4}");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles demoting a user from admin in a group chat.
/// </summary>
private void HandleDeadminUser(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var targetNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
// Only the creator or admins can remove admins
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
if (!isCreator && !isAdmin)
return;
var members = recipient.Value.Members ?? new HashSet<uint>();
if (!members.Contains(targetNumber))
return; // how
if (!isCreator && (targetNumber == recipient.Value.CreatorId || admins.Contains(targetNumber)))
return;
if (!admins.Remove(targetNumber))
return;
var updatedRecipient = recipient.Value with { Admins = admins };
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
// Sync to all members
foreach (var memberNumber in members)
{
var memberCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
{
if (memberCard.Number == memberNumber)
{
memberCards.Add((cardUid, memberCard));
}
}
foreach (var memberCard in memberCards)
{
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
UpdateUIForCard(memberCard);
}
}
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} removed admin from #{targetNumber:D4} in group chat #{groupNumber:D4}");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
}
@@ -62,7 +62,6 @@ public sealed class CosmicRiftSystem : EntitySystem
var pos = Transform(uid).Coordinates;
Spawn(comp.PulseVFX, pos);
_mobs.Clear();
_lookup.GetEntitiesInRange<MobStateComponent>(pos, comp.PulseRange, _mobs);
_mobs.RemoveWhere(target => _chaplainsQuery.HasComp(target) || _cultistsQuery.HasComp(target) || _colossiQuery.HasComp(target));
foreach(var mob in _mobs)
@@ -19,6 +19,6 @@ public sealed class SiliconEmpSystem : EntitySystem
{
args.Cancel(); // Stop all the normal effects of the EMP
if (args.Damage is not { } damage) return;
_damageable.TryChangeDamage(ent, damage / 2, false); // Damage is divided by 2 because the event is raised twice (once from entity itself, and another is relayed from it's power cell) and I'm too lazy for an actual fix - NoElka | Make EMP not ignore armor.
_damageable.TryChangeDamage(ent, damage / 2, true); // Damage is divided by 2 because the event is raised twice (once from entity itself, and another is relayed from it's power cell) and I'm too lazy for an actual fix
}
}
@@ -1,22 +0,0 @@
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// DeltaV - Prohibits meteor swarms from targeting their impact near this component.
/// </summary>
[RegisterComponent]
public sealed partial class AntiMeteorZoneComponent : Component
{
/// <summary>
/// The size of the area around this entity to avoid.
/// </summary>
[DataField]
public float ZoneRadius = 5f;
/// <summary>
/// Percentage (roughly) of meteors that are BLOCKED from hitting near this component.
/// </summary>
[DataField]
public float AvoidanceRate = 0.75f;
}
@@ -1,24 +0,0 @@
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// DeltaV - Prohibits meteor swarms from targeting their impact near this component.
/// Standardized avoidance rate; cannot be changed in View Variables.
/// </summary>
[RegisterComponent]
public sealed partial class AntiMeteorZoneStandardRateComponent : Component
{
/// <summary>
/// The size of the area around this entity to avoid.
/// </summary>
[DataField]
public float ZoneRadius = 5f;
/// <summary>
/// Percentage (roughly) of meteors that are BLOCKED from hitting near this component.
/// Standardized avoidance rate; cannot be changed in View Variables.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)] // I tried to inherit AntiMeteorZoneComponent, but this VVAccess wouldn't take effect
public float AvoidanceRate = 0.75f;
}
@@ -1,28 +0,0 @@
using Content.Server.Power.Components;
using Content.Server.StationEvents.Events;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// When fired, turns off power on the station for a few seconds, playing <see cref="EpsilonEventRuleComponent.PowerOffSound"/>
/// Afterwards turns power back on and sets alert level to epsilon
/// </summary>
[RegisterComponent, Access(typeof(EpsilonEventRule))]
public sealed partial class EpsilonEventRuleComponent : Component
{
/// <summary>
/// Default sound of the announcement when power turns off.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultPowerOff = new("PowerOff");
/// <summary>
/// Sound of the announcement to play when power turns off.
/// </summary>
[DataField]
public SoundSpecifier PowerOffSound = new SoundCollectionSpecifier(DefaultPowerOff, AudioParams.Default.WithVolume(-2f));
public EntityUid AffectedStation;
public readonly HashSet<Entity<ApcComponent>> ToggledAPCs = new();
}
@@ -1,33 +0,0 @@
using Content.Server._DV.StationEvents.Events;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server._DV.StationEvents.Components;
[RegisterComponent, Access(typeof(MinorMassMindSwapRule))]
public sealed partial class MinorMassMindSwapRuleComponent : Component
{
/// <summary>
/// The mind swap is only temporary if true.
/// </summary>
[DataField("isTemporary")]
public bool IsTemporary = false;
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(60);
[DataField]
public SoundSpecifier AnnouncementSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
[DataField]
public SoundSpecifier SwapWarningSound = new SoundPathSpecifier("/Audio/_DV/Effects/clang2.ogg");
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? SoundTime;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? SwapTime;
[DataField]
public int MaxNumberOfPairs = 3;
}
@@ -1,73 +0,0 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Server.AlertLevel;
using Content.Server.Audio;
using Robust.Shared.Audio.Systems;
namespace Content.Server.StationEvents.Events
{
public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleComponent>
{
[Dependency] private readonly ApcSystem _apcSystem = default!;
[Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!;
[Dependency] private readonly ServerGlobalSoundSystem _sound = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
protected override void Started(EntityUid uid, EpsilonEventRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
// This should be checked before playing the sound
if (!TryGetRandomStation(out var chosenStation))
return;
component.AffectedStation = chosenStation.Value;
// Plays the power off sound to the station.
_sound.PlayGlobalOnStation(component.AffectedStation, _audio.ResolveSound(component.PowerOffSound));
var query = AllEntityQuery<ApcComponent, TransformComponent>();
while (query.MoveNext(out var apcUid, out var apc, out var transform))
{
if (CompOrNull<StationMemberComponent>(transform.GridUid)?.Station != chosenStation)
continue;
// Turns off APCs during the event. Basic power-outage event.
if (apc.MainBreakerEnabled)
{
_apcSystem.ApcToggleBreaker(apcUid, apc);
component.ToggledAPCs.Add((apcUid, apc));
}
}
}
protected override void Ended(EntityUid uid, EpsilonEventRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
{
base.Ended(uid, component, gameRule, args);
// Checks if station exists.
if (!Exists(component.AffectedStation))
return;
// Restores the power to the station.
foreach (var apc in component.ToggledAPCs)
{
if (Deleted(apc))
continue;
if (!apc.Comp.MainBreakerEnabled)
_apcSystem.ApcToggleBreaker(apc, apc);
}
component.ToggledAPCs.Clear();
// Final effect, sets the code to Epsilon
if (!HasComp<AlertLevelComponent>(component.AffectedStation))
return;
_alertLevelSystem.SetLevel(component.AffectedStation, "epsilon", true, true, true);
}
}
}
@@ -1,4 +1,3 @@
using System.Linq;
using System.Numerics;
using Content.Server.Atmos.Components;
using Content.Server.GameTicking.Rules.Components;
@@ -8,7 +7,6 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
using Robust.Shared.Spawners;
namespace Content.Server.StationEvents.Events
@@ -89,83 +87,35 @@ namespace Content.Server.StationEvents.Events
(var target, var targetArea) = GetTarget();
var targetSpread = (targetArea.TopRight - targetArea.Center).Length() * component.TargetingSpread;
var protectedAreas = new List<(MapCoordinates center, float radiusSquared, float protectionRate)>();
var protectedAreaQuery = AllEntityQuery<AntiMeteorZoneComponent, TransformComponent>();
while (protectedAreaQuery.MoveNext(out var protectedEntityUid, out var antiZone, out var transform))
{
protectedAreas.Add((
center: _transform.ToMapCoordinates(transform.Coordinates),
radiusSquared: MathF.Pow(antiZone.ZoneRadius, 2f),
protectionRate: antiZone.AvoidanceRate
));
}
// dang, I wish I could use Linq on queries
var standardRateProtectedAreaQuery = AllEntityQuery<AntiMeteorZoneStandardRateComponent, TransformComponent>();
while (standardRateProtectedAreaQuery.MoveNext(out var protectedEntityUid, out var antiZone, out var transform))
{
protectedAreas.Add((
center: _transform.ToMapCoordinates(transform.Coordinates),
radiusSquared: MathF.Pow(antiZone.ZoneRadius, 2f),
protectionRate: antiZone.AvoidanceRate
));
}
float maxImpactTime = 0;
for (var i = 0; i < component.MeteorsPerWave; i++)
{
var protectedAreasThisMeteor =
protectedAreas.Where(protectedArea => RobustRandom.Prob(protectedArea.protectionRate)).ToList();
MapCoordinates spawnPosition;
Vector2 velocity;
int targetingAttempts = 0;
bool targetingSafe;
do
{
var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
spawnPosition = new MapCoordinates(
target.X + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.X,
target.Y + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.Y,
mapId
);
velocity = -offset.Normalized() * component.MeteorVelocity;
targetingSafe = true;
foreach (var protectedArea in protectedAreasThisMeteor)
{
var timeUntilClosestApproachToProtectedArea = TimeUntilClosestApproach(spawnPosition, velocity, protectedArea.center);
var pointOfClosestApproachToProtectedArea = spawnPosition.Position + timeUntilClosestApproachToProtectedArea * velocity;
if (
Vector2.DistanceSquared(protectedArea.center.Position, pointOfClosestApproachToProtectedArea)
< protectedArea.radiusSquared
)
{
targetingSafe = false;
break;
}
}
targetingAttempts++;
}
while (!targetingSafe && targetingAttempts <= 3); // attempt to avoid the protected areas a few times
var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
var spawnPosition = new MapCoordinates(
target.X + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.X,
target.Y + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.Y,
mapId
);
var meteor = Spawn(proto, spawnPosition);
var physics = EntityManager.GetComponent<PhysicsComponent>(meteor);
_physics.SetBodyStatus(meteor, physics, BodyStatus.InAir);
_physics.SetLinearDamping(meteor, physics, 0f);
_physics.SetAngularDamping(meteor, physics, 0f);
var velocity = -offset.Normalized() * component.MeteorVelocity;
_physics.ApplyLinearImpulse(meteor, velocity * physics.Mass, body: physics);
_physics.SetAngularVelocity(meteor, (component.MaxAngularVelocity - component.MinAngularVelocity) * RobustRandom.NextFloat() + component.MinAngularVelocity, body: physics);
EnsureComp<TimedDespawnComponent>(meteor).Lifetime = component.MeteorLifetime;
var timeUntilClosestApproachToTarget = TimeUntilClosestApproach(spawnPosition, velocity, target);
if (i == 0 || timeUntilClosestApproachToTarget > maxImpactTime)
var relativeSpawnPosition = new Vector2(target.X - spawnPosition.X, target.Y - spawnPosition.Y);
var timeUntilClosestApproach = (
(relativeSpawnPosition.X * velocity.X + relativeSpawnPosition.Y * velocity.Y) /
(MathF.Pow(velocity.X, 2f) + MathF.Pow(velocity.Y, 2f))
);
if (i == 0 || timeUntilClosestApproach > maxImpactTime)
{
maxImpactTime = timeUntilClosestApproachToTarget;
maxImpactTime = timeUntilClosestApproach;
}
}
@@ -182,14 +132,5 @@ namespace Content.Server.StationEvents.Events
component.Cooldown += (component.MaximumCooldown - component.MinimumCooldown) * RobustRandom.NextFloat() + component.MinimumCooldown;
}
}
private static float TimeUntilClosestApproach(MapCoordinates startPoint, Vector2 velocity, MapCoordinates approachPoint)
{
var approachPointRelativeToStartPoint = new Vector2(approachPoint.X - startPoint.X, approachPoint.Y - startPoint.Y);
return (
(approachPointRelativeToStartPoint.X * velocity.X + approachPointRelativeToStartPoint.Y * velocity.Y) /
(MathF.Pow(velocity.X, 2f) + MathF.Pow(velocity.Y, 2f))
);
}
}
}
@@ -1,107 +0,0 @@
using Content.Server.Abilities.Psionics;
using Content.Server.Chat.Systems;
using Content.Server._DV.StationEvents.Components;
using Content.Server.Psionics;
using Content.Server.StationEvents.Events;
using Content.Shared.Abilities.Psionics;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server._DV.StationEvents.Events;
/// <summary>
/// Forces a mind swap on a small amount of non-insulated psionic entities.
/// </summary>
internal sealed class MinorMassMindSwapRule : StationEventSystem<MinorMassMindSwapRuleComponent>
{
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly MindSwapPowerSystem _mindSwap = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MobStateSystem _mobstateSystem = default!;
private TimeSpan _warningSoundLength;
private ResolvedSoundSpecifier _resolvedWarningSound = string.Empty;
protected override void Started(EntityUid uid, MinorMassMindSwapRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
_resolvedWarningSound = _audio.ResolveSound(component.SwapWarningSound);
_warningSoundLength = _audio.GetAudioLength(_resolvedWarningSound);
component.SwapTime = Timing.CurTime + component.Delay;
component.SoundTime = component.SwapTime - _warningSoundLength;
component.MaxNumberOfPairs = component.MaxNumberOfPairs < 1 ? 1 : component.MaxNumberOfPairs;
var announcement = Loc.GetString("minor-mass-mind-swap-event-announcement", ("time", component.Delay.TotalSeconds));
var sender = Loc.GetString("minor-mass-mind-swap-event-sender");
_chat.DispatchGlobalAnnouncement(announcement, sender, true, component.AnnouncementSound, Color.White);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<MinorMassMindSwapRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var comp, out var ruleComp))
{
if (comp.SwapTime == null)
continue;
if (comp.SoundTime != null && comp.SoundTime <= Timing.CurTime)
{
_audio.PlayGlobal(_resolvedWarningSound, Filter.Broadcast(), true);
comp.SoundTime = null;
continue;
}
if (comp.SwapTime > Timing.CurTime)
continue;
SwapMinds(comp);
comp.SwapTime = null;
GameTicker.EndGameRule(uid, ruleComp);
}
}
private void SwapMinds(MinorMassMindSwapRuleComponent component)
{
List<EntityUid> psionicActors = new();
var query = EntityQueryEnumerator<PotentialPsionicComponent, MobStateComponent>();
while (query.MoveNext(out var psion, out _, out _))
{
if (_mobstateSystem.IsAlive(psion) && !HasComp<PsionicInsulationComponent>(psion)
&& HasComp<ActorComponent>(psion))
// Only a list of Players
psionicActors.Add(psion);
}
// We go with 4 pairs for now
List<EntityUid> actorsToSwap = new();
var swapPairCount = _random.Next(1, component.MaxNumberOfPairs);
for (; swapPairCount > 0 && psionicActors.Count > 1; swapPairCount--)
{
var target01 = _random.PickAndTake(psionicActors);
var target02 = _random.PickAndTake(psionicActors);
_mindSwap.Swap(target01, target02);
if (!component.IsTemporary)
{
_mindSwap.GetTrapped(target01);
_mindSwap.GetTrapped(target02);
}
}
}
}
@@ -1,5 +0,0 @@
using Content.Shared._EE.Flight;
namespace Content.Server._EE.Flight;
public sealed class FlightSystem : SharedFlightSystem;
@@ -1,9 +1,9 @@
using Content.Server.Atmos.Components;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared._EE.FootPrint;
// using Content.Shared.Standing;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -21,12 +21,10 @@ public sealed class FootPrintsSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
private EntityQuery<TransformComponent> _transformQuery;
private EntityQuery<MobThresholdsComponent> _mobThresholdQuery;
private EntityQuery<AppearanceComponent> _appearanceQuery;
// private EntityQuery<LayingDownComponent> _layingQuery;
public override void Initialize()
@@ -49,9 +47,6 @@ public sealed class FootPrintsSystem : EntitySystem
private void OnMove(EntityUid uid, FootPrintsComponent component, ref MoveEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (component.PrintsColor.A <= 0f
|| !_transformQuery.TryComp(uid, out var transform)
|| !_mobThresholdQuery.TryComp(uid, out var mobThreshHolds)
@@ -1,5 +1,4 @@
using System.Linq;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -13,7 +12,6 @@ public sealed class PuddleFootPrintsSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
public override void Initialize()
{
@@ -23,9 +21,6 @@ public sealed class PuddleFootPrintsSystem : EntitySystem
private void OnStepTrigger(EntityUid uid, PuddleFootPrintsComponent component, ref EndCollideEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (!TryComp<AppearanceComponent>(uid, out var appearance)
|| !TryComp<PuddleComponent>(uid, out var puddle)
|| !TryComp<FootPrintsComponent>(args.OtherEntity, out var tripper)
@@ -69,14 +69,12 @@ public sealed class IdExaminableSystem : EntitySystem
{
var jobSuffix = string.IsNullOrWhiteSpace(id.LocalizedJobTitle) ? string.Empty : $" ({id.LocalizedJobTitle})";
var val = FormattedMessage.EscapeText( // DeltaV - Escape job
string.IsNullOrWhiteSpace(id.FullName)
var val = string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString(id.NameLocId,
("jobSuffix", jobSuffix))
: Loc.GetString(id.FullNameLocId,
("fullName", id.FullName),
("jobSuffix", jobSuffix))
);
("jobSuffix", jobSuffix));
return val;
}
@@ -425,11 +425,6 @@ public abstract class SharedAnomalySystem : EntitySystem
if (!settings.CanSpawnOnEntities)
{
// DeltaV - start of duplicate spawn fix (borrowed from upstream #37833)
// If it can't spawn on entities, ensure that maximum one entity will be spawned here this pulse.
tilerefs.Remove(tileref);
// DeltaV - end of duplicate spawn fix (borrowed from upstream #37833)
var valid = true;
foreach (var ent in _map.GetAnchoredEntities(xform.GridUid.Value, grid, tileref.GridIndices))
{
@@ -446,7 +441,7 @@ public abstract class SharedAnomalySystem : EntitySystem
}
if (!valid)
{
// DeltaV - duplicate spawn fix removed: tilerefs.Remove(tileref);
tilerefs.Remove(tileref);
continue;
}
}
@@ -80,9 +80,7 @@ public abstract class SharedTypingIndicatorSystem : EntitySystem
return;
}
if(ev.State != TypingIndicatorState.Idle) // DeltaV - don't remove override when transitioning to idle
SetTypingOverride(uid.Value, ev.OverrideIndicator); // DeltaV
SetTypingOverride(uid.Value, ev.OverrideIndicator); // DeltaV
SetTypingIndicatorState(uid.Value, ev.State);
}
@@ -65,18 +65,6 @@ public sealed partial class ChameleonClothingComponent : Component
/// </summary>
[AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextEmpChange = TimeSpan.Zero;
/// <summary>
/// Delta-V: Override for the name of the chameleon stuff, so you can have items use the chameleon system without being illegal.
/// </summary>
[DataField]
public LocId? WindowTitleOverride;
/// <summary>
/// Delta-V: Override for the name of the chameleon stuff, so you can have items use the chameleon system without being illegal.
/// </summary>
[DataField]
public LocId? VerbNameOverride;
}
[Serializable, NetSerializable]
@@ -132,7 +132,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
args.Verbs.Add(new InteractionVerb()
{
Text = Loc.GetString(ent.Comp.VerbNameOverride ?? "chameleon-component-verb-text"), // Delta-V: Override for the name of the Chameleon Verb
Text = Loc.GetString("chameleon-component-verb-text"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
Act = () => UI.TryToggleUi(ent.Owner, ChameleonUiKey.Key, user)
});
@@ -1,5 +1,4 @@
using System.Linq;
using Content.Shared._EE.Flight; // DeltaV - Harpy Flight
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Components;
using Content.Shared.Administration.Logs;
@@ -56,7 +55,6 @@ namespace Content.Shared.Cuffs
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - Harpy flight
public override void Initialize()
{
@@ -516,15 +514,6 @@ namespace Content.Shared.Cuffs
return false;
}
// EE - Harpy Flight
if (_flight.IsFlying(target))
{
_popup.PopupClient(Loc.GetString("handcuff-component-target-flying-error",
("targetName", Identity.Name(target, EntityManager, user))), user, user);
return false;
}
// END EE
var cuffTime = handcuffComponent.CuffTime;
if (HasComp<StunnedComponent>(target))
@@ -45,7 +45,6 @@ public abstract partial class SharedStaminaSystem : EntitySystem
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly StatusEffectsSystem _status = default!;
[Dependency] protected readonly SharedStunSystem StunSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movement = default!; // EE - Harpy Flight
/// <summary>
/// How much of a buffer is there between the stun duration and when stuns can be re-applied.
@@ -266,8 +265,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
}
public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null,
EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool ignoreResist = false,
bool? allowsSlowdown = true) // EE - Harpy Flight
EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool ignoreResist = false)
{
if (!Resolve(uid, ref component, false))
return;
@@ -285,9 +283,6 @@ public abstract partial class SharedStaminaSystem : EntitySystem
value = UniversalStaminaDamageModifier * value;
if (allowsSlowdown == true) // EE - Harpy Flight
_movement.RefreshMovementSpeedModifiers(uid);
// Have we already reached the point of max stamina damage?
if (component.Critical)
return;
@@ -367,23 +362,12 @@ public abstract partial class SharedStaminaSystem : EntitySystem
{
// Just in case we have active but not stamina we'll check and account for it.
if (!stamQuery.TryGetComponent(uid, out var comp) ||
comp.StaminaDamage <= 0f && !comp.Critical && comp.ActiveDrains.Count == 0) // EE - Harpy Flight
comp.StaminaDamage <= 0f && !comp.Critical)
{
RemComp<ActiveStaminaComponent>(uid);
continue;
}
// EE - Harpy Flight
if (comp.ActiveDrains.Count > 0)
foreach (var (source, (drainRate, modifiesSpeed)) in comp.ActiveDrains)
TakeStaminaDamage(uid,
drainRate * frameTime,
comp,
source: source,
visual: false,
allowsSlowdown: modifiesSpeed);
// End EE
// Shouldn't need to consider paused time as we're only iterating non-paused stamina components.
var nextUpdate = comp.NextUpdate;
@@ -396,11 +380,10 @@ public abstract partial class SharedStaminaSystem : EntitySystem
comp.NextUpdate += TimeSpan.FromSeconds(1f);
if (comp.ActiveDrains.Count == 0) // EE - Harpy Flight
TakeStaminaDamage(
uid,
comp.AfterCritical ? -comp.Decay * comp.AfterCritDecayMultiplier : -comp.Decay, // Recover faster after crit
comp);
TakeStaminaDamage(
uid,
comp.AfterCritical ? -comp.Decay * comp.AfterCritDecayMultiplier : -comp.Decay, // Recover faster after crit
comp);
Dirty(uid, comp);
}
@@ -1,13 +1,12 @@
using System.Numerics;
using Robust.Shared.Map;
using Content.Shared._EE.Flight.Events;
namespace Content.Shared.Gravity;
/// <summary>
/// Handles offsetting a sprite when there is no gravity
/// </summary>
public abstract partial class SharedFloatingVisualizerSystem : EntitySystem // DeltaV - Made Partial for Harpy Flying
public abstract class SharedFloatingVisualizerSystem : EntitySystem
{
[Dependency] private readonly SharedGravitySystem _gravity = default!;
@@ -17,8 +16,6 @@ public abstract partial class SharedFloatingVisualizerSystem : EntitySystem // D
SubscribeLocalEvent<FloatingVisualsComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<FloatingVisualsComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
SubscribeNetworkEvent<FlightEvent>(OnFlight);
}
/// <summary>
@@ -1,4 +1,3 @@
using Content.Shared._EE.Flight; // DeltaV - Harpy Flight
using Content.Shared.Alert;
using Content.Shared.Inventory;
using Content.Shared.Throwing;
@@ -17,7 +16,6 @@ public abstract partial class SharedGravitySystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - Harpy Flight
public static readonly ProtoId<AlertPrototype> WeightlessAlert = "Weightless";
@@ -71,9 +69,6 @@ public abstract partial class SharedGravitySystem : EntitySystem
if (entity.Comp2.BodyType is BodyType.Static or BodyType.Kinematic)
return false;
if (_flight.IsFlying(entity.Owner)) // DeltaV - Harpy Flight
return true;
// Check if something other than the grid or map is overriding our gravity
var ev = new IsWeightlessEvent();
RaiseLocalEvent(entity, ref ev);
@@ -11,7 +11,6 @@ public abstract partial class SharedHandsSystem
private void InitializeEventListeners()
{
SubscribeLocalEvent<HandsComponent, GetStandUpTimeEvent>(OnStandupArgs);
SubscribeLocalEvent<HandsComponent, KnockedDownRefreshEvent>(OnKnockedDownRefresh);
}
/// <summary>
@@ -29,27 +28,4 @@ public abstract partial class SharedHandsSystem
time.DoAfterTime *= (float)ent.Comp.Count / (hands + ent.Comp.Count);
}
private void OnKnockedDownRefresh(Entity<HandsComponent> ent, ref KnockedDownRefreshEvent args)
{
var freeHands = CountFreeHands(ent.AsNullable());
var totalHands = GetHandCount(ent.AsNullable());
// Can't crawl around without any hands.
// Entities without the HandsComponent will always have full crawling speed.
if (totalHands == 0)
args.SpeedModifier = 0f;
// DeltaV - 1 hand free = 75% movespeed, no hands free = 50% movespeed
else if (totalHands > freeHands)
{
// For some reason, this feels a bit dirty
// 0 free hands out of 1 total hands = (1-(1-0)*.50) = 50% additional reduction to current move speed mod
// 1 free hand out of 2 total hands = (1-(2-1)*.25) = 75%
// 1 free hand out of 4 total hands = (1-(4-1)*.125) = 62.5%
// 6 free hands out of 10 total hands hands = (1-(10-6)*0.05) = 80%
var reductionPerHand = 1f / (totalHands + totalHands);
args.SpeedModifier *= (float)Math.Clamp(1f - (totalHands - freeHands) * reductionPerHand, 0.5, 1.0);
}
// END DeltaV
}
}
@@ -108,13 +108,13 @@ public sealed partial class SpeciesPrototype : IPrototype
/// Characters younger than this appear young.
/// </summary>
[DataField]
public int YoungAge = 40; // DeltaV - up from 30
public int YoungAge = 30;
/// <summary>
/// Characters older than this appear old. Characters in between young and old age appear middle aged.
/// </summary>
[DataField]
public int OldAge = 70; // DeltaV - up from 60
public int OldAge = 60;
/// <summary>
/// Characters cannot be older than this. Only used for restrictions...
@@ -60,7 +60,7 @@ public sealed partial class LabelSystem : EntitySystem
return;
label ??= EnsureComp<LabelComponent>(uid);
label.CurrentLabel = text == null ? null : FormattedMessage.EscapeText(text);
label.CurrentLabel = text;
_nameModifier.RefreshNameModifiers(uid);
Dirty(uid, label);
@@ -15,7 +15,7 @@ namespace Content.Shared.Movement.Components
// weightless
public const float DefaultWeightlessFriction = 1f;
public const float DefaultWeightlessModifier = 0.70f;
public const float DefaultWeightlessModifier = 0.7f;
public const float DefaultWeightlessAcceleration = 1f;
// friction
@@ -4,13 +4,13 @@ namespace Content.Shared.Radiation.Events;
/// Raised on entity when it was irradiated
/// by some radiation source.
/// </summary>
public readonly record struct OnIrradiatedEvent(float FrameTime, float RadsPerSecond, EntityUid? Origin)
public readonly record struct OnIrradiatedEvent(float FrameTime, float RadsPerSecond, EntityUid Origin)
{
public readonly float FrameTime = FrameTime;
public readonly float RadsPerSecond = RadsPerSecond;
public readonly EntityUid? Origin = Origin;
public readonly EntityUid Origin = Origin;
public float TotalRads => RadsPerSecond * FrameTime;
}
@@ -6,7 +6,6 @@ using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Gravity;
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Movement.Events;
@@ -54,7 +53,7 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<KnockedDownComponent, BuckleAttemptEvent>(OnBuckleAttempt);
SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
// Updating movement and friction
// Updating movement a friction
SubscribeLocalEvent<KnockedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshKnockedSpeed);
SubscribeLocalEvent<KnockedDownComponent, RefreshFrictionModifiersEvent>(OnRefreshFriction);
SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
@@ -66,9 +65,6 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<CrawlerComponent, KnockedDownRefreshEvent>(OnKnockdownRefresh);
SubscribeLocalEvent<CrawlerComponent, DamageChangedEvent>(OnDamaged);
SubscribeLocalEvent<KnockedDownComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
SubscribeLocalEvent<KnockedDownComponent, DidEquipHandEvent>(OnHandEquipped);
SubscribeLocalEvent<KnockedDownComponent, DidUnequipHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<KnockedDownComponent, HandCountChangedEvent>(OnHandCountChanged);
SubscribeLocalEvent<GravityAffectedComponent, KnockDownAttemptEvent>(OnKnockdownAttempt);
SubscribeLocalEvent<GravityAffectedComponent, GetStandUpTimeEvent>(OnGetStandUpTime);
@@ -235,14 +231,7 @@ public abstract partial class SharedStunSystem
if (playerSession.AttachedEntity is not { Valid: true } playerEnt || !Exists(playerEnt))
return;
// DeltaV - Double-tap Standup bind forces standup (unless hands full)
if (_standingState.IsDown(playerEnt)
&& TryComp<KnockedDownComponent>(playerEnt, out var knockedDown)
&& knockedDown.DoAfterId != null)
ForceStandUp(playerEnt);
else
ToggleKnockdown(playerEnt);
// END DeltaV
ToggleKnockdown(playerEnt);
}
/// <summary>
@@ -376,7 +365,7 @@ public abstract partial class SharedStunSystem
private void OnForceStandup(ForceStandUpEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not { } user)
if (args.SenderSession.AttachedEntity is not {} user)
return;
ForceStandUp(user);
@@ -518,30 +507,6 @@ public abstract partial class SharedStunSystem
RemCompDeferred<KnockedDownComponent>(entity);
}
private void OnHandEquipped(Entity<KnockedDownComponent> entity, ref DidEquipHandEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnHandUnequipped(Entity<KnockedDownComponent> entity, ref DidUnequipHandEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnHandCountChanged(Entity<KnockedDownComponent> entity, ref HandCountChangedEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnKnockdownAttempt(Entity<GravityAffectedComponent> entity, ref KnockDownAttemptEvent args)
{
// Directed, targeted moth attack.
@@ -602,7 +567,6 @@ public abstract partial class SharedStunSystem
ent.Comp.SpeedModifier = ev.SpeedModifier;
ent.Comp.FrictionModifier = ev.FrictionModifier;
Dirty(ent);
_movementSpeedModifier.RefreshMovementSpeedModifiers(ent);
_movementSpeedModifier.RefreshFrictionModifiers(ent);
@@ -91,31 +91,6 @@ public sealed partial class XenoArtifactComponent : Component
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextUnlockTime;
/// <summary>
/// DeltaV - The end time of the last-completed XenoArtifactUnlockingComponent phase.
/// If null, no unlocking phase has occurred yet, and the other LastUnlocking... fields are also invalid.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? LastUnlockingEndTime = null;
/// <summary>
/// DeltaV - The set of related nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<int> LastUnlockingTriggeredNodeIndexesRelated = new();
/// <summary>
/// DeltaV - The list of nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
/// </summary>
[DataField, AutoNetworkedField]
public List<int> LastUnlockingTriggeredNodeIndexesOrdered = new();
/// <summary>
/// DeltaV - Whether the last completed unlocking phase resulted in a newly-unlocked node.
/// </summary>
[DataField, AutoNetworkedField]
public bool LastUnlockingSuccessful = false;
#endregion
// NOTE: you should not be accessing any of these values directly. Use the methods in SharedXenoArtifactSystem.Graph
@@ -199,18 +174,6 @@ public sealed partial class XenoArtifactComponent : Component
/// </summary>
[DataField]
public EntProtoId<InstantActionComponent> SelfActivateAction = "ActionArtifactActivate";
/// <summary>
/// DeltaV - Probability for each node that its effect will be hidden in the analysis console until it is unlocked.
/// </summary>
[DataField]
public float LockedEffectHiddenProbability = 0.1f;
/// <summary>
/// DeltaV - Probability for each node that its effect will give a generic hint, rather than a specific one (until it is unlocked).
/// </summary>
[DataField]
public float LockedEffectVagueProbability = 0.67f;
}
/// <summary>
@@ -28,30 +28,6 @@ public sealed partial class XenoArtifactNodeComponent : Component
[DataField, AutoNetworkedField]
public LocId? TriggerTip;
/// <summary>
/// DeltaV - Unique effect description.
/// </summary>
[DataField, AutoNetworkedField]
public LocId? EffectTipSpecific;
/// <summary>
/// DeltaV - Vague effect description that may be shared by other effects.
/// </summary>
[DataField, AutoNetworkedField]
public LocId? EffectTipVague;
/// <summary>
/// DeltaV - Whether the effect of the node is hidden in the analysis console until unlocked.
/// </summary>
[DataField, AutoNetworkedField]
public bool LockedEffectTipHidden = false;
/// <summary>
/// DeltaV - Whether the effect of the node is described only vaguely in the analysis console until unlocked.
/// </summary>
[DataField, AutoNetworkedField]
public bool LockedEffectTipVague = false;
/// <summary>
/// The entity whose graph this node is a part of.
/// </summary>
@@ -15,21 +15,6 @@ public sealed partial class XenoArtifactUnlockingComponent : Component
[DataField, AutoNetworkedField]
public HashSet<int> TriggeredNodeIndexes = new();
/// <summary>
/// DeltaV - Ordered list of triggered nodes.
/// </summary>
[DataField, AutoNetworkedField]
public List<int> TriggeredNodeIndexesOrdered = new();
/// <summary>
/// DeltaV - A subset of TriggeredNodeIndexes whose elements are "related" to eachother.
/// </summary>
/// <remarks>
/// See definition of relatedness in GetRelatedNodes() of SharedXenoArtifactSystem.DV.cs
/// </remarks
[DataField, AutoNetworkedField]
public HashSet<int> TriggeredNodeIndexesRelated = new();
/// <summary>
/// The time at which the unlocking state ends.
/// </summary>
@@ -25,12 +25,6 @@ public sealed partial class XenoArchTriggerPrototype : IPrototype
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// DeltaV - Triggers that CANNOT appear on the same artifact as this trigger.
/// </summary>
[DataField]
public HashSet<ProtoId<XenoArchTriggerPrototype>>? TriggerBlacklist;
/// <summary>
/// List of components that represent ways to trigger node.
/// </summary>
@@ -1,11 +1,9 @@
using System.Linq;
using Content.Shared._DV.Xenoarchaeology.Artifact.Components; // DeltaV
using Content.Shared.EntityTable;
using Content.Shared.NameIdentifier;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Artifact.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; // DeltaV
using Robust.Shared.Utility;
namespace Content.Shared.Xenoarchaeology.Artifact;
@@ -113,17 +111,6 @@ public abstract partial class SharedXenoArtifactSystem
var nodeComponent = nodeEnt.Value.Comp;
nodeComponent.Depth = depth;
nodeComponent.TriggerTip = trigger.Tip;
// DeltaV - start of hide locked node effects
nodeComponent.LockedEffectTipHidden = RobustRandom.Prob(ent.Comp.LockedEffectHiddenProbability);
nodeComponent.LockedEffectTipVague = RobustRandom.Prob(ent.Comp.LockedEffectVagueProbability);
if (TryComp<XAEDetailsComponent>(nodeEnt.Value.Owner, out var details))
{
nodeComponent.EffectTipSpecific = details.SpecificTip;
nodeComponent.EffectTipVague = details.VagueTip;
if (!details.AllowLockedEffectHiding)
nodeComponent.LockedEffectTipHidden = false;
}
// DeltaV - end of hide locked node effects
EntityManager.AddComponents(nodeEnt.Value, trigger.Components);
Dirty(nodeEnt.Value);
@@ -82,13 +82,11 @@ public abstract partial class SharedXenoArtifactSystem
// var activated = ActivateNode((ent, artifactComponent), node.Value, null, null, Transform(ent).Coordinates, false);
// if (activated)
soundEffect = unlockingComponent.UnlockActivationSuccessfulSound;
artifactComponent.LastUnlockingSuccessful = true; // DeltaV - node scanner overhaul
}
else
{
unlockAttemptResultMsg = "artifact-unlock-state-end-failure";
soundEffect = unlockingComponent.UnlockActivationFailedSound;
artifactComponent.LastUnlockingSuccessful = false; // DeltaV - node scanner overhaul
}
if (_net.IsServer)
@@ -97,21 +95,6 @@ public abstract partial class SharedXenoArtifactSystem
_audio.PlayPvs(soundEffect, ent.Owner);
}
// DeltaV - start of node scanner overhaul
artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered.Clear();
artifactComponent.LastUnlockingTriggeredNodeIndexesRelated.Clear();
artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered
.AddRange(unlockingComponent.TriggeredNodeIndexesOrdered);
// one day we'll get a HashSet.AddRange
foreach (var i in unlockingComponent.TriggeredNodeIndexesRelated)
artifactComponent.LastUnlockingTriggeredNodeIndexesRelated.Add(i);
artifactComponent.LastUnlockingEndTime = unlockingComponent.EndTime;
Dirty(ent, artifactComponent);
// DeltaV - end of node scanner overhaul
RemComp(ent, unlockingComponent);
RaiseUnlockingFinished(ent, node);
artifactComponent.NextUnlockTime = _timing.CurTime + artifactComponent.UnlockStateRefractory;
@@ -63,15 +63,6 @@ public abstract partial class SharedXenoArtifactSystem
if (!force && _timing.CurTime < ent.Comp.NextUnlockTime)
return;
// DeltaV - start of node scanner overhaul
(Entity<XenoArtifactNodeComponent> node, int index)? parsedNode =
(node == null)
? null
: (node.Value, GetIndex(ent, node.Value));
bool partOfRelatedTriggersSet = true;
// DeltaV - end of node scanner overhaul
if (!_unlockingQuery.TryGetComponent(ent, out var unlockingComp))
{
unlockingComp = EnsureComp<XenoArtifactUnlockingComponent>(ent);
@@ -82,37 +73,24 @@ public abstract partial class SharedXenoArtifactSystem
_popup.PopupEntity(Loc.GetString("artifact-unlock-state-begin"), ent);
Dirty(ent);
}
else if (parsedNode != null)
else if (node != null)
{
// DeltaV - start of node scanner overhaul
var index = GetIndex(ent, node.Value);
var relatedNodeIndices = GetRelatedNodes((ent, ent), parsedNode.Value.index);
partOfRelatedTriggersSet = unlockingComp.TriggeredNodeIndexesRelated.All(x => relatedNodeIndices.Contains(x));
// Checking for trigger "relatedness" is a much more accurate measurement
// of "is this locking phase going to fail" than upstream's predecessor/successor check.
// Upstream's version of this check had edge-cases where time would not add, even though the
// unlocking phase ends up succeeding.
// See definition of GetRelatedNodes() for details on the concept of "relatedness".
if (
unlockingComp.TriggeredNodeIndexes.Count == unlockingComp.TriggeredNodeIndexesRelated.Count
&& partOfRelatedTriggersSet
)
var predecessorNodeIndices = GetPredecessorNodes((ent, ent), index);
var successorNodeIndices = GetSuccessorNodes((ent, ent), index);
if (unlockingComp.TriggeredNodeIndexes.Count == 0
|| unlockingComp.TriggeredNodeIndexes.All(
x => predecessorNodeIndices.Contains(x) || successorNodeIndices.Contains(x)
)
)
// we add time on each new trigger, if it is not going to fail us
unlockingComp.EndTime += ent.Comp.UnlockStateIncrementPerNode;
// DeltaV - end of node scanner overhaul
}
if (parsedNode != null && unlockingComp.TriggeredNodeIndexes.Add(parsedNode.Value.index))
if (node != null && unlockingComp.TriggeredNodeIndexes.Add(GetIndex(ent, node.Value)))
{
// DeltaV - start of changes
// node scanner overhaul:
unlockingComp.TriggeredNodeIndexesOrdered.Add(parsedNode.Value.index);
if (partOfRelatedTriggersSet)
unlockingComp.TriggeredNodeIndexesRelated.Add(parsedNode.Value.index);
// faster unlock effect:
// DeltaV - start of faster unlock effect
if (
ent.Comp.UnlockCompleteDuration is {} completeDuration
&& TryGetNodeFromUnlockState((ent.Owner, unlockingComp, ent.Comp), out var unlockingNode)
@@ -120,7 +98,7 @@ public abstract partial class SharedXenoArtifactSystem
{
unlockingComp.EndTime = _timing.CurTime + completeDuration;
}
// DeltaV - end of changes
// DeltaV - end of faster unlock effect
Dirty(ent, unlockingComp);
}
@@ -16,16 +16,6 @@ public sealed partial class ArtifactAnalyzerComponent : Component
[DataField]
public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(30);
/// <summary>
/// DeltaV - The ratio of research points per one glimmer.
/// </summary>
public int ExtractRatio = 750;
/// <summary>
/// DeltaV - The maximum added multiplier, reached at max glimmer.
/// </summary>
[DataField]
public float PointGlimmerMultiplier = 8f;
/// <summary>
/// The current artifact placed on this analyzer.
/// Can be null if none are present.
@@ -1,34 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Shared._DEN.TimedDespawn;
/// <summary>
/// This is used for a more detailed timed despawn component.
/// </summary>
[RegisterComponent]
public sealed partial class TimedDespawnDetailedComponent : Component
{
[DataField]
public TimeSpan StartTime { get; set; } = TimeSpan.Zero;
/// <summary>
/// How long the entity will exist, in seconds, before despawning.
/// </summary>
[DataField]
public float Lifetime = 5f;
[DataField("examineText")]
public LocId? ExamineLocId { get; set; } = "timed-despawn-holoprojection-examine";
[DataField]
public SoundSpecifier? StartSound { get; set; } = new SoundPathSpecifier("/Audio/_DEN/Effects/holofan_sound_looped.ogg");
[DataField]
public AudioParams StartSoundParams { get; set; } = AudioParams.Default;
[DataField]
public SoundSpecifier? EndSound { get; set; }
[DataField]
public AudioParams EndSoundParams { get; set; } = AudioParams.Default;
}
@@ -3,11 +3,4 @@ using Robust.Shared.GameStates;
namespace Content.Shared._DV.Abilities.Psionics;
[RegisterComponent, NetworkedComponent]
public sealed partial class FracturedFormBodyComponent : Component
{
/// <summary>
/// The entity uid of the form that is currently being controlled.
/// </summary>
[DataField]
public EntityUid ControllingForm { get; set; } = default!;
}
public sealed partial class FracturedFormBodyComponent : Component;
@@ -1,14 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Access.Components;
/// <summary>
/// Component to denote the ID card as one specific to a borg.
/// Used to prevent agent IDs from copying access from borg IDs chips.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PreventAgentIdComponent : Component
{
[DataField]
public LocId? PopupText = "agent-id-card-cannot-copy";
}
@@ -1,5 +1,4 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.Roles;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.CartridgeLoader.Cartridges;
@@ -58,107 +57,45 @@ public enum NanoChatUiMessageType : byte
ToggleMute,
ToggleMuteChat,
ToggleListNumber,
CreateGroupChat,
InviteToGroup,
KickFromGroup,
ViewGroupMembers,
AdminUser,
DeadminUser,
}
/// <summary>
/// Represents an actual NanoChat Chat
/// Either a Group, if <paramref name="isGroup"/> is <see langword="true" />, else a normal user.
/// </summary>
/// <param name="number">The recipient's NanoChat number</param>
/// <param name="name">The recipient's display name</param>
/// <param name="jobTitle">Optional job title for the recipient</param>
/// <param name="department">Optional department ID for the recipient</param>
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
/// <param name="isGroup">Whether this is a group chat</param>
/// <param name="members">For group chats: list of member NanoChat numbers</param>
/// <param name="creatorId">For group chats: the creator's NanoChat number</param>
/// <param name="admins">For group chats: set of admin NanoChat numbers</param>
// putting this here because i can
[Serializable, NetSerializable, DataRecord]
public struct NanoChatRecipient(
uint number,
string name,
string? jobTitle,
string? department,
bool hasUnread,
bool isGroup,
HashSet<uint>? members,
uint? creatorId,
HashSet<uint>? admins
) : IComparable<NanoChatRecipient>
public struct NanoChatRecipient
{
/// <summary>
/// The recipient's unique NanoChat number.
/// </summary>
public uint Number = number;
public uint Number;
/// <summary>
/// The recipient's display name, typically from their ID card.
/// </summary>
public string Name = name;
public string Name;
/// <summary>
/// The recipient's job title, if available.
/// </summary>
public string? JobTitle = jobTitle;
/// <summary>
/// The recipient's department ID, if available.
/// </summary>
public string? Department = department;
public string? JobTitle;
/// <summary>
/// Whether this recipient has unread messages.
/// </summary>
public bool HasUnread = hasUnread;
public bool HasUnread;
/// <summary>
/// Whether this is a group chat.
/// Creates a new NanoChat recipient.
/// </summary>
public bool IsGroup = isGroup;
/// <summary>
/// For group chats: list of member NanoChat numbers.
/// </summary>
public HashSet<uint>? Members = members;
/// <summary>
/// For group chats: the NanoChat number of the creator.
/// </summary>
public uint? CreatorId = creatorId;
/// <summary>
/// For group chats: set of admin NanoChat numbers who can invite/kick.
/// </summary>
public HashSet<uint>? Admins = admins;
readonly int IComparable<NanoChatRecipient>.CompareTo(NanoChatRecipient other)
/// <param name="number">The recipient's NanoChat number</param>
/// <param name="name">The recipient's display name</param>
/// <param name="jobTitle">Optional job title for the recipient</param>
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
public NanoChatRecipient(uint number, string name, string? jobTitle = null, bool hasUnread = false)
{
// Groups should go before normal users
if (IsGroup && !other.IsGroup)
return -1;
else if (!IsGroup && other.IsGroup)
return 1;
// FIXME: Order by Department?
// Order by name
var nameCompare = string.CompareOrdinal(Name, other.Name);
if (nameCompare != 0)
return nameCompare;
// Smaller number goes first
if (Number > other.Number)
return 1;
else if (Number < other.Number)
return -1;
else
return 0;
Number = number;
Name = name;
JobTitle = jobTitle;
HasUnread = hasUnread;
}
}

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