Merge branch 'DeltaV-Station:master' into fixarchiviststuff

This commit is contained in:
AeraAulin 2025-10-02 16:55:14 -07:00 committed by GitHub
commit 43ce1febbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
533 changed files with 12366 additions and 6244 deletions

View File

@ -129,6 +129,137 @@ Should any person or party aboard a NanoTrasen vessel be found to be currently a
== Hostile Corporation Technology ==
Additionally, due to the presence of our competitors and their agents in the sectors in which our company operates, there is an ever-present possibility that hostile parties' technology or other property may manifest aboard our station by any number of means. The presence, acquisition, and distribution of such items should always be investigated and scrutinised heavily, and if Space Law and local legal ordinances allow, held securely, confiscated, and restricted from use.
== Potential Threats ==
While working aboard NanoTrasen stations, you may encounter the highly unlikely scenario of your life being threatened by hostile forces. Fortunately, our highly-trained security teams are well-equipped to deal with such threats. Please note, however, that wasting company resources and time for a pre-emptive defense against a non-existent threat are grounds for demotion. Below are the potential hazards you may face:
'''NANOTRASEN DOES NOT TAKE RESPONSIBILITY FOR CREW INJURY OR DEATH IF CREW MEMBERS DISREGARD THE INFORMATION BELOW.'''
=== Hostile Fauna ===
Be it from Epistemics experiments, corpses brought in by salvage, or something living in the vents, you may be attacked by non-sophont creatures. NanoTrasen does not encourage befriending such animals, unless its an approved experiment for research. Below are the procedures of how to deal with such a threat:
*Do not engage with the threat yourself.
*Evacuate the department or the zone.
*Immediately inform Security of the incident. Provide details such as:
**Threat type.
**Estimated numbers.
**Property damage.
**Any additional information.
*Inform Medical if there are injuries. If paramedics are unavailable, stabilize and carefully deliver the wounded to Medical.
*Provide ID access if Security personnel are not able to enter the area.
*Once Security confirms the threat being neutralized, clean up the department and report the incident to Command or Central Command.
=== Hostile Boarding Parties ===
Due to the stations invaluable resources and/or high profile crewmembers, NanoTrasen stations may attract hostile boarding parties. These individuals typically gain unauthorized access to the station and are usually armored, armed with deadly weapons. While their intentions are rarely known, common goals may include:
*Assassination of crewmembers.
*Theft of valuable station materials.
*Theft of sensitive research.
*Mass crew harm or terrorism.
*Sedition of the chain of Command.
*Detonation of the stations nuclear warhead.
If such an unlikely scenario occurs, the following procedures are listed below:
*Do not engage with the threat yourself.
*Seek immediate shelter.
*Immediately inform Security of the incident. Provide details such as:
**Hostiles appearance.
**What kind of armaments theyre equipped with.
**Estimated numbers.
**Last known location.
**Property damage.
**Any additional information.
*Inform Medical if there are injuries. If paramedics are unavailable, stabilize and treat the wounded. Only move the wounded if the path to Medical is safe. Do not take unnecessary risks.
*Remain in place until the situation has been resolved.
In the case of overwhelming force that Security cannot handle:
*Request assistance from Central Command through fax. In your fax, give as much detail of the threat as you can and the damages to the station and crew, alongside with signatures and stamps. Read “Contacting Central Command” for more information.
*Evacuate the station.
=== Unauthorized Cults ===
NanoTrasen recognizes and respects the free expression of religion aboard its stations. Discrimination of any kind will not be tolerated and is grounds for termination. However, Nanotrasen does not permit religious activity that involves:
*Fanatical recruitment of additional members.
*Harm to crew members, be it physical, psychological or metaphysical.
*Practices associated with EarthGov banned religions that are officially designated as harmful cults.
For classification, speak with the stations Mystagogue or Priest.
Please note that the Oracles religion is permitted aboard NanoTrasen stations, as they are the primary subject of research.
In the case of an unauthorized cult is suspected of forming on board, all crew members are encouraged to report unusual activity. Suspicious behaviour can be:
*Secrecy and Isolation. Suspicious individuals may engage only with selected individuals in secluded areas.
**Sudden changes in behaviour. Known members having a sudden change in their behavior or personality may indicate their affiliation.
**Otherworldly or physics defying effects occurring around certain crew members.
**Appearance of unidentified anomalies.
If a hostile cult is confirmed, follow these procedures:
*Listen to Epistemics, Security and Command for any relevant information and orders about the threat.
*Report suspicious individuals to Security and Epistemics. Give as much detail as possible, such as their appearance, potential tools, behaviour and more.
*Form a group of trusted co-workers and stick together. Do not isolate yourself.
*Await further instructions and remain in secure areas until the situation has been resolved by Security and Epistemics personnel.
=== Pathogens ===
While all NanoTrasen personnel are vaccinated against a wide range of known diseases, and while the station's atmospheric systems utilize only the highest-grade filtration technology, an outbreak may still occur on the station. Most such incidents are minor in scope - like the common cold. However, employees are reminded to remain vigilant and to treat any outbreak with the seriousness it deserves.
In the event of a confirmed outbreak, please follow these procedures:
*Follow instructions. Listen to Medical staff and comply with all orders and updates.
*Wear protective equipment, such as masks and gloves. Biosuits are recommended if available.
*Maintain hygiene. Wash hands frequently, and clean up any potential biohazards.
*Report Symptoms. Notify Medical and Security if any personnel display signs of infection.
*Stay in your department.
*Limit physical contact.
If youre starting to show symptoms of the pathogen, please immediately follow these procedures:
*Immediately head to the Medical Department.
*Inform Medical personnel of your symptoms.
*Listen to all orders from Medical personnel.
*Store any valuable NanoTrasen gear, weaponry or property in a secure location.
*If a treatment is currently unavailable, quarantine yourself in Medical.
*Await treatment.
=== Unions ===
NanoTrasen supports free speech, the right to fair working conditions and does not consider itself anti-union. However, The Company is not neutral either. NanoTrasen will always defend the interests of its customers, shareholders and associates. Worker unions may disrupt work productivity, which can in turn jeopardize job security for all crew members.
If an organizing union attempts to use force to advance its goals, such as vandalizing Company property or physically attacking other crewmembers, Security is authorized under Space Law to quell and suppress unions.
It is every crew member's duty to report potential warning signs of unionization to their managers. Warnings signs may be:
*Crew members refusing to work.
*Usage of union related terminology, (living wage, working conditions, etc.)
*Circulation or distribution of petitions and union fliers.
*Unusual interest in Company policies, benefits, employee list or any other Company information.
*Noticeable changes in behavior among known crew members.
*Non-manifest individuals supporting union activity.
*Clothing, accessories associated with unions.
*Increased negativity and complaints.
In the case of a violent union riot, follow these procedures:
*Do not join the riot.
*Inform Security and Command immediately. Provide details:
**Names and job positions of any suspected union members and leaders.
**Whether the union members are armed.
*Inform Central Command of the incident.
*Shelter in a safe place and await for the situation to be resolved.
== Contacting Central Command ==
Should you require a question answered about your duties, advice on how to properly handle situations, or wish to submit an incident report (including gross misconduct by Command staff), Central Command Officials are always happy to assist. Please ensure your fax is descriptive, legible and includes your signature or departmental stamp.
Refrain from sending junk mail to Central Command. Penalties may include pay cuts or a renegotiation of your contract.
In the highly unlikely event that station security is faced with an overwhelming hostile force or the chain of Command is compromised, crewmembers are advised to request assistance from Central Command.
Your fax should include:
*Stations designation (available in your PDA)
*The overwhelming threat. Try to provide as much detail as possible, such as:
**Estimated number of hostiles.
**If armed, what kind of armaments theyre equipped with.
**Number of casualties. Information about Securitys and Commands status is recommended.
**Stations structural integrity.
Due to the prevalence of misinformation reaching Central Command, please ensure to add as many stamps and signatures as possible/convenient.
Be warned that all faxes, urgent or otherwise, may take some time to be properly read and receive a response. Central Command is dedicated to managing multiple stations simultaneously. Rest assured: your fax will be addressed, even without your notice.
= Conclusion =
In closing, we thank you for your commitment to our mission and the values that guide NanoTrasen as a whole. Your dedication is the keystone to our success.

View File

@ -92,16 +92,8 @@ namespace Content.Client.Access.UI
}
}
private void ClearAllAccess()
{
foreach (var button in _accessButtons.ButtonsList.Values)
{
if (button.Pressed)
{
button.Pressed = false;
}
}
}
// DeltaV - removed as part of job preset access fix
// private void ClearAllAccess()
private void SelectJobPreset(OptionButton.ItemSelectedEventArgs args)
{
@ -113,34 +105,28 @@ namespace Content.Client.Access.UI
JobTitleLineEdit.Text = Loc.GetString(job.Name);
args.Button.SelectId(args.Id);
ClearAllAccess();
// this is a sussy way to do this
foreach (var access in job.Access)
{
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
{
button.Pressed = true;
}
}
// DeltaV - start of job preset access fix
SubmitData();
var targetAccesses = job.Access.ToHashSet();
foreach (var group in job.AccessGroups)
{
if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}
foreach (var access in groupPrototype.Tags)
{
if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
{
button.Pressed = true;
}
}
targetAccesses.UnionWith(groupPrototype.Tags);
}
SubmitData();
// this is a sussy way to do this
foreach (var (id, button) in _accessButtons.ButtonsList)
{
if (!button.Disabled && button.Pressed != targetAccesses.Contains(id))
{
OnToggleAccess?.Invoke(id);
}
}
// DeltaV - end of job preset access fix
}
public void UpdateState(IdCardConsoleBoundUserInterfaceState state)

View File

@ -426,7 +426,12 @@ namespace Content.Client.Actions
private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
{
if (args.Handled || args.Input.EntityUid is not { Valid: true } entity)
if (args.Handled)
return;
args.Handled = true;
if (args.Input.EntityUid is not { Valid: true } entity)
return;
// let world target component handle it
@ -437,8 +442,6 @@ namespace Content.Client.Actions
return;
}
args.Handled = true;
var action = args.Action;
var user = args.User;

View File

@ -90,10 +90,12 @@ public sealed partial class NanoTaskItemPopup : DefaultWindow
{
if (item is NanoTaskItem task)
{
var button = task.Priority switch {
var button = task.Priority switch
{
NanoTaskPriority.High => HighButton,
NanoTaskPriority.Medium => MediumButton,
NanoTaskPriority.Low => LowButton,
_ => throw new ArgumentException("Invalid priority"),
};
button.Pressed = true;
DescriptionInput.Text = task.Description;

View File

@ -38,10 +38,12 @@ public sealed partial class NanoTaskUiFragment : BoxContainer
foreach (var task in tasks)
{
var container = task.Data.Priority switch {
var container = task.Data.Priority switch
{
NanoTaskPriority.High => HighContainer,
NanoTaskPriority.Medium => MediumContainer,
NanoTaskPriority.Low => LowContainer,
_ => throw new ArgumentException("Invalid priority"),
};
var control = new NanoTaskItemControl(task);
container.AddChild(control);

View File

@ -39,6 +39,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Content.Client._NF.Emp.Overlays; // Frontier
using Content.Client._RMC14.Xenonids.Screech; // RMC14
namespace Content.Client.Entry
{
@ -173,6 +174,7 @@ namespace Content.Client.Entry
_overlayManager.AddOverlay(new SingularityOverlay());
_overlayManager.AddOverlay(new RadiationPulseOverlay());
_overlayManager.AddOverlay(new RMCXenoScreechShockWaveOverlay()); // RMC14
_overlayManager.AddOverlay(new EmpBlastOverlay()); // Frontier
_chatManager.Initialize();
_clientPreferencesManager.Initialize();

View File

@ -797,6 +797,9 @@ namespace Content.Client.Lobby.UI
PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed);
SpriteView.SetEntity(PreviewDummy);
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, Profile.Name);
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
SetDirty();
}
/// <summary>
@ -862,6 +865,9 @@ namespace Content.Client.Lobby.UI
return;
_entManager.System<HumanoidAppearanceSystem>().LoadProfile(PreviewDummy, Profile);
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
SetDirty();
}
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)

View File

@ -29,6 +29,14 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
private void OnGetEyeOffsetEvent(EntityUid uid, EyeCursorOffsetComponent component, ref GetEyeOffsetEvent args)
{
// Begin DeltaV - enable/disable
if (!component.Enabled)
{
args.Offset = Vector2.Zero;
return;
}
// End DeltaV - enable/disable
var offset = OffsetAfterMouse(uid, component);
if (offset == null)
return;

View File

@ -7,6 +7,8 @@ using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
using System.Linq;
using Content.Shared.Mobs; // The Den - Nuke Health icons.
namespace Content.Client.Overlays;
@ -77,6 +79,8 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
{
if (TryComp<MobStateComponent>(entity, out var state))
{
if (state.CurrentState != MobState.Alive) // Den: Nuke Alive Icon so they don't hide speech bubbles
return result;
// Since there is no MobState for a rotting mob, we have to deal with this case first.
if (HasComp<RottingComponent>(entity) && _prototypeMan.TryIndex(damageableComponent.RottingIcon, out var rottingIcon))
result.Add(rottingIcon);

View File

@ -209,7 +209,7 @@ public sealed class ActionButton : Control, IEntityControl
if (_entities.TryGetComponent(Action, out AutoRechargeComponent? autoRecharge))
{
var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((Action.Value, actionCharges, autoRecharge));
chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining.TotalSeconds:F1} seconds")); // DeltaV - Better formatted, easier to read.
}
}

View File

@ -0,0 +1,6 @@
using Content.Shared.Xenoarchaeology.Artifact;
namespace Content.Client.Xenoarchaeology.Artifact;
/// <inheritdoc/>
public sealed class XenoArtifactSystem : SharedXenoArtifactSystem;

View File

@ -0,0 +1,40 @@
using Content.Client.Xenoarchaeology.Ui;
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Xenoarchaeology.Equipment;
/// <inheritdoc />
public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnalysisConsoleComponent, AfterAutoHandleStateEvent>(OnAnalysisConsoleAfterAutoHandleState);
SubscribeLocalEvent<ArtifactAnalyzerComponent, AfterAutoHandleStateEvent>(OnAnalyzerAfterAutoHandleState);
}
private void OnAnalysisConsoleAfterAutoHandleState(Entity<AnalysisConsoleComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateBuiIfCanGetAnalysisConsoleUi(ent);
}
private void OnAnalyzerAfterAutoHandleState(Entity<ArtifactAnalyzerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!TryGetAnalysisConsole(ent, out var analysisConsole))
return;
UpdateBuiIfCanGetAnalysisConsoleUi(analysisConsole.Value);
}
private void UpdateBuiIfCanGetAnalysisConsoleUi(Entity<AnalysisConsoleComponent> analysisConsole)
{
if (_ui.TryGetOpenUi<AnalysisConsoleBoundUserInterface>(analysisConsole.Owner, ArtifactAnalyzerUiKey.Key, out var bui))
bui.Update(analysisConsole);
}
}

View File

@ -6,4 +6,4 @@ namespace Content.Client.Xenoarchaeology.Equipment;
public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
{
}
}

View File

@ -1,66 +1,50 @@
using Content.Shared.Xenoarchaeology.Equipment;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Content.Shared.Research.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.UserInterface;
using JetBrains.Annotations;
namespace Content.Client.Xenoarchaeology.Ui;
/// <summary>
/// BUI for artifact analysis console, proxies server-provided UI updates
/// (related to device, connected artifact analyzer, and artifact lying on it).
/// </summary>
[UsedImplicitly]
public sealed class AnalysisConsoleBoundUserInterface : BoundUserInterface
public sealed class AnalysisConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private AnalysisConsoleMenu? _consoleMenu;
public AnalysisConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
/// <inheritdoc />
protected override void Open()
{
base.Open();
_consoleMenu = this.CreateWindow<AnalysisConsoleMenu>();
_consoleMenu.SetOwner(owner);
_consoleMenu.OnClose += Close;
_consoleMenu.OpenCentered();
_consoleMenu.OnServerSelectionButtonPressed += () =>
{
SendMessage(new AnalysisConsoleServerSelectionMessage());
};
_consoleMenu.OnScanButtonPressed += () =>
{
SendMessage(new AnalysisConsoleScanButtonPressedMessage());
};
_consoleMenu.OnPrintButtonPressed += () =>
{
SendMessage(new AnalysisConsolePrintButtonPressedMessage());
SendMessage(new ConsoleServerSelectionMessage());
};
_consoleMenu.OnExtractButtonPressed += () =>
{
SendMessage(new AnalysisConsoleExtractButtonPressedMessage());
};
_consoleMenu.OnUpBiasButtonPressed += () =>
{
SendMessage(new AnalysisConsoleBiasButtonPressedMessage(false));
};
_consoleMenu.OnDownBiasButtonPressed += () =>
{
SendMessage(new AnalysisConsoleBiasButtonPressedMessage(true));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
/// <summary>
/// Update UI state based on corresponding component.
/// </summary>
public void Update(Entity<AnalysisConsoleComponent> ent)
{
base.UpdateState(state);
switch (state)
{
case AnalysisConsoleUpdateState msg:
_consoleMenu?.SetButtonsDisabled(msg);
_consoleMenu?.UpdateInformationDisplay(msg);
_consoleMenu?.UpdateProgressBar(msg);
break;
}
_consoleMenu?.Update(ent);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@ -1,80 +1,91 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client.Xenoarchaeology.Ui"
Title="{Loc 'analysis-console-menu-title'}"
MinSize="620 295"
SetSize="620 295"> <!-- DeltaV - increase vertical size for glimmer multiplier -->
MinSize="700 350"
SetSize="980 550">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Margin="10 10 10 10" MinWidth="150" Orientation="Vertical"
VerticalExpand="True" SizeFlagsStretchRatio="1">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<Button Name="ServerSelectionButton"
Text="{Loc 'analysis-console-server-list-button'}"></Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="ScanButton"
Text="{Loc 'analysis-console-scan-button'}"
ToolTip="{Loc 'analysis-console-scan-tooltip-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="PrintButton"
Text="{Loc 'analysis-console-print-button'}"
ToolTip="{Loc 'analysis-console-print-tooltip-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="UpBiasButton"
Text="{Loc 'analysis-console-bias-up'}"
ToolTip="{Loc 'analysis-console-bias-button-info-up'}"
HorizontalExpand="True"
StyleClasses="OpenRight">
</Button>
<Button Name="DownBiasButton"
Text="{Loc 'analysis-console-bias-down'}"
ToolTip="{Loc 'analysis-console-bias-button-info-down'}"
HorizontalExpand="True"
StyleClasses="OpenLeft">
</Button>
<BoxContainer Margin="10 10 10 10" MaxWidth="240" SetWidth="240" Orientation="Vertical" HorizontalExpand="False" VerticalExpand="True">
<PanelContainer Name="BackPanel" HorizontalAlignment="Center">
<PanelContainer.PanelOverride>
<gfx:StyleBoxTexture Modulate="#1B1B1E" PatchMarginBottom="10" PatchMarginLeft="10" PatchMarginRight="10" PatchMarginTop="10"/>
</PanelContainer.PanelOverride>
<BoxContainer HorizontalExpand="True" VerticalExpand="True" MinSize="128 128">
<SpriteView Name="ArtifactView" Scale="4 4" HorizontalAlignment="Center" VerticalAlignment="Center" HorizontalExpand="True" VerticalExpand="True"/>
</BoxContainer>
<BoxContainer MinHeight="15"></BoxContainer>
<Button Name="ExtractButton"
Text="{Loc 'analysis-console-extract-button'}"
ToolTip="{Loc 'analysis-console-extract-button-info'}">
</Button>
</PanelContainer>
<customControls:HSeparator StyleClasses="HighDivider" Margin="0 15 0 10"/>
<BoxContainer Name="ExtractContainer" Orientation="Vertical" VerticalExpand="True" Visible="False">
<PanelContainer HorizontalExpand="True" VerticalExpand="True" RectClipContent="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Margin="10 10 10 5" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<RichTextLabel Name="ExtractionResearchLabel" VerticalAlignment="Top" HorizontalAlignment="Left"/>
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<RichTextLabel Name="ExtractionSumLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
</BoxContainer>
</PanelContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical">
<Label Name="ProgressLabel"></Label>
<ProgressBar
Name="ProgressBar"
MinValue="0"
MaxValue="1"
SetHeight="20">
</ProgressBar>
<BoxContainer Name="NodeViewContainer" Orientation="Vertical" VerticalExpand="True">
<ScrollContainer HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="False" VerticalExpand="True">
<Label Name="NoneSelectedLabel" Text="{Loc 'analysis-console-no-node'}" HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True" Visible="False"/>
<BoxContainer Name="InfoContainer" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="IDLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-id'}"/>
<RichTextLabel Name="IDValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="ClassLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-class'}"/>
<RichTextLabel Name="ClassValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="LockedLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-locked'}"/>
<RichTextLabel Name="LockedValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="DurabilityLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-durability'}"/>
<RichTextLabel Name="DurabilityValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<Control MinHeight="20"/>
<RichTextLabel Name="EffectLabel" Text="{Loc 'analysis-console-info-effect'}"/>
<RichTextLabel Name="EffectValueLabel" HorizontalExpand="True"/>
<RichTextLabel Name="TriggerLabel" Text="{Loc 'analysis-console-info-trigger'}"/>
<RichTextLabel Name="TriggerValueLabel" HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="ServerButton" Text="{Loc 'analysis-console-server-list-button'}" StyleClasses="OpenRight" HorizontalExpand="True" MinHeight="35"/>
<Button Name="ExtractButton" Text="{Loc 'analysis-console-extract-button'}" StyleClasses="OpenLeft" HorizontalExpand="True" MinHeight="35"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<customControls:VSeparator StyleClasses="LowDivider" />
<PanelContainer Margin="10 10 10 10" HorizontalExpand="True" SizeFlagsStretchRatio="3">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Margin="10 10 10 10" Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer VerticalExpand="True">
<RichTextLabel Name="Information"> </RichTextLabel>
</BoxContainer>
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<PanelContainer Margin="10 10 10 10" HorizontalExpand="True" RectClipContent="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Margin="10 10 10 10" Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<ui:XenoArtifactGraphControl Name="GraphControl" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Label Name="NoArtiLabel"
Text="{Loc 'analysis-console-info-no-artifact'}"
HorizontalExpand="True"
VerticalExpand="True"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</ui:XenoArtifactGraphControl>
</BoxContainer>
<BoxContainer VerticalExpand="False" Orientation="Vertical" MaxSize="64 64">
<SpriteView
Name="ArtifactDisplay"
OverrideDirection="South"
VerticalExpand="False"
SetSize="64 64"
MaxSize="64 64"
Scale="2 2">
</SpriteView>
</BoxContainer>
<BoxContainer VerticalExpand="True"></BoxContainer>
</BoxContainer>
</PanelContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -1,11 +1,18 @@
using Content.Client.Stylesheets;
using System.Text;
using Content.Client.Message;
using Content.Client.Resources;
using Content.Client.UserInterface.Controls;
using Content.Shared.Xenoarchaeology.Equipment;
using Microsoft.VisualBasic;
using Content.Client.Xenoarchaeology.Artifact;
using Content.Client.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Audio;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@ -14,170 +21,213 @@ namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class AnalysisConsoleMenu : FancyWindow
{
private static readonly TimeSpan ExtractInfoDisplayForDuration = TimeSpan.FromSeconds(3);
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly ArtifactAnalyzerSystem _artifactAnalyzer;
private readonly XenoArtifactSystem _xenoArtifact;
private readonly AudioSystem _audio;
private readonly MetaDataSystem _meta = default!;
private Entity<AnalysisConsoleComponent> _owner;
private Entity<XenoArtifactNodeComponent>? _currentNode;
private TimeSpan? _hideExtractInfoIn;
private int _extractionSum;
public event Action? OnServerSelectionButtonPressed;
public event Action? OnScanButtonPressed;
public event Action? OnPrintButtonPressed;
public event Action? OnExtractButtonPressed;
public event Action? OnUpBiasButtonPressed;
public event Action? OnDownBiasButtonPressed;
// For rendering the progress bar, updated from BUI state
private TimeSpan? _startTime;
private TimeSpan? _totalTime;
private TimeSpan? _accumulatedRunTime;
private bool _paused;
public AnalysisConsoleMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ServerSelectionButton.OnPressed += _ => OnServerSelectionButtonPressed?.Invoke();
ScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
PrintButton.OnPressed += _ => OnPrintButtonPressed?.Invoke();
ExtractButton.OnPressed += _ => OnExtractButtonPressed?.Invoke();
UpBiasButton.OnPressed += _ => OnUpBiasButtonPressed?.Invoke();
DownBiasButton.OnPressed += _ => OnDownBiasButtonPressed?.Invoke();
_xenoArtifact = _ent.System<XenoArtifactSystem>();
_artifactAnalyzer = _ent.System<ArtifactAnalyzerSystem>();
_audio = _ent.System<AudioSystem>();
_meta = _ent.System<MetaDataSystem>();
var buttonGroup = new ButtonGroup(false);
UpBiasButton.Group = buttonGroup;
DownBiasButton.Group = buttonGroup;
if (BackPanel.PanelOverride is StyleBoxTexture tex)
tex.Texture = _resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
GraphControl.OnNodeSelected += node =>
{
_currentNode = node;
SetSelectedNode(node);
};
ServerButton.OnPressed += _ =>
{
OnServerSelectionButtonPressed?.Invoke();
};
ExtractButton.OnPressed += StartExtract;
}
/// <summary>
/// Set entity that corresponds analysis console, for which window is opened.
/// Closes window if <see cref="AnalysisConsoleComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid owner)
{
if (!_ent.TryGetComponent<AnalysisConsoleComponent>(owner, out var comp))
{
Close();
return;
}
_owner = (owner, comp);
Update(_owner);
}
private void StartExtract(BaseButton.ButtonEventArgs obj)
{
if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
return;
ExtractContainer.Visible = true;
NodeViewContainer.Visible = false;
_extractionSum = 0;
var extractionMessage = new FormattedMessage();
var nodes = _xenoArtifact.GetAllNodes(artifact.Value);
var count = 0;
foreach (var node in nodes)
{
var pointValue = _xenoArtifact.GetResearchValue(node);
if (pointValue <= 0)
continue;
count++;
var nodeId = _xenoArtifact.GetNodeId(node);
var text = Loc.GetString("analysis-console-extract-value", ("id", nodeId), ("value", pointValue));
extractionMessage.AddMarkupOrThrow(text);
extractionMessage.PushNewline();
}
if (count == 0)
extractionMessage.AddMarkupOrThrow(Loc.GetString("analysis-console-extract-none"));
_hideExtractInfoIn = _timing.CurTime + ExtractInfoDisplayForDuration;
ExtractionResearchLabel.SetMessage(extractionMessage);
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)
{
base.FrameUpdate(args);
if (_startTime is not { } start || _totalTime is not { } total || _accumulatedRunTime is not { } accumulated)
if (_hideExtractInfoIn == null || _timing.CurTime + _meta.GetPauseTime(_owner) < _hideExtractInfoIn)
return;
var remaining = total - accumulated;
if (!_paused)
{
// If the analyzer is running, its remaining time is further discounted by the time it's been running for.
remaining += start - _timing.CurTime;
}
var secsText = Math.Max((int) remaining.TotalSeconds, 0);
ProgressLabel.Text = Loc.GetString("analysis-console-progress-text",
("seconds", secsText));
// 1.0 - div because we want it to tick up not down
ProgressBar.Value = Math.Clamp(1.0f - (float) remaining.Divide(total), 0.0f, 1.0f);
ExtractContainer.Visible = false;
NodeViewContainer.Visible = true;
_hideExtractInfoIn = null;
}
public void SetButtonsDisabled(AnalysisConsoleUpdateState state)
public void Update(Entity<AnalysisConsoleComponent> ent)
{
ScanButton.Disabled = !state.CanScan;
PrintButton.Disabled = !state.CanPrint;
if (state.IsTraversalDown)
DownBiasButton.Pressed = true;
_artifactAnalyzer.TryGetArtifactFromConsole(ent, out var arti);
ArtifactView.SetEntity(arti);
GraphControl.SetArtifact(arti);
ExtractButton.Disabled = arti == null;
if (arti == null)
NoneSelectedLabel.Visible = false;
NoArtiLabel.Visible = true;
if (!_artifactAnalyzer.TryGetAnalyzer(ent, out _))
NoArtiLabel.Text = Loc.GetString("analysis-console-info-no-scanner");
else if (arti == null)
NoArtiLabel.Text = Loc.GetString("analysis-console-info-no-artifact");
else
UpBiasButton.Pressed = true;
NoArtiLabel.Visible = false;
ExtractButton.Disabled = false;
if (!state.ServerConnected)
if (_currentNode == null
|| arti == null
|| !_xenoArtifact.TryGetIndex((arti.Value, arti.Value), _currentNode.Value, out _))
{
ExtractButton.Disabled = true;
ExtractButton.ToolTip = Loc.GetString("analysis-console-no-server-connected");
SetSelectedNode(null);
}
else if (!state.CanScan)
{
ExtractButton.Disabled = true;
}
// CanScan can be false if either there's no analyzer connected or if there's
// no entity on the scanner. The `Information` text will always tell the user
// of the former case, but in the latter, it'll only show a message if a scan
// has never been performed, so add a tooltip to indicate that the artifact
// is gone.
if (state.AnalyzerConnected)
{
ExtractButton.ToolTip = Loc.GetString("analysis-console-no-artifact-placed");
}
else
{
ExtractButton.ToolTip = null;
}
}
else if (state.PointAmount <= 0)
{
ExtractButton.Disabled = true;
ExtractButton.ToolTip = Loc.GetString("analysis-console-no-points-to-extract");
}
public void SetSelectedNode(Entity<XenoArtifactNodeComponent>? node)
{
InfoContainer.Visible = node != null;
if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
return;
if (ExtractButton.Disabled)
NoneSelectedLabel.Visible = node == null;
if (node == null)
return;
var nodeId = _xenoArtifact.GetNodeId(node.Value);
IDValueLabel.SetMarkup(Loc.GetString("analysis-console-info-id-value", ("id", nodeId)));
// If active, state is 2. else, it is 0 or 1 based on whether it is unlocked, or not.
int lockedState;
if (_xenoArtifact.IsNodeActive(artifact.Value, node.Value))
lockedState = 2;
else
lockedState = node.Value.Comp.Locked ? 0 : 1;
LockedValueLabel.SetMarkup(Loc.GetString("analysis-console-info-locked-value", ("state", lockedState)));
var percent = (float) node.Value.Comp.Durability / node.Value.Comp.MaxDurability;
var color = percent switch
{
ExtractButton.RemoveStyleClass("ButtonColorGreen");
>= 0.75f => Color.Lime,
>= 0.50f => Color.Yellow,
_ => Color.Red
};
DurabilityValueLabel.SetMarkup(Loc.GetString("analysis-console-info-durability-value",
("color", color),
("current", node.Value.Comp.Durability),
("max", node.Value.Comp.MaxDurability)));
var hasInfo = _xenoArtifact.HasUnlockedPredecessor(artifact.Value, node.Value);
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)
{
TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value", ("state", false)));
}
else
{
ExtractButton.AddStyleClass("ButtonColorGreen");
ExtractButton.ToolTip = null;
}
}
private void UpdateArtifactIcon(EntityUid? uid)
{
if (uid == null)
{
ArtifactDisplay.Visible = false;
return;
}
var triggerStr = new StringBuilder();
triggerStr.Append("- ");
triggerStr.Append(Loc.GetString(node.Value.Comp.TriggerTip!));
ArtifactDisplay.Visible = true;
ArtifactDisplay.SetEntity(uid);
}
public void UpdateInformationDisplay(AnalysisConsoleUpdateState state)
{
var message = new FormattedMessage();
if (state.Scanning)
{
if (state.Paused)
foreach (var predecessor in predecessorNodes)
{
message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-scanner-paused"));
triggerStr.AppendLine();
triggerStr.Append("- ");
triggerStr.Append(Loc.GetString(predecessor.Comp.TriggerTip!));
}
else
{
message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-scanner"));
}
Information.SetMessage(message);
UpdateArtifactIcon(null); //set it to blank
return;
TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-triggered-value", ("triggers", triggerStr.ToString())));
}
UpdateArtifactIcon(_ent.GetEntity(state.Artifact));
if (state.ScanReport == null)
{
if (!state.AnalyzerConnected) //no analyzer connected
message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-no-scanner"));
else if (!state.CanScan) //no artifact
message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-no-artifact"));
else if (state.Artifact == null) //ready to go
message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-ready"));
}
else
{
message.AddMessage(state.ScanReport);
}
Information.SetMessage(message);
}
public void UpdateProgressBar(AnalysisConsoleUpdateState state)
{
ProgressBar.Visible = state.Scanning;
ProgressLabel.Visible = state.Scanning;
_startTime = state.StartTime;
_totalTime = state.TotalTime;
_accumulatedRunTime = state.AccumulatedRunTime;
_paused = state.Paused;
ClassValueLabel.SetMarkup(Loc.GetString("analysis-console-info-class-value",
("class", Loc.GetString($"artifact-node-class-{Math.Min(6, predecessorNodes.Count + 1)}"))));
}
}

View File

@ -0,0 +1,32 @@
using Robust.Client.UserInterface;
namespace Content.Client.Xenoarchaeology.Ui;
/// <summary>
/// BUI for hand-held xeno artifact scanner, server-provided UI updates.
/// </summary>
public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private NodeScannerDisplay? _scannerDisplay;
/// <inheritdoc />
protected override void Open()
{
base.Open();
_scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
_scannerDisplay.SetOwner(Owner);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_scannerDisplay?.Dispose();
}
}

View File

@ -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>

View File

@ -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"),
};
}
}

View File

@ -0,0 +1,6 @@
<controls:XenoArtifactGraphControl
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Xenoarchaeology.Ui"
HorizontalExpand="True"
VerticalExpand="True"
MouseFilter="Stop"/>

View File

@ -0,0 +1,208 @@
using System.Linq;
using System.Numerics;
using Content.Client.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class XenoArtifactGraphControl : BoxContainer
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly XenoArtifactSystem _artifactSystem;
private Entity<XenoArtifactComponent>? _artifact;
private Entity<XenoArtifactNodeComponent>? _hoveredNode;
private readonly Font _font;
public event Action<Entity<XenoArtifactNodeComponent>>? OnNodeSelected;
private float NodeRadius => 25 * UIScale;
private float NodeDiameter => NodeRadius * 2;
private float MinYSpacing => NodeDiameter * 0.75f;
private float MaxYSpacing => NodeDiameter * 1.5f;
private float MinXSpacing => NodeDiameter * 0.33f;
private float MaxXSpacing => NodeDiameter * 1f;
private float MinXSegmentSpacing => NodeDiameter * 0.5f;
private float MaxXSegmentSpacing => NodeDiameter * 3f;
public XenoArtifactGraphControl()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_artifactSystem = _entityManager.System<XenoArtifactSystem>();
var fontResource = IoCManager.Resolve<IResourceCache>()
.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf");
_font = new VectorFont(fontResource, 16);
}
public Color LockedNodeColor { get; set; } = Color.FromHex("#777777");
public Color ActiveNodeColor { get; set; } = Color.Plum;
public Color UnlockedNodeColor { get; set; } = Color.White;
public Color HoveredNodeColor { get; set; } = Color.DimGray;
public Color UnlockableNodeColor { get; set; } = Color.LightSlateGray;
public void SetArtifact(Entity<XenoArtifactComponent>? artifact)
{
_artifact = artifact;
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (args.Handled || args.Function != EngineKeyFunctions.UIClick)
return;
if (_hoveredNode == null)
return;
OnNodeSelected?.Invoke(_hoveredNode.Value);
UserInterfaceManager.ClickSound();
}
/// <summary>
/// Renders artifact node graph control, consisting of nodes and edges connecting them.
/// </summary>
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
_hoveredNode = null;
if (_artifact == null)
return;
var artifact = _artifact.Value;
var maxDepth = _artifactSystem.GetAllNodes(artifact)
.Max(s => s.Comp.Depth);
var segments = _artifactSystem.GetSegments(artifact);
var bottomLeft = Position // the position
+ new Vector2(0, Size.Y * UIScale) // the scaled height of the control
+ new Vector2(NodeRadius, -NodeRadius); // offset half a node so we don't render off screen
var controlHeight = bottomLeft.Y;
var controlWidth = Size.X * UIScale - NodeRadius;
// select y spacing based on max number of nodes we have on Y axis - that is max depth of artifact graph node
var ySpacing = 0f;
if (maxDepth != 0)
ySpacing = Math.Clamp((controlHeight - ((maxDepth + 1) * NodeDiameter)) / maxDepth, MinYSpacing, MaxYSpacing);
// gets settings for visualizing segments (groups of interconnected nodes - there may be 1 or more per artifact).
var segmentWidths = segments.Sum(GetBiggestWidth);
var segmentSpacing = Math.Clamp((controlWidth - segmentWidths) / (segments.Count - 1), MinXSegmentSpacing, MaxXSegmentSpacing);
var segmentOffset = Math.Max((controlWidth - (segmentWidths) - (segmentSpacing * (segments.Count - 1))) / 2, 0);
bottomLeft.X += segmentOffset;
bottomLeft.Y -= (controlHeight - (ySpacing * maxDepth) - (NodeDiameter * (maxDepth + 1))) / 2;
var cursor = (UserInterfaceManager.MousePositionScaled.Position * UIScale) - GlobalPixelPosition;
foreach (var segment in segments)
{
// For each segment we draw nodes in order of depth. Method returns List of nodes for each depth level.
var orderedNodes = _artifactSystem.GetDepthOrderedNodes(segment);
foreach (var (_, nodes) in orderedNodes)
{
for (var i = 0; i < nodes.Count; i++)
{
// selecting color for node based on its state
var node = nodes[i];
var color = LockedNodeColor;
if (_artifactSystem.IsNodeActive(artifact, node))
{
color = ActiveNodeColor;
}
else if (!node.Comp.Locked)
{
color = UnlockedNodeColor;
}
else
{
var directPredecessorNodes = _artifactSystem.GetDirectPredecessorNodes((artifact, artifact), node);
if (directPredecessorNodes.Count == 0 || directPredecessorNodes.All(x => !x.Comp.Locked))
{
color = UnlockableNodeColor;
}
}
var pos = GetNodePos(node, ySpacing, segments, ref bottomLeft);
var hovered = (cursor - pos).LengthSquared() <= NodeRadius * NodeRadius;
if (hovered)
{
// render hovered node if we have one
_hoveredNode = node;
handle.DrawCircle(pos, NodeRadius, HoveredNodeColor);
}
// render circle and text with node id inside
handle.DrawCircle(pos, NodeRadius, Color.ToSrgb(color), false);
var text = _artifactSystem.GetNodeId(node);
var dimensions = handle.GetDimensions(_font, text, 1);
handle.DrawString(_font, pos - new Vector2(dimensions.X / 2, dimensions.Y / 2), text, color);
}
}
// draw edges for each segment and each node that have successors
foreach (var node in segment)
{
var fromNode = GetNodePos(node, ySpacing, segments, ref bottomLeft) + new Vector2(0, -NodeRadius);
var successorNodes = _artifactSystem.GetDirectSuccessorNodes((artifact, artifact), node);
foreach (var successorNode in successorNodes)
{
var color = node.Comp.Locked
? LockedNodeColor
: UnlockedNodeColor;
var toNode = GetNodePos(successorNode, ySpacing, segments, ref bottomLeft) + new Vector2(0, NodeRadius);
handle.DrawLine(fromNode, toNode, color);
}
}
bottomLeft.X += GetBiggestWidth(segment) + segmentSpacing;
}
}
private Vector2 GetNodePos(Entity<XenoArtifactNodeComponent> node, float ySpacing, List<List<Entity<XenoArtifactNodeComponent>>> segments, ref Vector2 bottomLeft)
{
var yPos = -(NodeDiameter + ySpacing) * node.Comp.Depth;
var segment = segments.First(s => s.Contains(node));
var depthOrderedNodes = _artifactSystem.GetDepthOrderedNodes(segment);
var biggestTier = depthOrderedNodes.Max(s => s.Value.Count);
var nodesInLayer = depthOrderedNodes.GetValueOrDefault(node.Comp.Depth)!.Count;
var biggestWidth = (NodeDiameter + MinXSpacing) * biggestTier;
var xSpacing = Math.Clamp((biggestWidth - (NodeDiameter * nodesInLayer)) / (nodesInLayer - 1), MinXSpacing, MaxXSpacing);
var layerXOffset = (biggestWidth - (xSpacing * (nodesInLayer - 1)) - (NodeDiameter * nodesInLayer)) / 2;
// get index of node in current segment's row (row per depth level)
var index = depthOrderedNodes.GetValueOrDefault(node.Comp.Depth)!.IndexOf(node);
var xPos = NodeDiameter * index + (xSpacing * index) + layerXOffset;
return bottomLeft + new Vector2(xPos, yPos);
}
private float GetBiggestWidth(List<Entity<XenoArtifactNodeComponent>> nodes)
{
var num = _artifactSystem.GetDepthOrderedNodes(nodes)
.Max(p => p.Value.Count);
return (NodeDiameter * num) + MinXSpacing * (num - 1);
}
}

View File

@ -1,10 +1,12 @@
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Client.GameObjects;
namespace Content.Client.Xenoarchaeology.XenoArtifacts;
public sealed class RandomArtifactSpriteSystem : VisualizerSystem<RandomArtifactSpriteComponent>
{
[Dependency] private readonly SpriteSystem _sprite = default!;
protected override void OnAppearanceChange(EntityUid uid, RandomArtifactSpriteComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@ -13,25 +15,34 @@ public sealed class RandomArtifactSpriteSystem : VisualizerSystem<RandomArtifact
if (!AppearanceSystem.TryGetData<int>(uid, SharedArtifactsVisuals.SpriteIndex, out var spriteIndex, args.Component))
return;
if (!AppearanceSystem.TryGetData<bool>(uid, SharedArtifactsVisuals.IsUnlocking, out var isUnlocking, args.Component))
isUnlocking = false;
if (!AppearanceSystem.TryGetData<bool>(uid, SharedArtifactsVisuals.IsActivated, out var isActivated, args.Component))
isActivated = false;
var spriteIndexStr = spriteIndex.ToString("D2");
var spritePrefix = isActivated ? "_on" : "";
var spritePrefix = isUnlocking ? "_on" : "";
// layered artifact sprite
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), ArtifactsVisualLayers.Effect, out var layer, false))
if (_sprite.LayerMapTryGet((uid, args.Sprite), ArtifactsVisualLayers.UnlockingEffect, out var layer, false))
{
var spriteState = "ano" + spriteIndexStr;
SpriteSystem.LayerSetRsiState((uid, args.Sprite), ArtifactsVisualLayers.Base, spriteState);
SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, spriteState + "_on");
SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, isActivated);
_sprite.LayerSetRsiState((uid, args.Sprite), ArtifactsVisualLayers.Base, spriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), layer, spriteState + "_on");
_sprite.LayerSetVisible((uid, args.Sprite), layer, isUnlocking);
if (_sprite.LayerMapTryGet((uid, args.Sprite), ArtifactsVisualLayers.ActivationEffect, out var activationEffectLayer, false))
{
_sprite.LayerSetRsiState((uid, args.Sprite), activationEffectLayer, "artifact-activation");
_sprite.LayerSetVisible((uid, args.Sprite), activationEffectLayer, isActivated);
}
}
// non-layered
else
{
var spriteState = "ano" + spriteIndexStr + spritePrefix;
SpriteSystem.LayerSetRsiState((uid, args.Sprite), ArtifactsVisualLayers.Base, spriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), ArtifactsVisualLayers.Base, spriteState);
}
}
}
@ -39,5 +50,6 @@ public sealed class RandomArtifactSpriteSystem : VisualizerSystem<RandomArtifact
public enum ArtifactsVisualLayers : byte
{
Base,
Effect
UnlockingEffect, // doesn't have to use this
ActivationEffect
}

View File

@ -0,0 +1,33 @@
using Content.Shared._DV.Body;
using Content.Shared.Alert;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
public sealed partial class ResurrectWhenAbleSystem : EntitySystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private ProtoId<AlertPrototype> _resurrectingIcon = "ResurrectingIcon";
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_player.LocalSession?.AttachedEntity is not { } ent)
return;
if (!TryComp<ResurrectWhenAbleComponent>(ent, out var comp))
return;
if (comp.ResurrectAt is not { } resurrectTime)
{
_alerts.ClearAlert(ent, _resurrectingIcon);
return;
}
var alertProto = _prototype.Index(_resurrectingIcon);
_alerts.ShowAlert(ent, alertProto, cooldown: (resurrectTime - TimeSpan.FromSeconds(comp.TimeToResurrect), resurrectTime));
}
}

View File

@ -0,0 +1,22 @@
using Content.Client.Light.Components;
using Content.Shared._DV.Light;
using Robust.Client.GameObjects;
namespace Content.Client._DV.Light.EntitySystems;
public sealed partial class ExpendableLightEnergySystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExpendableLightComponent, OnGetLightEnergyEvent>(GetLightEnergy);
}
private void GetLightEnergy(Entity<ExpendableLightComponent> ent, ref OnGetLightEnergyEvent args)
{
if (!TryComp<PointLightComponent>(ent, out var pointLight))
return;
args.LightEnergy = pointLight.Energy;
args.LightRadius = pointLight.Radius;
}
}

View File

@ -16,7 +16,7 @@ public sealed partial class LightReactiveSystem : SharedLightReactiveSystem
_validLightsInRange.Clear();
foreach (var light in _lightsInRange)
{
if(light.Comp.Enabled && !light.Comp.Deleted && light.Comp.NetSyncEnabled)
if (light.Comp.Enabled && !light.Comp.Deleted)
_validLightsInRange.Add(new(light.Owner, light.Comp));
}
return _validLightsInRange;

View File

@ -0,0 +1,35 @@
using System.Numerics;
using Content.Client.Movement.Components;
using Content.Shared._DV.Movement;
using Robust.Client.Timing;
namespace Content.Client._DV.Movement;
public sealed class CursorOffsetActionSystem : SharedCursorOffsetActionSystem
{
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
protected override void OnInit(Entity<CursorOffsetActionComponent> ent, ref ComponentInit args)
{
base.OnInit(ent, ref args);
if (!TryComp<EyeCursorOffsetComponent>(ent, out var eyeOffset))
return;
eyeOffset.Enabled = ent.Comp.Active;
eyeOffset.CurrentPosition = Vector2.Zero;
}
protected override void OnAction(Entity<CursorOffsetActionComponent> ent, ref CursorOffsetActionEvent args)
{
base.OnAction(ent, ref args);
if (!TryComp<EyeCursorOffsetComponent>(ent, out var eyeOffset))
return;
eyeOffset.Enabled = ent.Comp.Active;
if (_gameTiming.IsFirstTimePredicted)
eyeOffset.CurrentPosition = Vector2.Zero;
}
}

View File

@ -21,6 +21,7 @@ public sealed partial class GlimmerOverlaySystem : EntitySystem
_overlay = new GlimmerOverlay();
SubscribeNetworkEvent<GlimmerChangedEvent>(OnGlimmerChanged);
_cfg.OnValueChanged(DCCVars.DisableGlimmerShader, OnDisableGlimmerShaderChanged);
OnDisableGlimmerShaderChanged(_cfg.GetCVar(DCCVars.DisableGlimmerShader));
}
private void OnGlimmerChanged(GlimmerChangedEvent eventArgs)

View File

@ -80,6 +80,7 @@ public sealed class ThermalVisionOverlay : Overlay
_lightEntity ??= _entity.SpawnAttachedTo(null, playerXform.Coordinates);
_transform.SetParent(_lightEntity.Value, player.Value);
var light = _entity.EnsureComponent<PointLightComponent>(_lightEntity.Value);
light.NetSyncEnabled = false; // DeltaV - Desync this component
_light.SetRadius(_lightEntity.Value, LightRadius, light);
_light.SetEnergy(_lightEntity.Value, alpha, light);
_light.SetColor(_lightEntity.Value, Comp.Color, light);

View File

@ -0,0 +1,43 @@
using Content.Shared._RMC14.Effect;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client._RMC14.Effect;
public sealed class RMCEffectSystem : SharedRMCEffectSystem
{
// Most effects are pretty large and flashy so we're dividing the opacity of the parent by 3 before applying it to the effect.
private const int OpacityDivider = 3;
[Dependency] private readonly IGameTiming _timing = default!;
public override void FrameUpdate(float frameTime)
{
var time = _timing.CurTime;
var query = EntityQueryEnumerator<EffectAlphaAnimationComponent, SpriteComponent>();
while (query.MoveNext(out var effect, out var sprite))
{
if (effect.SpawnedAt is not { } spawned)
continue;
var alpha = MathHelper.Lerp((spawned + effect.Delay).TotalSeconds, spawned.TotalSeconds, time.TotalSeconds);
sprite.Color = sprite.Color.WithAlpha((float) alpha);
}
var query2 = EntityQueryEnumerator<RMCEffectComponent>();
while (query2.MoveNext(out var uid, out var effect))
{
var parent = Transform(uid).ParentUid;
if (!TryComp(parent, out SpriteComponent? parentSprite))
return;
if (!TryComp(uid, out SpriteComponent? sprite))
return;
// Only apply the reduced opacity to the effect if the parent's opacity is < 1.
if (sprite.Color.A < 1)
sprite.Color = sprite.Color.WithAlpha(parentSprite.Color.A / OpacityDivider);
}
}
}

View File

@ -0,0 +1,77 @@
using System.Numerics;
using Content.Shared._RMC14.Xenonids.Screech;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
namespace Content.Client._RMC14.Xenonids.Screech;
public sealed class RMCXenoScreechShockWaveOverlay : Overlay, IEntityEventSubscriber
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private SharedTransformSystem? _xformSystem;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
private readonly ShaderInstance _shader;
public RMCXenoScreechShockWaveOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("RMCXenoScreechShockWave").Instance().Duplicate();
}
private Vector2 _position;
private float _waveStrength;
private float _waveSpeed;
private float _downScale;
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (args.Viewport.Eye == null || _xformSystem is null && !_entMan.TrySystem(out _xformSystem))
return false;
var query = _entMan.EntityQueryEnumerator<RMCXenoScreechShockWaveComponent, TransformComponent>();
if (query.MoveNext(out var uid, out var distortion, out var xform))
{
if (xform.MapID != args.MapId)
return false;
var mapPos = _xformSystem.GetWorldPosition(uid);
var tempCoords = args.Viewport.WorldToLocal(mapPos);
// normalized coords, 0 - 1 plane. This is pure hell, we subtract 1 because fragment calculates from the bottom and local goes from the top of the viewport
tempCoords.Y = 1 - (tempCoords.Y / args.Viewport.Size.Y);
tempCoords.X /= args.Viewport.Size.X;
_position = tempCoords;
_waveStrength = distortion.WaveStrength;
_waveSpeed = distortion.WaveSpeed;
_downScale = distortion.DownScale;
return true;
}
return false;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture == null || args.Viewport.Eye == null)
return;
_shader?.SetParameter("position", _position);
_shader?.SetParameter("waveSpeed", _waveSpeed);
_shader?.SetParameter("downScale", _downScale);
_shader?.SetParameter("waveStrength", _waveStrength);
_shader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
var worldHandle = args.WorldHandle;
worldHandle.UseShader(_shader);
worldHandle.DrawRect(args.WorldBounds, Color.White);
worldHandle.UseShader(null);
}
}

View File

@ -0,0 +1,419 @@
using System.Linq;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests;
[TestFixture]
public sealed class XenoArtifactTest
{
[TestPrototypes]
private const string Prototypes = @"
- type: entity
id: TestArtifact
parent: BaseXenoArtifact
name: artifact
components:
- type: XenoArtifact
isGenerationRequired: false
effectsTable: !type:NestedSelector
tableId: XenoArtifactEffectsDefaultTable
- type: entity
id: TestGenArtifactFlat
parent: BaseXenoArtifact
name: artifact
components:
- type: XenoArtifact
isGenerationRequired: true
nodeCount:
min: 2
max: 2
segmentSize:
min: 1
max: 1
nodesPerSegmentLayer:
min: 1
max: 1
effectsTable: !type:NestedSelector
tableId: XenoArtifactEffectsDefaultTable
- type: entity
id: TestGenArtifactTall
parent: BaseXenoArtifact
name: artifact
components:
- type: XenoArtifact
isGenerationRequired: true
nodeCount:
min: 2
max: 2
segmentSize:
min: 2
max: 2
nodesPerSegmentLayer:
min: 1
max: 1
effectsTable: !type:NestedSelector
tableId: XenoArtifactEffectsDefaultTable
- type: entity
id: TestGenArtifactFull
name: artifact
components:
- type: XenoArtifact
isGenerationRequired: true
nodeCount:
min: 6
max: 6
segmentSize:
min: 6
max: 6
nodesPerSegmentLayer:
min: 2
max: 2
effectsTable: !type:NestedSelector
tableId: XenoArtifactEffectsDefaultTable
- type: entity
id: TestArtifactNode
name: artifact node
components:
- type: XenoArtifactNode
maxDurability: 3
";
/// <summary>
/// Checks that adding nodes and edges properly adds them into the adjacency matrix
/// </summary>
[Test]
public async Task XenoArtifactAddNodeTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(artifactUid));
// Create 3 nodes
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
Assert.That(artifactSystem.GetAllNodeIndices(artifactEnt).Count(), Is.EqualTo(3));
// Add connection from 1 -> 2 and 2-> 3
artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
// Assert that successors and direct successors are counted correctly for node 1.
Assert.That(artifactSystem.GetDirectSuccessorNodes(artifactEnt, node1!.Value).Count, Is.EqualTo(1));
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1!.Value).Count, Is.EqualTo(2));
// Assert that we didn't somehow get predecessors on node 1.
Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node1!.Value), Is.Empty);
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node1!.Value), Is.Empty);
// Assert that successors and direct successors are counted correctly for node 2.
Assert.That(artifactSystem.GetDirectSuccessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
// Assert that predecessors and direct predecessors are counted correctly for node 2.
Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
// Assert that successors and direct successors are counted correctly for node 3.
Assert.That(artifactSystem.GetDirectSuccessorNodes(artifactEnt, node3!.Value), Is.Empty);
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node3!.Value), Is.Empty);
// Assert that predecessors and direct predecessors are counted correctly for node 3.
Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(1));
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(2));
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
/// <summary>
/// Checks to make sure that removing nodes properly cleans up all connections.
/// </summary>
[Test]
public async Task XenoArtifactRemoveNodeTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(artifactUid));
// Create 3 nodes
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node5, false));
Assert.That(artifactSystem.GetAllNodeIndices(artifactEnt).Count(), Is.EqualTo(5));
// Add connection: 1 -> 2 -> 3 -> 4 -> 5
artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
artifactSystem.AddEdge(artifactEnt, node3!.Value, node4!.Value, false);
artifactSystem.AddEdge(artifactEnt, node4!.Value, node5!.Value, false);
// Make sure we have a continuous connection between the two ends of the graph.
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1.Value), Has.Count.EqualTo(4));
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node5.Value), Has.Count.EqualTo(4));
// Remove the node and make sure it's no longer in the artifact.
Assert.That(artifactSystem.RemoveNode(artifactEnt, node3!.Value, false));
Assert.That(artifactSystem.TryGetIndex(artifactEnt, node3!.Value, out _), Is.False, "Node 3 still present in artifact.");
// Check to make sure that we got rid of all the connections.
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node2!.Value), Is.Empty);
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty);
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
/// <summary>
/// Sets up series of linked nodes and ensures that resizing the adjacency matrix doesn't disturb the connections
/// </summary>
[Test]
public async Task XenoArtifactResizeTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(artifactUid));
// Create 3 nodes
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
// Add connection: 1 -> 2 -> 3
artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
// Make sure our connection is set up
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node2.Value));
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node3.Value));
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node1.Value), Is.False);
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node2.Value), Is.False);
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node3.Value), Is.False);
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node1.Value), Is.False);
Assert.That(artifactSystem.GetIndex(artifactEnt, node1!.Value), Is.EqualTo(0));
Assert.That(artifactSystem.GetIndex(artifactEnt, node2!.Value), Is.EqualTo(1));
Assert.That(artifactSystem.GetIndex(artifactEnt, node3!.Value), Is.EqualTo(2));
// Add a new node, resizing the original adjacency matrix and array.
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4));
// Check that our connections haven't changed.
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node2.Value));
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node3.Value));
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node1.Value), Is.False);
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node2.Value), Is.False);
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node3.Value), Is.False);
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node1.Value), Is.False);
// Has our array shifted any when we resized?
Assert.That(artifactSystem.GetIndex(artifactEnt, node1!.Value), Is.EqualTo(0));
Assert.That(artifactSystem.GetIndex(artifactEnt, node2!.Value), Is.EqualTo(1));
Assert.That(artifactSystem.GetIndex(artifactEnt, node3!.Value), Is.EqualTo(2));
// Check that 4 didn't somehow end up with connections
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty);
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node4!.Value), Is.Empty);
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
/// <summary>
/// Checks if removing a node and adding a new node into its place in the adjacency matrix doesn't accidentally retain extra data.
/// </summary>
[Test]
public async Task XenoArtifactReplaceTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(artifactUid));
// Create 3 nodes
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
// Add connection: 1 -> 2 -> 3
artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
// Make sure our connection is set up
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node2.Value));
Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node3.Value));
// Remove middle node, severing connections
artifactSystem.RemoveNode(artifactEnt, node2!.Value, false);
// Make sure our connection are properly severed.
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1.Value), Is.Empty);
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3.Value), Is.Empty);
// Make sure our matrix is 3x3
Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixRows, Is.EqualTo(3));
Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixColumns, Is.EqualTo(3));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4, false));
// Make sure that adding in a new node didn't add a new slot but instead re-used the middle slot.
Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixRows, Is.EqualTo(3));
Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixColumns, Is.EqualTo(3));
// Ensure that all connections are still severed
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1.Value), Is.Empty);
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3.Value), Is.Empty);
Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node4!.Value), Is.Empty);
Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty);
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
/// <summary>
/// Checks if the active nodes are properly detected.
/// </summary>
[Test]
public async Task XenoArtifactBuildActiveNodesTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
Entity<XenoArtifactComponent> artifactEnt = (artifactUid, entManager.GetComponent<XenoArtifactComponent>(artifactUid));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node5, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node6, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node7, false));
Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node8, false));
// /----( 6 )
// /----[*3 ]-/----( 7 )----( 8 )
// /
// / /----[*5 ]
// [ 1 ]--/----[ 2 ]--/----( 4 )
// Diagram of the example generation. Nodes in [brackets] are unlocked, nodes in (braces) are locked
// and nodes with an *asterisk are supposed to be active.
artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
artifactSystem.AddEdge(artifactEnt, node1!.Value, node3!.Value, false);
artifactSystem.AddEdge(artifactEnt, node2!.Value, node4!.Value, false);
artifactSystem.AddEdge(artifactEnt, node2!.Value, node5!.Value, false);
artifactSystem.AddEdge(artifactEnt, node3!.Value, node6!.Value, false);
artifactSystem.AddEdge(artifactEnt, node3!.Value, node7!.Value, false);
artifactSystem.AddEdge(artifactEnt, node7!.Value, node8!.Value, false);
artifactSystem.SetNodeUnlocked(node1!.Value);
artifactSystem.SetNodeUnlocked(node2!.Value);
artifactSystem.SetNodeUnlocked(node3!.Value);
artifactSystem.SetNodeUnlocked(node5!.Value);
NetEntity[] expectedActiveNodes =
[
entManager.GetNetEntity(node3!.Value.Owner),
entManager.GetNetEntity(node5!.Value.Owner)
];
Assert.That(artifactEnt.Comp.CachedActiveNodes, Is.SupersetOf(expectedActiveNodes));
Assert.That(artifactEnt.Comp.CachedActiveNodes, Has.Count.EqualTo(expectedActiveNodes.Length));
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
[Test]
public async Task XenoArtifactGenerateSegmentsTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifact1Uid = entManager.Spawn("TestGenArtifactFlat");
Entity<XenoArtifactComponent> artifact1Ent = (artifact1Uid, entManager.GetComponent<XenoArtifactComponent>(artifact1Uid));
var segments1 = artifactSystem.GetSegments(artifact1Ent);
Assert.That(segments1.Count, Is.EqualTo(2));
Assert.That(segments1[0].Count, Is.EqualTo(1));
Assert.That(segments1[1].Count, Is.EqualTo(1));
var artifact2Uid = entManager.Spawn("TestGenArtifactTall");
Entity<XenoArtifactComponent> artifact2Ent = (artifact2Uid, entManager.GetComponent<XenoArtifactComponent>(artifact2Uid));
var segments2 = artifactSystem.GetSegments(artifact2Ent);
Assert.That(segments2.Count, Is.EqualTo(1));
Assert.That(segments2[0].Count, Is.EqualTo(2));
var artifact3Uid = entManager.Spawn("TestGenArtifactFull");
Entity<XenoArtifactComponent> artifact3Ent = (artifact3Uid, entManager.GetComponent<XenoArtifactComponent>(artifact3Uid));
var segments3 = artifactSystem.GetSegments(artifact3Ent);
Assert.That(segments3.Count, Is.EqualTo(1));
Assert.That(segments3.Sum(x => x.Count), Is.EqualTo(6));
var nodesDepths = segments3[0].Select(x => x.Comp.Depth).ToArray();
Assert.That(nodesDepths.Distinct().Count(), Is.EqualTo(3));
var grouped = nodesDepths.ToLookup(x => x);
Assert.That(grouped[0].Count(), Is.EqualTo(2));
Assert.That(grouped[1].Count(), Is.GreaterThanOrEqualTo(2)); // tree is attempting sometimes to get wider (so it will look like a tree)
Assert.That(grouped[2].Count(), Is.LessThanOrEqualTo(2)); // maintain same width or, if we used 3 nodes on previous layer - we only have 1 left!
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
}

View File

@ -9,8 +9,6 @@ using Content.Server.Mind;
using Content.Server.Prayer;
using Content.Server.Silicons.Laws;
using Content.Server.Station.Systems;
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@ -59,7 +57,6 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly DisposalTubeSystem _disposalTubes = default!;
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem = default!;
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly PrayerSystem _prayerSystem = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
@ -485,29 +482,6 @@ namespace Content.Server.Administration.Systems
args.Verbs.Add(verb);
}
// XenoArcheology
if (_adminManager.IsAdmin(player) && TryComp<ArtifactComponent>(args.Target, out var artifact))
{
// make artifact always active (by adding timer trigger)
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("artifact-verb-make-always-active"),
Category = VerbCategory.Debug,
Act = () => EntityManager.AddComponent<ArtifactTimerTriggerComponent>(args.Target),
Disabled = EntityManager.HasComponent<ArtifactTimerTriggerComponent>(args.Target),
Impact = LogImpact.High
});
// force to activate artifact ignoring timeout
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("artifact-verb-activate"),
Category = VerbCategory.Debug,
Act = () => _artifactSystem.ForceActivateArtifact(args.Target, component: artifact),
Impact = LogImpact.High
});
}
// Make Sentient verb
if (_groupController.CanCommand(player, "makesentient") &&
args.User != args.Target &&

View File

@ -61,6 +61,22 @@ public sealed class EmpSystem : SharedEmpSystem
Dirty(empBlast, empBlastComp); // Frontier
}
/// <summary>
/// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
/// </summary>
/// <param name="coordinates">The location to trigger the EMP pulse at.</param>
/// <param name="range">The range of the EMP pulse.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, float duration)
{
foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
{
TryEmpEffects(uid, energyConsumption, duration);
}
Spawn(EmpPulseEffectPrototype, coordinates);
}
/// <summary>
/// Attempts to apply the effects of an EMP pulse onto an entity by first raising an <see cref="EmpAttemptEvent"/>, followed by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>

View File

@ -5,9 +5,9 @@ using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Botany;
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Server.Botany;
using Content.Server.Chat.Systems;
using Content.Server.Emp;
using Content.Server.Explosion.EntitySystems;
@ -28,17 +28,19 @@ using Content.Server.Zombies;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.EntityEffects.Effects.PlantMetabolism;
using Content.Shared.EntityEffects.Effects.StatusEffects;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.EntityEffects;
using Content.Shared.Humanoid;
using Content.Shared.Maps;
using Content.Shared.Mind.Components;
using Content.Shared.Nyanotrasen.Chemistry.Effects;
using Content.Shared.Popups;
using Content.Shared.Random;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Zombies;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@ -47,9 +49,8 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using TemperatureCondition = Content.Shared.EntityEffects.EffectConditions.Temperature; // disambiguate the namespace
using PolymorphEffect = Content.Shared.EntityEffects.Effects.Polymorph;
using TemperatureCondition = Content.Shared.EntityEffects.EffectConditions.Temperature; // disambiguate the namespace
namespace Content.Server.EntityEffects;
@ -999,8 +1000,23 @@ public sealed class EntityEffectSystem : EntitySystem
private void OnActivateArtifact(ref ExecuteEntityEffectEvent<ActivateArtifact> args)
{
var artifact = args.Args.EntityManager.EntitySysManager.GetEntitySystem<ArtifactSystem>();
artifact.TryActivateArtifact(args.Args.TargetEntity, logMissing: false);
// Get the XenoArtifact system
var xenoArtifactSystem = EntityManager.EntitySysManager.GetEntitySystem<SharedXenoArtifactSystem>();
// Make sure the target entity has a XenoArtifactComponent
if (!TryComp<XenoArtifactComponent>(args.Args.TargetEntity, out var artifactComp))
return;
// Wrap the entity and component in the Entity<> type
var artifactEntity = new Entity<XenoArtifactComponent>(args.Args.TargetEntity, artifactComp);
// Try to activate the artifact
xenoArtifactSystem.TryActivateXenoArtifact(
artifact: artifactEntity,
user: null, // no performer info in the effect
target: args.Args.TargetEntity,
coordinates: Transform(args.Args.TargetEntity).Coordinates
);
}
// Nyanotrasen

View File

@ -236,7 +236,7 @@ namespace Content.Server.Flash
public readonly EntityUid Target;
public readonly EntityUid? User;
public readonly EntityUid? Used;
public readonly bool IgnoreProtection; //DeltaV: allow flashing to ignore flash protection
public bool IgnoreProtection; //DeltaV: allow flashing to ignore flash protection
public FlashAttemptEvent(EntityUid target, EntityUid? user, EntityUid? used, bool ignoreProtection) //DeltaV: allow flashing to ignore flash protection
{

View File

@ -1,4 +1,3 @@
using Content.Server.UserInterface;
using Content.Shared.Instruments;
using Robust.Shared.Player;
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;

View File

@ -231,7 +231,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
if (msg.Value)
{
// Prevent stuck notes when turning off a channel... Shrimple.
RaiseNetworkEvent(new InstrumentMidiEventEvent(msg.Uid, new []{RobustMidiEvent.AllNotesOff((byte)msg.Channel, 0)}));
RaiseNetworkEvent(new InstrumentMidiEventEvent(msg.Uid, new[] { RobustMidiEvent.AllNotesOff((byte)msg.Channel, 0) }));
}
Dirty(uid, instrument);
@ -277,7 +277,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
var instrumentQuery = EntityManager.GetEntityQuery<InstrumentComponent>();
if (!TryComp(uid, out InstrumentComponent? originInstrument)
|| originInstrument.InstrumentPlayer is not {} originPlayer)
|| originInstrument.InstrumentPlayer is not { } originPlayer)
return Array.Empty<(NetEntity, string)>();
// It's probably faster to get all possible active instruments than all entities in range
@ -292,7 +292,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
continue;
// We want to use the instrument player's name.
if (instrument.InstrumentPlayer is not {} playerUid)
if (instrument.InstrumentPlayer is not { } playerUid)
continue;
// Maybe a bit expensive but oh well GetBands is queued and has a timer anyway.
@ -320,7 +320,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
var netUid = GetNetEntity(uid);
// Reset puppet instruments too.
RaiseNetworkEvent(new InstrumentMidiEventEvent(netUid, new[]{RobustMidiEvent.SystemReset(0)}));
RaiseNetworkEvent(new InstrumentMidiEventEvent(netUid, new[] { RobustMidiEvent.SystemReset(0) }));
RaiseNetworkEvent(new InstrumentStopMidiEvent(netUid));
}
@ -371,12 +371,12 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
if (instrument.RespectMidiLimits)
{
if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (1 / 3d) + 1))
if (instrument.LaggedBatches == (int)(MaxMidiLaggedBatches * (1 / 3d) + 1))
{
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-light-message"),
uid, attached, PopupType.SmallCaution);
}
else if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (2 / 3d) + 1))
else if (instrument.LaggedBatches == (int)(MaxMidiLaggedBatches * (2 / 3d) + 1))
{
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-serious-message"),
uid, attached, PopupType.MediumCaution);
@ -430,7 +430,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
var query = AllEntityQuery<ActiveInstrumentComponent, InstrumentComponent>();
while (query.MoveNext(out var uid, out _, out var instrument))
{
if (instrument.Master is {} master)
if (instrument.Master is { } master)
{
if (Deleted(master))
{
@ -456,7 +456,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
(instrument.BatchesDropped >= MaxMidiBatchesDropped
|| instrument.LaggedBatches >= MaxMidiLaggedBatches))
{
if (instrument.InstrumentPlayer is {Valid: true} mob)
if (instrument.InstrumentPlayer is { Valid: true } mob)
{
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true);

View File

@ -444,8 +444,15 @@ namespace Content.Server.Kitchen.EntitySystems
private void OnAnchorChanged(EntityUid uid, MicrowaveComponent component, ref AnchorStateChangedEvent args)
{
if (!args.Anchored)
// DeltaV - start of microwave ejection bugfix
if (!args.Anchored)
{
// DeltaV's MicrowaveEventsSystem changes prevent ejection from active microwave, so stop cooking first
StopCooking((uid, component));
_container.EmptyContainer(component.Storage);
UpdateUserInterfaceState(uid, component);
}
// DeltaV - end of microwave ejection bugfix
}
private void OnSignalReceived(Entity<MicrowaveComponent> ent, ref SignalReceivedEvent args)
@ -463,7 +470,12 @@ namespace Content.Server.Kitchen.EntitySystems
{
_userInterface.SetUiState(uid, MicrowaveUiKey.Key, new MicrowaveUpdateUserInterfaceState(
GetNetEntityArray(component.Storage.ContainedEntities.ToArray()),
HasComp<ActiveMicrowaveComponent>(uid),
// DeltaV - start of microwave ejection bugfix
(
EntityManager.TryGetComponent<ActiveMicrowaveComponent>(uid, out var active)
&& active.LifeStage < ComponentLifeStage.Stopping
),
// DeltaV - end of microwave ejection bugfix
component.CurrentCookTimeButtonIndex,
component.CurrentCookTimerTime,
component.CurrentCookTimeEnd
@ -489,6 +501,10 @@ namespace Content.Server.Kitchen.EntitySystems
/// <param name="ent"></param>
public void Explode(Entity<MicrowaveComponent> ent)
{
// DeltaV - start of microwave ejection bugfix
// DeltaV's MicrowaveEventsSystem changes prevent ejection from active microwave, so stop cooking first
StopCooking(ent);
// DeltaV - end of microwave ejection bugfix
ent.Comp.Broken = true; // Make broken so we stop processing stuff
_explosion.TriggerExplosive(ent);
if (TryComp<MachineComponent>(ent, out var machine))
@ -497,6 +513,10 @@ namespace Content.Server.Kitchen.EntitySystems
_container.EmptyContainer(machine.PartContainer);
}
// DeltaV - start of microwave ejection bugfix
UpdateUserInterfaceState(ent, ent.Comp);
// DeltaV - end of microwave ejection bugfix
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(ent)} exploded from unsafe cooking!");
}
@ -710,11 +730,16 @@ namespace Content.Server.Kitchen.EntitySystems
}
}
// DeltaV - start of microwave ejection bugfix
// StopCooking should be in front of both:
// - EmptyContainer() call, because DeltaV MicrowaveEventsSystem prevents ejection from active microwave
// - UpdateUserInterfaceState() call - not very relevant, but UI shouldn't be "busy" after cooking is done
StopCooking((uid, microwave));
_container.EmptyContainer(microwave.Storage);
microwave.CurrentCookTimeEnd = TimeSpan.Zero;
UpdateUserInterfaceState(uid, microwave);
_audio.PlayPvs(microwave.FoodDoneSound, uid);
StopCooking((uid, microwave));
// DeltaV - end of microwave ejection bugfix
}
}

View File

@ -138,15 +138,20 @@ namespace Content.Server.Abilities.Psionics
if (args.Handled)
return;
if (!HasComp<MindSwappedComponent>(args.Mind.CurrentEntity))
return;
//No idea where the viaCommand went. It's on the internal OnGhostAttempt, but not this layer. Maybe unnecessary.
/*if (!args.viaCommand)
return;*/
args.Result = false;
args.Handled = true;
// DeltaV - start of trapped ghost fix
// If you're able to swap back to your original body, you should swap back before you ghost.
if (TryComp<MindSwappedComponent>(args.Mind.CurrentEntity, out var component)
&& _actions.GetAction(component.MindSwapReturnActionEntity) is { } action
&& action.Comp.AttachedEntity is not null)
{
args.Result = false;
args.Handled = true;
}
// DeltaV - end of trapped ghost fix
}
private void OnSwapInit(EntityUid uid, MindSwappedComponent component, ComponentInit args)

View File

@ -222,7 +222,7 @@ namespace Content.Server.Psionics.Glimmer
var slope = (float) (11 - _glimmerSystem.Glimmer / 100);
var maxIntensity = 20;
var removed = (float) _glimmerSystem.Glimmer * _random.NextFloat(0.1f, 0.15f);
var removed = (float) _glimmerSystem.Glimmer * _random.NextFloat(0.06f, 0.08f);
_glimmerSystem.Glimmer -= (int) removed;
BeamRandomNearProber(uid, _glimmerSystem.Glimmer / 350, _glimmerSystem.Glimmer / 50);
_explosionSystem.QueueExplosion(uid, "Default", totalIntensity, slope, maxIntensity);

View File

@ -1,18 +1,15 @@
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Instruments;
using Content.Shared.Kitchen.Components; // DeltaV - shared
using Content.Server.Store.Systems;
using Content.Shared.Kitchen.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind.Components;
using Content.Shared.PAI;
using Content.Shared.Popups;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Content.Shared.Instruments;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using System.Text;
using Content.Shared.Instruments;
using Robust.Shared.Player;
namespace Content.Server.PAI;
@ -22,13 +19,12 @@ public sealed class PAISystem : SharedPAISystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly StoreSystem _store = default!;
[Dependency] private readonly ToggleableGhostRoleSystem _toggleableGhostRole = default!;
/// <summary>
/// Possible symbols that can be part of a scrambled pai's name.
/// </summary>
private static readonly char[] SYMBOLS = new[] { '#', '~', '-', '@', '&', '^', '%', '$', '*', ' ' };
private static readonly char[] SYMBOLS = new[] { '#', '~', '-', '@', '&', '^', '%', '$', '*', ' '};
public override void Initialize()
{
@ -38,8 +34,6 @@ public sealed class PAISystem : SharedPAISystem
SubscribeLocalEvent<PAIComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<PAIComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<PAIComponent, BeingMicrowavedEvent>(OnMicrowaved);
SubscribeLocalEvent<PAIComponent, PAIShopActionEvent>(OnShop);
}
private void OnUseInHand(EntityUid uid, PAIComponent component, UseInHandEvent args)
@ -107,14 +101,6 @@ public sealed class PAISystem : SharedPAISystem
_metaData.SetEntityName(uid, val);
}
private void OnShop(Entity<PAIComponent> ent, ref PAIShopActionEvent args)
{
if (!TryComp<StoreComponent>(ent, out var store))
return;
_store.ToggleUi(args.Performer, ent, store);
}
public void PAITurningOff(EntityUid uid)
{
// Close the instrument interface if it was open

View File

@ -1,4 +1,4 @@
using Content.Server.Radiation.Components;
using Content.Server.Radiation.Components;
using Content.Shared.Nutrition.EntitySystems; // DeltaV
using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Events;
@ -53,7 +53,7 @@ public sealed partial class RadiationSystem : EntitySystem
public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time)
{
var msg = new OnIrradiatedEvent(time, radsPerSecond);
var msg = new OnIrradiatedEvent(time, radsPerSecond, uid);
RaiseLocalEvent(uid, msg);
}

View File

@ -8,6 +8,7 @@ using Content.Shared.Station.Components;
using JetBrains.Annotations;
using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Chemistry.Reaction;
namespace Content.Server.StationEvents.Events;
@ -47,7 +48,7 @@ public sealed class VentClogRule : StationEventSystem<VentClogRuleComponent>
var quantity = weak ? component.WeakReagentQuantity : component.ReagentQuantity;
solution.AddReagent(reagent, quantity);
var foamEnt = Spawn("Foam", transform.Coordinates);
var foamEnt = Spawn(ChemicalReactionSystem.FoamReaction, transform.Coordinates);
var spreadAmount = weak ? component.WeakSpread : component.Spread;
_smoke.StartSmoke(foamEnt, solution, component.Time, spreadAmount);
Audio.PlayPvs(component.Sound, transform.Coordinates);

View File

@ -404,10 +404,12 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
!someRecord.JobTitle.ToLower().Contains(filterLowerCaseValue),
StationRecordFilterType.Species =>
!someRecord.Species.ToLower().Contains(filterLowerCaseValue),
StationRecordFilterType.Prints => someRecord.Fingerprint != null
&& IsFilterWithSomeCodeValue(someRecord.Fingerprint, filterLowerCaseValue),
StationRecordFilterType.DNA => someRecord.DNA != null
&& IsFilterWithSomeCodeValue(someRecord.DNA, filterLowerCaseValue),
// DeltaV - start of silicon bio filters fix
StationRecordFilterType.Prints => someRecord.Fingerprint == null
|| IsFilterWithSomeCodeValue(someRecord.Fingerprint, filterLowerCaseValue),
StationRecordFilterType.DNA => someRecord.DNA == null
|| IsFilterWithSomeCodeValue(someRecord.DNA, filterLowerCaseValue),
// DeltaV - end of silicon bio filters fix
_ => throw new IndexOutOfRangeException(nameof(filter.Type)),
};
}

View File

@ -1,11 +1,11 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Item;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
namespace Content.Server.Xenoarchaeology.Artifact;
public sealed class RandomArtifactSpriteSystem : EntitySystem
{
@ -17,8 +17,11 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomArtifactSpriteComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RandomArtifactSpriteComponent, ArtifactActivatedEvent>(OnActivated);
SubscribeLocalEvent<RandomArtifactSpriteComponent, ArtifactUnlockingStartedEvent>(UnlockingStageStarted);
SubscribeLocalEvent<RandomArtifactSpriteComponent, ArtifactUnlockingFinishedEvent>(UnlockingStageFinished);
SubscribeLocalEvent<RandomArtifactSpriteComponent, XenoArtifactActivatedEvent>(ArtifactActivated);
}
public override void Update(float frameTime)
@ -47,9 +50,19 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
_item.SetHeldPrefix(uid, "ano" + randomSprite.ToString("D2")); //set item artifact inhands
}
private void OnActivated(EntityUid uid, RandomArtifactSpriteComponent component, ArtifactActivatedEvent args)
private void UnlockingStageStarted(Entity<RandomArtifactSpriteComponent> ent, ref ArtifactUnlockingStartedEvent args)
{
_appearance.SetData(uid, SharedArtifactsVisuals.IsActivated, true);
component.ActivationStart = _time.CurTime;
_appearance.SetData(ent, SharedArtifactsVisuals.IsUnlocking, true);
}
private void UnlockingStageFinished(Entity<RandomArtifactSpriteComponent> ent, ref ArtifactUnlockingFinishedEvent args)
{
_appearance.SetData(ent, SharedArtifactsVisuals.IsUnlocking, false);
}
private void ArtifactActivated(Entity<RandomArtifactSpriteComponent> ent, ref XenoArtifactActivatedEvent args)
{
_appearance.SetData(ent, SharedArtifactsVisuals.IsActivated, true);
ent.Comp.ActivationStart = _time.CurTime;
}
}

View File

@ -0,0 +1,14 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// This is used for recharging all nearby batteries when activated.
/// </summary>
[RegisterComponent, Access(typeof(XAEChargeBatterySystem))]
public sealed partial class XAEChargeBatteryComponent : Component
{
/// <summary>
/// The radius of entities that will be affected.
/// </summary>
[DataField("radius")]
public float Radius = 15f;
}

View File

@ -0,0 +1,16 @@
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// XenoArtifact effect that creates gas in atmosphere.
/// </summary>
[RegisterComponent, Access(typeof(XAECreateGasSystem))]
public sealed partial class XAECreateGasComponent : Component
{
/// <summary>
/// The gases and how many moles will be created of each.
/// </summary>
[DataField]
public Dictionary<Gas, float> Gases = new();
}

View File

@ -0,0 +1,46 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Destructible.Thresholds;
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// This is used for an artifact that creates a puddle of
/// random chemicals upon being triggered.
/// </summary>
[RegisterComponent, Access(typeof(XAECreatePuddleSystem))]
public sealed partial class XAECreatePuddleComponent : Component
{
/// <summary>
/// The solution where all the chemicals are stored.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public Solution ChemicalSolution = default!;
/// <summary>
/// The different chemicals that can be spawned by this effect.
/// </summary>
[DataField]
public List<ProtoId<ReagentPrototype>> PossibleChemicals = new();
/// <summary>
/// The number of chemicals in the puddle.
/// </summary>
[DataField]
public MinMax ChemAmount = new MinMax(1, 3);
/// <summary>
/// List of reagents selected for this node. Selected ones are chosen on first activation
/// and are picked from <see cref="PossibleChemicals"/> and is calculated separately for each node.
/// </summary>
[DataField]
public List<ProtoId<ReagentPrototype>>? SelectedChemicals;
/// <summary>
/// Marker, if entity where this component is placed should have description replaced with selected chemicals
/// on component init.
/// </summary>
[DataField]
public bool ReplaceDescription;
}

View File

@ -0,0 +1,26 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Effect of EMP on activation.
/// </summary>
[RegisterComponent, Access(typeof(XAEEmpInAreaSystem))]
public sealed partial class XAEEmpInAreaComponent : Component
{
/// <summary>
/// Range of EMP effect.
/// </summary>
[DataField]
public float Range = 4f;
/// <summary>
/// Energy to be consumed from energy containers.
/// </summary>
[DataField]
public float EnergyConsumption = 1000000;
/// <summary>
/// Duration (in seconds) for which devices going to be disabled.
/// </summary>
[DataField]
public float DisableDuration = 60f;
}

View File

@ -0,0 +1,56 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Generates foam from the artifact when activated.
/// </summary>
[RegisterComponent, Access(typeof(XAEFoamSystem))]
public sealed partial class XAEFoamComponent : Component
{
/// <summary>
/// The list of reagents that will randomly be picked from
/// to choose the foam reagent.
/// </summary>
[DataField(required: true)]
public List<ProtoId<ReagentPrototype>> Reagents = new();
/// <summary>
/// The foam reagent.
/// </summary>
[DataField]
public string? SelectedReagent;
/// <summary>
/// How long does the foam last?
/// </summary>
[DataField]
public float Duration = 10f;
/// <summary>
/// How much reagent is in the foam?
/// </summary>
[DataField]
public float ReagentAmount = 100f;
/// <summary>
/// Minimum radius of foam spawned.
/// </summary>
[DataField]
public int MinFoamAmount = 15;
/// <summary>
/// Maximum radius of foam spawned.
/// </summary>
[DataField]
public int MaxFoamAmount = 20;
/// <summary>
/// Marker, if entity where this component is placed should have description replaced with selected chemicals
/// on component init.
/// </summary>
[DataField]
public bool ReplaceDescription;
}

View File

@ -0,0 +1,22 @@
using Content.Shared.Destructible.Thresholds;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Artifact that ignites surrounding entities when triggered.
/// </summary>
[RegisterComponent, Access(typeof(XAEIgniteSystem))]
public sealed partial class XAEIgniteComponent : Component
{
/// <summary>
/// Range, inside which all entities going be set on fire.
/// </summary>
[DataField]
public float Range = 2f;
/// <summary>
/// Amount of fire stacks to apply
/// </summary>
[DataField]
public MinMax FireStack = new(2, 5);
}

View File

@ -1,20 +1,20 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Flickers all the lights within a certain radius.
/// </summary>
[RegisterComponent]
public sealed partial class LightFlickerArtifactComponent : Component
[RegisterComponent, Access(typeof(XAELightFlickerSystem))]
public sealed partial class XAELightFlickerComponent : Component
{
/// <summary>
/// Lights within this radius will be flickered on activation
/// Lights within this radius will be flickered on activation.
/// </summary>
[DataField("radius")]
[DataField]
public float Radius = 4;
/// <summary>
/// The chance that the light will flicker
/// The chance that the light will flicker.
/// </summary>
[DataField("flickerChance")]
[DataField]
public float FlickerChance = 0.75f;
}

View File

@ -1,16 +1,14 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
using Robust.Shared.Audio;
using Content.Shared.Polymorph;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Artifact polymorphs surrounding entities when triggered.
/// Artifact polymorphs entities when triggered.
/// </summary>
[RegisterComponent]
[Access(typeof(PolyOthersArtifactSystem))]
public sealed partial class PolyOthersArtifactComponent : Component
[RegisterComponent, Access(typeof(XAEPolymorphSystem))]
public sealed partial class XAEPolymorphComponent : Component
{
/// <summary>
/// The polymorph effect to trigger.
@ -19,7 +17,7 @@ public sealed partial class PolyOthersArtifactComponent : Component
public ProtoId<PolymorphPrototype> PolymorphPrototypeName = "ArtifactMonkey";
/// <summary>
/// range of the effect.
/// Range of the effect.
/// </summary>
[DataField]
public float Range = 2f;

View File

@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// When activated artifact will spawn a pair of portals. First - right in artifact, Second - at random point of station.
/// </summary>
[RegisterComponent, Access(typeof(XAEPortalSystem))]
public sealed partial class XAEPortalComponent : Component
{
/// <summary>
/// Entity that should be spawned as portal.
/// </summary>
[DataField, AutoNetworkedField]
public EntProtoId PortalProto = "PortalArtifact";
}

View File

@ -1,11 +1,11 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Harmless artifact that broadcast "thoughts" to players nearby.
/// Thoughts are shown as popups and unique for each player.
/// </summary>
[RegisterComponent]
public sealed partial class TelepathicArtifactComponent : Component
[RegisterComponent, Access(typeof(XAETelepathicSystem))]
public sealed partial class XAETelepathicComponent : Component
{
/// <summary>
/// Loc string ids of telepathic messages.

View File

@ -1,12 +1,12 @@
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Change atmospherics temperature until it reach target.
/// </summary>
[RegisterComponent]
public sealed partial class TemperatureArtifactComponent : Component
[RegisterComponent, Access(typeof(XAETemperatureSystem))]
public sealed partial class XAETemperatureComponent : Component
{
[DataField("targetTemp"), ViewVariables(VVAccess.ReadWrite)]
public float TargetTemperature = Atmospherics.T0C;

View File

@ -1,11 +1,11 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Throws all nearby entities backwards.
/// Also pries nearby tiles.
/// </summary>
[RegisterComponent]
public sealed partial class ThrowArtifactComponent : Component
[RegisterComponent, Access(typeof(XAEThrowThingsAroundSystem))]
public sealed partial class XAEThrowThingsAroundComponent : Component
{
/// <summary>
/// How close do you have to be to get yeeted?

View File

@ -0,0 +1,9 @@
using Content.Shared.Explosion.Components.OnTrigger;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Activates 'trigger' for <see cref="ExplodeOnTriggerComponent"/>.
/// </summary>
[RegisterComponent, Access(typeof(XAETriggerExplosivesSystem))]
public sealed partial class XAETriggerExplosivesComponent : Component;

View File

@ -0,0 +1,31 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact activation effect that is fully charging batteries in certain range.
/// </summary>
public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryComponent>
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<Entity<BatteryComponent>> _batteryEntities = new();
/// <inheritdoc />
protected override void OnActivated(Entity<XAEChargeBatteryComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var chargeBatteryComponent = ent.Comp;
_batteryEntities.Clear();
_lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities);
foreach (var battery in _batteryEntities)
{
_battery.SetCharge(battery, battery.Comp.MaxCharge, battery);
}
}
}

View File

@ -0,0 +1,52 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Atmos;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that creates certain atmospheric gas on artifact tile / adjacent tiles.
/// </summary>
public sealed class XAECreateGasSystem : BaseXAESystem<XAECreateGasComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly MapSystem _map = default!;
protected override void OnActivated(Entity<XAECreateGasComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var grid = _transform.GetGrid(args.Coordinates);
var map = _transform.GetMap(args.Coordinates);
if (map == null || !TryComp<MapGridComponent>(grid, out var gridComp))
return;
var tile = _map.LocalToTile(grid.Value, gridComp, args.Coordinates);
var mixtures = new ValueList<GasMixture>();
if (_atmosphere.GetTileMixture(grid.Value, map.Value, tile, excite: true) is { } localMixture)
mixtures.Add(localMixture);
if (_atmosphere.GetAdjacentTileMixtures(grid.Value, tile, excite: true) is var adjacentTileMixtures)
{
while (adjacentTileMixtures.MoveNext(out var adjacentMixture))
{
mixtures.Add(adjacentMixture);
}
}
foreach (var (gas, moles) in ent.Comp.Gases)
{
var molesPerMixture = moles / mixtures.Count;
foreach (var mixture in mixtures)
{
mixture.AdjustMoles(gas, molesPerMixture);
}
}
}
}

View File

@ -0,0 +1,77 @@
using Content.Server.Fluids.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that creates puddle of chemical reagents under artifact.
/// </summary>
public sealed class XAECreatePuddleSystem: BaseXAESystem<XAECreatePuddleComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly MetaDataSystem _metaData= default!;
[Dependency] private readonly IPrototypeManager _prototypeManager= default!;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<XAECreatePuddleComponent, MapInitEvent>(OnInit);
}
private void OnInit(EntityUid uid, XAECreatePuddleComponent component, MapInitEvent _)
{
if (component.PossibleChemicals == null || component.PossibleChemicals.Count == 0)
return;
if (component.SelectedChemicals == null)
{
var chemicalList = new List<ProtoId<ReagentPrototype>>();
var chemAmount = component.ChemAmount.Next(_random);
for (var i = 0; i < chemAmount; i++)
{
var chemProto = _random.Pick(component.PossibleChemicals);
chemicalList.Add(chemProto);
}
component.SelectedChemicals = chemicalList;
}
if (component.ReplaceDescription)
{
var reagentNames = new HashSet<string>();
foreach (var chemProtoId in component.SelectedChemicals)
{
var reagent = _prototypeManager.Index(chemProtoId);
reagentNames.Add(reagent.LocalizedName);
}
var reagentNamesStr = string.Join(", ", reagentNames);
var newEntityDescription = Loc.GetString("xenoarch-effect-puddle", ("reagent", reagentNamesStr));
_metaData.SetEntityDescription(uid, newEntityDescription);
}
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAECreatePuddleComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
if (component.SelectedChemicals == null)
return;
var amountPerChem = component.ChemicalSolution.MaxVolume / component.SelectedChemicals.Count;
foreach (var reagent in component.SelectedChemicals)
{
component.ChemicalSolution.AddReagent(reagent, amountPerChem);
}
_puddle.TrySpillAt(ent, component.ChemicalSolution, out _);
}
}

View File

@ -0,0 +1,20 @@
using Content.Server.Emp;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that creates EMP on use.
/// </summary>
public sealed class XAEEmpInAreaSystem : BaseXAESystem<XAEEmpInAreaComponent>
{
[Dependency] private readonly EmpSystem _emp = default!;
/// <inheritdoc />
protected override void OnActivated(Entity<XAEEmpInAreaComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
_emp.EmpPulse(args.Coordinates, ent.Comp.Range, ent.Comp.EnergyConsumption, ent.Comp.DisableDuration);
}
}

View File

@ -0,0 +1,63 @@
using Content.Server.Fluids.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that starts Foam chemical reaction with random-ish reagents inside.
/// </summary>
public sealed class XAEFoamSystem : BaseXAESystem<XAEFoamComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SmokeSystem _smoke = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager= default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<XAEFoamComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, XAEFoamComponent component, MapInitEvent args)
{
if (component.SelectedReagent != null)
return;
if (component.Reagents.Count == 0)
return;
component.SelectedReagent = _random.Pick(component.Reagents);
if (component.ReplaceDescription)
{
var reagent = _prototypeManager.Index<ReagentPrototype>(component.SelectedReagent);
var newEntityDescription = Loc.GetString("xenoarch-effect-foam", ("reagent", reagent.LocalizedName));
_metaData.SetEntityDescription(uid, newEntityDescription);
}
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAEFoamComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
if (component.SelectedReagent == null)
return;
var sol = new Solution();
var range = (int)MathF.Round(MathHelper.Lerp(component.MinFoamAmount, component.MaxFoamAmount, _random.NextFloat(0, 1f)));
sol.AddReagent(component.SelectedReagent, component.ReagentAmount);
var foamEnt = Spawn(ChemicalReactionSystem.FoamReaction, args.Coordinates);
var spreadAmount = range * 4;
_smoke.StartSmoke(foamEnt, sol, component.Duration, spreadAmount);
}
}

View File

@ -0,0 +1,47 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact activation effect that ignites any flammable entity in range.
/// </summary>
public sealed class XAEIgniteSystem : BaseXAESystem<XAEIgniteComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FlammableSystem _flammable = default!;
private EntityQuery<FlammableComponent> _flammables;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_flammables = GetEntityQuery<FlammableComponent>();
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAEIgniteComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
_entities.Clear();
_lookup.GetEntitiesInRange(ent.Owner, component.Range, _entities);
foreach (var target in _entities)
{
if (!_flammables.TryGetComponent(target, out var fl))
continue;
fl.FireStacks += component.FireStack.Next(_random);
_flammable.Ignite(target, ent.Owner, fl);
}
}
}

View File

@ -0,0 +1,49 @@
using Content.Server.Ghost;
using Content.Server.Light.Components;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact activation effect that flickers light on and off.
/// </summary>
public sealed class XAELightFlickerSystem : BaseXAESystem<XAELightFlickerComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
private EntityQuery<PoweredLightComponent> _lights;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_lights = GetEntityQuery<PoweredLightComponent>();
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAELightFlickerComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
_entities.Clear();
_lookup.GetEntitiesInRange(ent.Owner, ent.Comp.Radius, _entities, LookupFlags.StaticSundries);
foreach (var light in _entities)
{
if (!_lights.HasComponent(light))
continue;
if (!_random.Prob(ent.Comp.FlickerChance))
continue;
//todo: extract effect from ghost system, update power system accordingly
_ghost.DoGhostBooEvent(light);
}
}
}

View File

@ -0,0 +1,39 @@
using Content.Server.Polymorph.Systems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Humanoid;
using Content.Shared.Mobs.Systems;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact activation effect that is polymorphing all humanoid entities in range.
/// </summary>
public sealed class XAEPolymorphSystem : BaseXAESystem<XAEPolymorphComponent>
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly PolymorphSystem _poly = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<Entity<HumanoidAppearanceComponent>> _humanoids = new();
/// <inheritdoc />
protected override void OnActivated(Entity<XAEPolymorphComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
_humanoids.Clear();
_lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Range, _humanoids);
foreach (var comp in _humanoids)
{
var target = comp.Owner;
if (!_mob.IsAlive(target))
continue;
_poly.PolymorphEntity(target, ent.Comp.PolymorphPrototypeName);
_audio.PlayPvs(ent.Comp.PolySound, ent);
}
}
}

View File

@ -0,0 +1,58 @@
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Teleportation.Systems;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Collections;
using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that creates temporary portal between places on station.
/// </summary>
public sealed class XAEPortalSystem : BaseXAESystem<XAEPortalComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly LinkedEntitySystem _link = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly IGameTiming _timing = default!;
/// <inheritdoc />
protected override void OnActivated(Entity<XAEPortalComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
if (!_timing.IsFirstTimePredicted)
return;
var map = Transform(ent).MapID;
var validMinds = new ValueList<EntityUid>();
var mindQuery = EntityQueryEnumerator<MindContainerComponent, MobStateComponent, TransformComponent, MetaDataComponent>();
while (mindQuery.MoveNext(out var uid, out var mc, out _, out var xform, out var meta))
{
// check if the MindContainer has a Mind and if the entity is not in a container (this also auto excludes AI) and if they are on the same map
if (mc.HasMind && !_container.IsEntityOrParentInContainer(uid, meta: meta, xform: xform) && xform.MapID == map)
{
validMinds.Add(uid);
}
}
// this would only be 0 if there were a station full of AIs and no one else, in that case just stop this function
if (validMinds.Count == 0)
return;
if(!TrySpawnNextTo(ent.Comp.PortalProto, args.Artifact, out var firstPortal))
return;
var target = _random.Pick(validMinds);
if(!TrySpawnNextTo(ent.Comp.PortalProto, target, out var secondPortal))
return;
// Manual position swapping, because the portal that opens doesn't trigger a collision, and doesn't teleport targets the first time.
_transform.SwapPositions(target, args.Artifact.Owner);
_link.TryLink(firstPortal.Value, secondPortal.Value, true);
}
}

View File

@ -1,31 +1,34 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
public sealed class TelepathicArtifactSystem : EntitySystem
/// <summary>
/// System for xeno artifact activation effect that sends sublime telepathic messages.
/// </summary>
public sealed class XAETelepathicSystem : BaseXAESystem<XAETelepathicComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TelepathicArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
private void OnActivate(EntityUid uid, TelepathicArtifactComponent component, ArtifactActivatedEvent args)
/// <inheritdoc />
protected override void OnActivated(Entity<XAETelepathicComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
// try to find victims nearby
var victims = _lookup.GetEntitiesInRange(uid, component.Range);
foreach (var victimUid in victims)
_entities.Clear();
_lookup.GetEntitiesInRange(ent, component.Range, _entities);
foreach (var victimUid in _entities)
{
if (!EntityManager.HasComponent<ActorComponent>(victimUid))
if (!HasComp<ActorComponent>(victimUid))
continue;
// roll if msg should be usual or drastic

View File

@ -0,0 +1,49 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Atmos;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Server.GameObjects;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that changes atmospheric temperature on adjacent tiles.
/// </summary>
public sealed class XAETemperatureSystem : BaseXAESystem<XAETemperatureComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
/// <inheritdoc />
protected override void OnActivated(Entity<XAETemperatureComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
var transform = Transform(ent);
var center = _atmosphereSystem.GetContainingMixture(ent.Owner, false, true);
if (center == null)
return;
UpdateTileTemperature(component, center);
if (component.AffectAdjacentTiles && transform.GridUid != null)
{
var position = _transformSystem.GetGridOrMapTilePosition(ent, transform);
var enumerator = _atmosphereSystem.GetAdjacentTileMixtures(transform.GridUid.Value, position, excite: true);
while (enumerator.MoveNext(out var mixture))
{
UpdateTileTemperature(component, mixture);
}
}
}
private void UpdateTileTemperature(XAETemperatureComponent component, GasMixture environment)
{
var dif = component.TargetTemperature - environment.Temperature;
var absDif = Math.Abs(dif);
var step = Math.Min(absDif, component.SpawnTemperature);
environment.Temperature += dif > 0 ? step : -step;
}
}

View File

@ -0,0 +1,72 @@
using System.Numerics;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Throwing;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact activation effect that pries tiles and throws stuff around.
/// </summary>
public sealed class XAEThrowThingsAroundSystem : BaseXAESystem<XAEThrowThingsAroundComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly TileSystem _tile = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
private EntityQuery<PhysicsComponent> _physQuery;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_physQuery = GetEntityQuery<PhysicsComponent>();
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAEThrowThingsAroundComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
var xform = Transform(ent);
if (TryComp<MapGridComponent>(xform.GridUid, out var grid))
{
var areaForTilesPry = new Circle(_transform.GetWorldPosition(xform), component.Range);
var tiles = _map.GetTilesIntersecting(xform.GridUid.Value, grid, areaForTilesPry, true);
foreach (var tile in tiles)
{
if (!_random.Prob(component.TilePryChance))
continue;
_tile.PryTile(tile);
}
}
_entities.Clear();
_lookup.GetEntitiesInRange(ent, component.Range, _entities, LookupFlags.Dynamic | LookupFlags.Sundries);
foreach (var entity in _entities)
{
if (_physQuery.TryGetComponent(entity, out var phys)
&& (phys.CollisionMask & (int)CollisionGroup.GhostImpassable) != 0)
continue;
var tempXform = Transform(entity);
var foo = _transform.GetWorldPosition(tempXform) - _transform.GetWorldPosition(xform);
_throwing.TryThrow(entity, foo * 2, component.ThrowStrength, ent, 0);
}
}
}

View File

@ -0,0 +1,24 @@
using Content.Server.Explosion.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Explosion.Components;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect of triggering explosion.
/// </summary>
public sealed class XAETriggerExplosivesSystem : BaseXAESystem<XAETriggerExplosivesComponent>
{
[Dependency] private readonly ExplosionSystem _explosion = default!;
/// <inheritdoc />
protected override void OnActivated(Entity<XAETriggerExplosivesComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
if(!TryComp<ExplosiveComponent>(ent, out var explosiveComp))
return;
_explosion.TriggerExplosive(ent, explosiveComp);
}
}

View File

@ -0,0 +1,28 @@
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// This is used for an artifact that is activated by having a certain amount of gas around it.
/// </summary>
[RegisterComponent, Access(typeof(XATGasSystem))]
public sealed partial class XATGasComponent : Component
{
/// <summary>
/// The gas that is related to trigger.
/// </summary>
[DataField]
public Gas TargetGas;
/// <summary>
/// The amount of gas needed.
/// </summary>
[DataField]
public float Moles = Atmospherics.MolesCellStandard * 0.1f;
/// <summary>
/// Marker, if mentioned gas should be present in entity tile for trigger to activate, or it should not.
/// </summary>
[DataField]
public bool ShouldBePresent = true;
}

View File

@ -0,0 +1,21 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// Component for triggering node on getting activated by powerful magnets.
/// </summary>
[RegisterComponent, Access(typeof(XATMagnetSystem))]
public sealed partial class XATMagnetComponent : Component
{
/// <summary>
/// How close to the magnet do you have to be?
/// </summary>
[DataField]
public float MagnetRange = 40f;
/// <summary>
/// How close do active magboots have to be?
/// This is smaller because they are weaker magnets
/// </summary>
[DataField]
public float MagbootsRange = 2f;
}

View File

@ -0,0 +1,20 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// This is used for an artifact that activates when above or below a certain pressure.
/// </summary>
[RegisterComponent, Access(typeof(XATPressureSystem))]
public sealed partial class XATPressureComponent : Component
{
/// <summary>
/// The lower-end pressure threshold. Is not considered when null.
/// </summary>
[DataField]
public float? MinPressureThreshold;
/// <summary>
/// The higher-end pressure threshold. Is not considered when null.
/// </summary>
[DataField]
public float? MaxPressureThreshold;
}

View File

@ -0,0 +1,20 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// This is used for an artifact that is activated by having a certain temperature near it.
/// </summary>
[RegisterComponent, Access(typeof(XATTemperatureSystem))]
public sealed partial class XATTemperatureComponent : Component
{
/// <summary>
/// Threshold temperature for trigger activation.
/// </summary>
[DataField]
public float TargetTemperature;
/// <summary>
/// Marker, if temp needs to be above or below the target.
/// </summary>
[DataField]
public bool TriggerOnHigherTemp = true;
}

View File

@ -0,0 +1,36 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Artifact.XAT;
namespace Content.Server.Xenoarchaeology.Artifact.XAT;
/// <summary>
/// System for xeno artifact trigger, which gets activated from some gas being on the same time as artifact with certain concentration.
/// </summary>
public sealed class XATGasSystem : BaseQueryUpdateXATSystem<XATGasComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATGasComponent, XenoArtifactNodeComponent> node, float frameTime)
{
var xform = Transform(artifact);
if (_atmosphere.GetTileMixture((artifact, xform)) is not { } mixture)
return;
var gasTrigger = node.Comp1;
var moles = mixture.GetMoles(gasTrigger.TargetGas);
if (gasTrigger.ShouldBePresent)
{
if (moles >= gasTrigger.Moles)
Trigger(artifact, node);
}
else
{
if (moles <= gasTrigger.Moles)
Trigger(artifact, node);
}
}
}

View File

@ -0,0 +1,67 @@
using Content.Server.Salvage;
using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
using Content.Shared.Clothing;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Artifact.XAT;
namespace Content.Server.Xenoarchaeology.Artifact.XAT;
/// <summary>
/// System for checking if magnets-related xeno artifact node should be triggered.
/// Works with magboots and salvage magnet, salvage magnet triggers only upon pulsing on activation.
/// </summary>
public sealed class XATMagnetSystem : BaseQueryUpdateXATSystem<XATMagnetComponent>
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private HashSet<Entity<MagbootsComponent>> _magbootEntities = new();
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SalvageMagnetActivatedEvent>(OnMagnetActivated);
}
/// <inheritdoc />
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATMagnetComponent, XenoArtifactNodeComponent> node, float frameTime)
{
var coords = Transform(artifact.Owner).Coordinates;
_magbootEntities.Clear();
_lookup.GetEntitiesInRange(coords, node.Comp1.MagbootsRange, _magbootEntities);
foreach (var ent in _magbootEntities)
{
if(!TryComp<ItemToggleComponent>(ent, out var itemToggle) || !itemToggle.Activated)
continue;
Trigger(artifact, node);
break;
}
}
private void OnMagnetActivated(ref SalvageMagnetActivatedEvent args)
{
var magnetCoordinates = Transform(args.Magnet).Coordinates;
var query = EntityQueryEnumerator<XATMagnetComponent, XenoArtifactNodeComponent>();
while (query.MoveNext(out var uid, out var comp, out var node))
{
if (node.Attached == null)
continue;
var artifact = _xenoArtifactQuery.Get(GetEntity(node.Attached.Value));
if (!CanTrigger(artifact, (uid, node)))
continue;
var artifactCoordinates = Transform(artifact).Coordinates;
if (_transform.InRange(magnetCoordinates, artifactCoordinates, comp.MagnetRange))
Trigger(artifact, (uid, comp, node));
}
}
}

View File

@ -0,0 +1,29 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Artifact.XAT;
namespace Content.Server.Xenoarchaeology.Artifact.XAT;
/// <summary>
/// System for checking if pressure-related xeno artifact node should be triggered.
/// </summary>
public sealed class XATPressureSystem : BaseQueryUpdateXATSystem<XATPressureComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
/// <inheritdoc />
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATPressureComponent, XenoArtifactNodeComponent> node, float frameTime)
{
var xform = Transform(artifact);
if (_atmosphere.GetTileMixture((artifact, xform)) is not { } mixture)
return;
var pressure = mixture.Pressure;
if (pressure >= node.Comp1.MaxPressureThreshold || pressure <= node.Comp1.MinPressureThreshold)
{
Trigger(artifact, node);
}
}
}

View File

@ -0,0 +1,37 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Artifact.XAT;
namespace Content.Server.Xenoarchaeology.Artifact.XAT;
/// <summary>
/// System for checking if temperature-related xeno artifact node should be triggered.
/// </summary>
public sealed class XATTemperatureSystem : BaseQueryUpdateXATSystem<XATTemperatureComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
/// <inheritdoc />
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATTemperatureComponent, XenoArtifactNodeComponent> node, float frameTime)
{
var xform = Transform(artifact);
if (_atmosphere.GetTileMixture((artifact, xform)) is not { } mixture)
return;
var curTemp = mixture.Temperature;
var temperatureTriggerComponent = node.Comp1;
if (temperatureTriggerComponent.TriggerOnHigherTemp)
{
if (curTemp >= temperatureTriggerComponent.TargetTemperature)
Trigger(artifact, node);
}
else
{
if (curTemp <= temperatureTriggerComponent.TargetTemperature)
Trigger(artifact, node);
}
}
}

View File

@ -0,0 +1,125 @@
using System.Text;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
namespace Content.Server.Xenoarchaeology.Artifact;
/// <summary>
/// Toolshed commands for manipulating xeno artifact.
/// </summary>
[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
public sealed class XenoArtifactCommand : ToolshedCommand
{
[ValidatePrototypeId<EntityPrototype>]
public const string ArtifactPrototype = "BaseXenoArtifact";
/// <summary> List existing artifacts. </summary>
[CommandImplementation("list")]
public IEnumerable<EntityUid> List()
{
var query = EntityManager.EntityQueryEnumerator<XenoArtifactComponent>();
while (query.MoveNext(out var uid, out _))
{
yield return uid;
}
}
/// <summary>
/// Output matrix of artifact nodes and how they are connected.
/// </summary>
[CommandImplementation("printMatrix")]
public string PrintMatrix([PipedArgument] EntityUid artifactEntitUid)
{
var comp = EntityManager.GetComponent<XenoArtifactComponent>(artifactEntitUid);
var nodeCount = comp.NodeVertices.Length;
var sb = new StringBuilder("\n |");
for (var i = 0; i < nodeCount; i++)
{
sb.Append($" {i:D2}|");
}
AddHorizontalFiller(sb);
for (var i = 0; i < nodeCount; i++)
{
sb.Append($"\n{i:D2}|");
for (var j = 0; j < nodeCount; j++)
{
var value = comp.NodeAdjacencyMatrix[i][j]
? "X"
: " ";
sb.Append($" {value} |");
}
AddHorizontalFiller(sb);
}
return sb.ToString();
void AddHorizontalFiller(StringBuilder builder)
{
builder.AppendLine();
builder.Append("--+");
for (var i = 0; i < nodeCount; i++)
{
builder.Append($"---+");
}
}
}
/// <summary> Output total research points artifact contains. </summary>
[CommandImplementation("totalResearch")]
public int TotalResearch([PipedArgument] EntityUid artifactEntityUid)
{
var artiSys = EntityManager.System<XenoArtifactSystem>();
var comp = EntityManager.GetComponent<XenoArtifactComponent>(artifactEntityUid);
var sum = 0;
var nodes = artiSys.GetAllNodes((artifactEntityUid, comp));
foreach (var node in nodes)
{
sum += node.Comp.ResearchValue;
}
return sum;
}
/// <summary>
/// Spawns a bunch of artifacts and gets average total research points they can yield.
/// </summary>
[CommandImplementation("averageResearch")]
public float AverageResearch()
{
const int n = 100;
var sum = 0;
for (var i = 0; i < n; i++)
{
var ent = Spawn(ArtifactPrototype, MapCoordinates.Nullspace);
sum += TotalResearch(ent);
Del(ent);
}
return (float) sum / n;
}
/// <summary> Unlocks all nodes of artifact. </summary>
[CommandImplementation("unlockAllNodes")]
public void UnlockAllNodes([PipedArgument] EntityUid artifactEntityUid)
{
var artiSys = EntityManager.System<XenoArtifactSystem>();
var comp = EntityManager.GetComponent<XenoArtifactComponent>(artifactEntityUid);
var nodes = artiSys.GetAllNodes((artifactEntityUid, comp));
foreach (var node in nodes)
{
artiSys.SetNodeUnlocked((node, node.Comp));
}
}
}

View File

@ -0,0 +1,219 @@
using System.Linq;
using Content.Shared.Random.Helpers;
using Content.Shared.Whitelist;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Artifact.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.Artifact;
public sealed partial class XenoArtifactSystem
{
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
private void GenerateArtifactStructure(Entity<XenoArtifactComponent> ent)
{
var nodeCount = ent.Comp.NodeCount.Next(RobustRandom);
var triggerPool = CreateTriggerPool(ent, nodeCount);
// trigger pool could be smaller, then requested node count
nodeCount = triggerPool.Count;
ResizeNodeGraph(ent, nodeCount);
while (nodeCount > 0)
{
GenerateArtifactSegment(ent, triggerPool, ref nodeCount);
}
RebuildXenoArtifactMetaData((ent, ent));
}
/// <summary>
/// Creates pool from all node triggers that current artifact can support.
/// As artifact cannot re-use triggers, pool will be growing smaller
/// and smaller with each node generated.
/// </summary>
/// <param name="ent">Artifact for which pool should be created.</param>
/// <param name="size">
/// Max size of pool. Resulting pool is not guaranteed to be exactly as large, but it will 100% won't be bigger.
/// </param>
private List<XenoArchTriggerPrototype> CreateTriggerPool(Entity<XenoArtifactComponent> ent, int size)
{
var triggerPool = new List<XenoArchTriggerPrototype>(size);
var weightsProto = PrototypeManager.Index(ent.Comp.TriggerWeights);
var weightsByTriggersLeft = new Dictionary<string, float>(weightsProto.Weights);
while (triggerPool.Count < size)
{
// OOPS! We ran out of triggers.
if (weightsByTriggersLeft.Count == 0)
{
Log.Error($"Insufficient triggers for generating {ToPrettyString(ent)}! Needed {size} but had {triggerPool.Count}");
return triggerPool;
}
var triggerId = RobustRandom.Pick(weightsByTriggersLeft);
weightsByTriggersLeft.Remove(triggerId);
var trigger = PrototypeManager.Index<XenoArchTriggerPrototype>(triggerId);
if (_entityWhitelist.IsWhitelistFail(trigger.Whitelist, ent))
continue;
triggerPool.Add(trigger);
}
return triggerPool;
}
/// <summary>
/// Generates segment of artifact - isolated graph, nodes inside which are interconnected.
/// As size of segment is randomized - it is subtracted from node count.
/// </summary>
private void GenerateArtifactSegment(
Entity<XenoArtifactComponent> ent,
List<XenoArchTriggerPrototype> triggerPool,
ref int nodeCount
)
{
var segmentSize = GetArtifactSegmentSize(ent, nodeCount);
nodeCount -= segmentSize;
var populatedNodes = PopulateArtifactSegmentRecursive(ent, triggerPool, ref segmentSize);
var segments = GetSegmentsFromNodes(ent, populatedNodes).ToList();
// We didn't connect all of our nodes: do extra work to make sure there's a connection.
if (segments.Count > 1)
{
var parent = segments.MaxBy(s => s.Count)!;
var minP = parent.Min(n => n.Comp.Depth);
var maxP = parent.Max(n => n.Comp.Depth);
segments.Remove(parent);
foreach (var segment in segments)
{
// calculate the range of the depth of the nodes in the segment
var minS = segment.Min(n => n.Comp.Depth);
var maxS = segment.Max(n => n.Comp.Depth);
// Figure out the range of depths that allows for a connection between these two.
// The range is essentially the lower values + 1 on each side.
var min = Math.Max(minS, minP) - 1;
var max = Math.Min(maxS, maxP) + 1;
// how the fuck did you do this? you don't even deserve to get a parent. fuck you.
if (min > max || min == max)
continue;
var node1Options = segment.Where(n => n.Comp.Depth >= min && n.Comp.Depth <= max)
.ToList();
if (node1Options.Count == 0)
{
continue;
}
var node1 = RobustRandom.Pick(node1Options);
var node1Depth = node1.Comp.Depth;
var node2Options = parent.Where(n => n.Comp.Depth >= node1Depth - 1 && n.Comp.Depth <= node1Depth + 1 && n.Comp.Depth != node1Depth)
.ToList();
if (node2Options.Count == 0)
{
continue;
}
var node2 = RobustRandom.Pick(node2Options);
if (node1.Comp.Depth < node2.Comp.Depth)
{
AddEdge((ent, ent.Comp), node1, node2, false);
}
else
{
AddEdge((ent, ent.Comp), node2, node1, false);
}
}
}
}
/// <summary>
/// Recursively populate layers of artifact segment - isolated graph, nodes inside which are interconnected.
/// Each next iteration is going to have more chances to have more nodes (so it goes 'from top to bottom' of
/// the tree, creating its peak nodes first, and then making layers with more and more branches).
/// </summary>
private List<Entity<XenoArtifactNodeComponent>> PopulateArtifactSegmentRecursive(
Entity<XenoArtifactComponent> ent,
List<XenoArchTriggerPrototype> triggerPool,
ref int segmentSize,
int iteration = 0
)
{
if (segmentSize == 0)
return new();
// Try and get larger as we create more layers. Prevents excessive layers.
var mod = RobustRandom.Next((int) (iteration / 1.5f), iteration + 1);
var layerMin = Math.Min(ent.Comp.NodesPerSegmentLayer.Min + mod, segmentSize);
var layerMax = Math.Min(ent.Comp.NodesPerSegmentLayer.Max + mod, segmentSize);
// Default to one node if we had shenanigans and ended up with weird layer counts.
var nodeCount = 1;
if (layerMax >= layerMin)
nodeCount = RobustRandom.Next(layerMin, layerMax + 1); // account for non-inclusive max
segmentSize -= nodeCount;
var nodes = new List<Entity<XenoArtifactNodeComponent>>();
for (var i = 0; i < nodeCount; i++)
{
var trigger = RobustRandom.PickAndTake(triggerPool);
nodes.Add(CreateNode(ent, trigger, iteration));
}
var successors = PopulateArtifactSegmentRecursive(
ent,
triggerPool,
ref segmentSize,
iteration: iteration + 1
);
if (successors.Count == 0)
return nodes;
foreach (var successor in successors)
{
var node = RobustRandom.Pick(nodes);
AddEdge((ent, ent), node, successor, dirty: false);
}
// randomly add in some extra edges for variance.
var scatterCount = ent.Comp.ScatterPerLayer.Next(RobustRandom);
for (var i = 0; i < scatterCount; i++)
{
var node = RobustRandom.Pick(nodes);
var successor = RobustRandom.Pick(successors);
AddEdge((ent, ent), node, successor, dirty: false);
}
return nodes;
}
/// <summary>
/// Rolls segment size, based on amount of nodes left and XenoArtifactComponent settings.
/// </summary>
private int GetArtifactSegmentSize(Entity<XenoArtifactComponent> ent, int nodeCount)
{
// Make sure we can't generate a single segment artifact.
// We always want to have at least 2 segments. For variety.
var segmentMin = ent.Comp.SegmentSize.Min;
var segmentMax = Math.Min(ent.Comp.SegmentSize.Max, Math.Max(nodeCount / 2, segmentMin));
var segmentSize = RobustRandom.Next(segmentMin, segmentMax + 1); // account for non-inclusive max
var remainder = nodeCount - segmentSize;
// If our next segment is going to be undersized, then we just absorb it into this segment.
if (remainder < ent.Comp.SegmentSize.Min)
segmentSize += remainder;
// Sanity check to make sure we don't exceed the node count. (it shouldn't happen prior anyway but oh well)
segmentSize = Math.Min(nodeCount, segmentSize);
return segmentSize;
}
}

View File

@ -0,0 +1,35 @@
using Content.Shared.Cargo;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
namespace Content.Server.Xenoarchaeology.Artifact;
/// <inheritdoc cref="SharedXenoArtifactSystem"/>
public sealed partial class XenoArtifactSystem : SharedXenoArtifactSystem
{
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<XenoArtifactComponent, MapInitEvent>(OnArtifactMapInit);
SubscribeLocalEvent<XenoArtifactComponent, PriceCalculationEvent>(OnCalculatePrice);
}
private void OnArtifactMapInit(Entity<XenoArtifactComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.IsGenerationRequired)
GenerateArtifactStructure(ent);
}
private void OnCalculatePrice(Entity<XenoArtifactComponent> ent, ref PriceCalculationEvent args)
{
foreach (var node in GetAllNodes(ent))
{
if (node.Comp.Locked)
continue;
args.Price += node.Comp.ResearchValue * ent.Comp.PriceMultiplier;
}
}
}

View File

@ -0,0 +1,84 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Robust.Shared.Console;
namespace Content.Server.Xenoarchaeology.Artifact;
/// <summary> Command for unlocking specific node of xeno artifact. </summary>
[AdminCommand(AdminFlags.Debug)]
public sealed class XenoArtifactUnlockNodeCommand : LocalizedCommands
{
[Dependency] private readonly EntityManager _entities = default!;
/// <inheritdoc />
public override string Command => "unlocknode";
/// <inheritdoc />
public override string Description => Loc.GetString("cmd-unlocknode-desc");
/// <inheritdoc />
public override string Help => Loc.GetString("cmd-unlocknode-help");
/// <inheritdoc />
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString("cmd-parse-failure-unlocknode-arg-num"));
return;
}
if (!NetEntity.TryParse(args[1], out var netNode))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-unlocknode-invalid-entity"));
return;
}
if (!_entities.TryGetEntity(netNode, out var entityUid))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-unlocknode-invalid-entity"));
return;
}
_entities.System<XenoArtifactSystem>()
.SetNodeUnlocked(entityUid.Value);
}
/// <inheritdoc />
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var query = _entities.EntityQueryEnumerator<XenoArtifactComponent>();
var completionOptions = new List<CompletionOption>();
while (query.MoveNext(out var uid, out _))
{
completionOptions.Add(new CompletionOption(uid.ToString()));
}
return CompletionResult.FromHintOptions(completionOptions, "<artifact uid>");
}
if (args.Length == 2 &&
NetEntity.TryParse(args[0], out var netEnt) &&
_entities.TryGetEntity(netEnt, out var artifactUid) &&
_entities.TryGetComponent<XenoArtifactComponent>(artifactUid, out var comp))
{
var artifactSystem = _entities.System<XenoArtifactSystem>();
var result = new List<CompletionOption>();
foreach (var node in artifactSystem.GetAllNodes((artifactUid.Value, comp)))
{
var metaData = _entities.MetaQuery.Comp(artifactUid.Value);
var entityUidStr = _entities.GetNetEntity(node)
.ToString();
var completionOption = new CompletionOption(entityUidStr, metaData.EntityName);
result.Add(completionOption);
}
return CompletionResult.FromHintOptions(result, "<node uid>");
}
return CompletionResult.Empty;
}
}

View File

@ -0,0 +1,51 @@
using Content.Server.Research.Systems;
using Content.Server.Xenoarchaeology.Artifact;
using Content.Shared.Popups;
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Xenoarchaeology.Equipment;
/// <inheritdoc />
public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly XenoArtifactSystem _xenoArtifact = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleExtractButtonPressedMessage>(OnExtractButtonPressed);
}
private void OnExtractButtonPressed(Entity<AnalysisConsoleComponent> ent, ref AnalysisConsoleExtractButtonPressedMessage args)
{
if (!TryGetArtifactFromConsole(ent, out var artifact))
return;
if (!_research.TryGetClientServer(ent, out var server, out var serverComponent))
return;
var sumResearch = 0;
foreach (var node in _xenoArtifact.GetAllNodes(artifact.Value))
{
var research = _xenoArtifact.GetResearchValue(node);
_xenoArtifact.SetConsumedResearchValue(node, node.Comp.ConsumedResearchValue + research);
sumResearch += research;
}
// 4-16-25: It's a sad day when a scientist makes negative 5k research
if (sumResearch <= 0)
return;
_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);
}
}

View File

@ -1,36 +0,0 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// Activecomp used for tracking artifact analyzers that are currently
/// in the process of scanning an artifact.
/// </summary>
[RegisterComponent]
public sealed partial class ActiveArtifactAnalyzerComponent : Component
{
/// <summary>
/// When did the scanning start or last resume?
/// </summary>
[DataField("startTime", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan StartTime;
/// <summary>
/// When pausing, this will store the duration the scan has already been running for.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan AccumulatedRunTime;
/// <summary>
/// Is analysis paused?
/// It could be when the Artifact Analyzer has no power, for example.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool AnalysisPaused = false;
/// <summary>
/// What is being scanned?
/// </summary>
[DataField]
public EntityUid Artifact;
}

View File

@ -1,22 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// This is used for tracking artifacts that are currently
/// being scanned by <see cref="ActiveArtifactAnalyzerComponent"/>
/// </summary>
[RegisterComponent]
public sealed partial class ActiveScannedArtifactComponent : Component
{
/// <summary>
/// The scanner that is scanning this artifact
/// </summary>
[ViewVariables]
public EntityUid Scanner;
/// <summary>
/// The sound that plays when the scan fails
/// </summary>
public readonly SoundSpecifier ScanFailureSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
}

View File

@ -1,38 +0,0 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// The console that is used for artifact analysis
/// </summary>
[RegisterComponent]
public sealed partial class AnalysisConsoleComponent : Component
{
/// <summary>
/// The analyzer entity the console is linked.
/// Can be null if not linked.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public EntityUid? AnalyzerEntity;
/// <summary>
/// The machine linking port for the analyzer
/// </summary>
[DataField("linkingPort", customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
public string LinkingPort = "ArtifactAnalyzerSender";
/// <summary>
/// The sound played when an artifact has points extracted.
/// </summary>
[DataField("extractSound")]
public SoundSpecifier ExtractSound = new SoundPathSpecifier("/Audio/Effects/radpulse11.ogg");
/// <summary>
/// The entity spawned by a report.
/// </summary>
[DataField("reportEntityId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ReportEntityId = "Paper";
}

View File

@ -1,60 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Shared.Construction.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// A machine that is combined and linked to the <see cref="AnalysisConsoleComponent"/>
/// in order to analyze artifacts and extract points.
/// </summary>
[RegisterComponent]
public sealed partial class ArtifactAnalyzerComponent : Component
{
/// <summary>
/// How long it takes to analyze an artifact
/// </summary>
[DataField("analysisDuration", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(30);
// Nyano - Summary - Begin modified code block: tie artifacts to glimmer.
/// <summary>
/// The ratio of research points per one glimmer.
/// </summary>
public int ExtractRatio = 750;
// Nyano - End modified code block.
// Begin DeltaV - Tie point output to glimmer
/// <summary>
/// The maximum added multiplier, reached at max glimmer.
/// </summary>
[DataField]
public float PointGlimmerMultiplier = 8f;
// End DeltaV - Tie point output to glimmer
/// <summary>
/// The corresponding console entity.
/// Can be null if not linked.
/// </summary>
[ViewVariables]
public EntityUid? Console;
[ViewVariables(VVAccess.ReadWrite)]
public bool ReadyToPrint = false;
[DataField("scanFinishedSound")]
public SoundSpecifier ScanFinishedSound = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
#region Analysis Data
[DataField]
public EntityUid? LastAnalyzedArtifact;
[ViewVariables]
public ArtifactNode? LastAnalyzedNode;
[ViewVariables(VVAccess.ReadWrite)]
public int? LastAnalyzerPointValue;
#endregion
}

View File

@ -1,12 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// This is used for artifacts that are biased to move
/// in a particular direction via the <see cref="TraversalDistorterComponent"/>
/// </summary>
[RegisterComponent]
public sealed partial class BiasedArtifactComponent : Component
{
[ViewVariables]
public EntityUid Provider;
}

View File

@ -1,7 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
[RegisterComponent]
public sealed partial class NodeScannerComponent : Component
{
}

View File

@ -1,10 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// Suppress artifact activation, when entity is placed inside this container.
/// </summary>
[RegisterComponent]
public sealed partial class SuppressArtifactContainerComponent : Component
{
}

View File

@ -1,21 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// This is used for a machine that biases
/// an artifact placed on it to move up/down
/// </summary>
[RegisterComponent]
public sealed partial class TraversalDistorterComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
public BiasDirection BiasDirection = BiasDirection.Up;
public TimeSpan NextActivation = default!;
public TimeSpan ActivationDelay = TimeSpan.FromSeconds(1);
}
public enum BiasDirection : byte
{
Up, //Towards depth 0
Down, //Away from depth 0
}

View File

@ -1,549 +0,0 @@
using System.Linq;
using Content.Server.Power.Components;
using Content.Server.Research.Systems;
using Content.Shared.UserInterface;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Audio;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Paper;
using Content.Shared.Placeable;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Research.Components;
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Psionics.Glimmer; // DeltaV
namespace Content.Server.Xenoarchaeology.Equipment.Systems;
/// <summary>
/// This system is used for managing the artifact analyzer as well as the analysis console.
/// It also hanadles scanning and ui updates for both systems.
/// </summary>
public sealed class ArtifactAnalyzerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
[Dependency] private readonly GlimmerSystem _glimmerSystem = default!; // DeltaV
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] private readonly TraversalDistorterSystem _traversalDistorter = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ActiveScannedArtifactComponent, ArtifactActivatedEvent>(OnArtifactActivated);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, ComponentStartup>(OnAnalyzeStart);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, ComponentShutdown>(OnAnalyzeEnd);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ArtifactAnalyzerComponent, ItemPlacedEvent>(OnItemPlaced);
SubscribeLocalEvent<ArtifactAnalyzerComponent, ItemRemovedEvent>(OnItemRemoved);
SubscribeLocalEvent<ArtifactAnalyzerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AnalysisConsoleComponent, NewLinkEvent>(OnNewLink);
SubscribeLocalEvent<AnalysisConsoleComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleServerSelectionMessage>(OnServerSelectionMessage);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleScanButtonPressedMessage>(OnScanButton);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsolePrintButtonPressedMessage>(OnPrintButton);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleExtractButtonPressedMessage>(OnExtractButton);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleBiasButtonPressedMessage>(OnBiasButton);
SubscribeLocalEvent<AnalysisConsoleComponent, ResearchClientServerSelectedMessage>((e, c, _) => UpdateUserInterface(e, c),
after: new[] { typeof(ResearchSystem) });
SubscribeLocalEvent<AnalysisConsoleComponent, ResearchClientServerDeselectedMessage>((e, c, _) => UpdateUserInterface(e, c),
after: new[] { typeof(ResearchSystem) });
SubscribeLocalEvent<AnalysisConsoleComponent, BeforeActivatableUIOpenEvent>((e, c, _) => UpdateUserInterface(e, c));
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ActiveArtifactAnalyzerComponent, ArtifactAnalyzerComponent>();
while (query.MoveNext(out var uid, out var active, out var scan))
{
if (active.AnalysisPaused)
continue;
if (_timing.CurTime - active.StartTime < scan.AnalysisDuration - active.AccumulatedRunTime)
continue;
FinishScan(uid, scan, active);
}
}
/// <summary>
/// Resets the current scan on the artifact analyzer
/// </summary>
/// <param name="uid">The analyzer being reset</param>
/// <param name="component"></param>
[PublicAPI]
public void ResetAnalyzer(EntityUid uid, ArtifactAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.LastAnalyzedArtifact = null;
component.ReadyToPrint = false;
UpdateAnalyzerInformation(uid, component);
}
/// <summary>
/// Goes through the current entities on
/// the analyzer and returns a valid artifact
/// </summary>
/// <param name="uid"></param>
/// <param name="placer"></param>
/// <returns></returns>
private EntityUid? GetArtifactForAnalysis(EntityUid? uid, ItemPlacerComponent? placer = null)
{
if (uid == null || !Resolve(uid.Value, ref placer))
return null;
return placer.PlacedEntities.FirstOrNull();
}
/// <summary>
/// Updates the current scan information based on
/// the last artifact that was scanned.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
private void UpdateAnalyzerInformation(EntityUid uid, ArtifactAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.LastAnalyzedArtifact == null)
{
component.LastAnalyzerPointValue = null;
component.LastAnalyzedNode = null;
}
else if (TryComp<ArtifactComponent>(component.LastAnalyzedArtifact, out var artifact))
{
var lastNode = artifact.CurrentNodeId == null
? null
: (ArtifactNode?) _artifact.GetNodeFromId(artifact.CurrentNodeId.Value, artifact).Clone();
component.LastAnalyzedNode = lastNode;
// DeltaV - dynamic glimmer multiplier doesn't play nice with static artifact point values,
// so clamp this to prevent negative values on analyzer.
component.LastAnalyzerPointValue = Math.Clamp(_artifact.GetResearchPointValue(component.LastAnalyzedArtifact.Value, artifact), 0, int.MaxValue);
}
}
private void OnMapInit(EntityUid uid, ArtifactAnalyzerComponent component, MapInitEvent args)
{
if (!TryComp<DeviceLinkSinkComponent>(uid, out var sink))
return;
foreach (var source in sink.LinkedSources)
{
if (!TryComp<AnalysisConsoleComponent>(source, out var analysis))
continue;
component.Console = source;
analysis.AnalyzerEntity = uid;
return;
}
}
private void OnNewLink(EntityUid uid, AnalysisConsoleComponent component, NewLinkEvent args)
{
if (!TryComp<ArtifactAnalyzerComponent>(args.Sink, out var analyzer))
return;
component.AnalyzerEntity = args.Sink;
analyzer.Console = uid;
UpdateUserInterface(uid, component);
}
private void OnPortDisconnected(EntityUid uid, AnalysisConsoleComponent component, PortDisconnectedEvent args)
{
if (args.Port == component.LinkingPort && component.AnalyzerEntity != null)
{
if (TryComp<ArtifactAnalyzerComponent>(component.AnalyzerEntity, out var analyzezr))
analyzezr.Console = null;
component.AnalyzerEntity = null;
}
UpdateUserInterface(uid, component);
}
private void UpdateUserInterface(EntityUid uid, AnalysisConsoleComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
EntityUid? artifact = null;
FormattedMessage? msg = null;
TimeSpan? totalTime = null;
var canScan = false;
var canPrint = false;
var points = 0;
if (TryComp<ArtifactAnalyzerComponent>(component.AnalyzerEntity, out var analyzer))
{
artifact = analyzer.LastAnalyzedArtifact;
msg = GetArtifactScanMessage(analyzer);
totalTime = analyzer.AnalysisDuration;
if (TryComp<ItemPlacerComponent>(component.AnalyzerEntity, out var placer))
canScan = placer.PlacedEntities.Any();
canPrint = analyzer.ReadyToPrint;
// the artifact that's actually on the scanner right now.
if (GetArtifactForAnalysis(component.AnalyzerEntity, placer) is { } current)
points = _artifact.GetResearchPointValue(current);
}
var analyzerConnected = component.AnalyzerEntity != null;
var serverConnected = TryComp<ResearchClientComponent>(uid, out var client) && client.ConnectedToServer;
var scanning = TryComp<ActiveArtifactAnalyzerComponent>(component.AnalyzerEntity, out var active);
var paused = active != null ? active.AnalysisPaused : false;
var biasDirection = BiasDirection.Up;
if (TryComp<TraversalDistorterComponent>(component.AnalyzerEntity, out var trav))
biasDirection = trav.BiasDirection;
var state = new AnalysisConsoleUpdateState(GetNetEntity(artifact), analyzerConnected, serverConnected,
canScan, canPrint, msg, scanning, paused, active?.StartTime, active?.AccumulatedRunTime, totalTime, points, biasDirection == BiasDirection.Down);
_ui.SetUiState(uid, ArtifactAnalzyerUiKey.Key, state);
}
/// <summary>
/// opens the server selection menu.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnServerSelectionMessage(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleServerSelectionMessage args)
{
_ui.OpenUi(uid, ResearchClientUiKey.Key, args.Actor);
}
/// <summary>
/// Starts scanning the artifact.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnScanButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleScanButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (HasComp<ActiveArtifactAnalyzerComponent>(component.AnalyzerEntity))
return;
var ent = GetArtifactForAnalysis(component.AnalyzerEntity);
if (ent == null)
return;
var activeComp = EnsureComp<ActiveArtifactAnalyzerComponent>(component.AnalyzerEntity.Value);
activeComp.StartTime = _timing.CurTime;
activeComp.AccumulatedRunTime = TimeSpan.Zero;
activeComp.Artifact = ent.Value;
if (TryComp<ApcPowerReceiverComponent>(component.AnalyzerEntity.Value, out var powa))
activeComp.AnalysisPaused = !powa.Powered;
var activeArtifact = EnsureComp<ActiveScannedArtifactComponent>(ent.Value);
activeArtifact.Scanner = component.AnalyzerEntity.Value;
UpdateUserInterface(uid, component);
}
private void OnPrintButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsolePrintButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (!TryComp<ArtifactAnalyzerComponent>(component.AnalyzerEntity, out var analyzer) ||
analyzer.LastAnalyzedNode == null ||
analyzer.LastAnalyzerPointValue == null ||
!analyzer.ReadyToPrint)
{
return;
}
analyzer.ReadyToPrint = false;
var report = Spawn(component.ReportEntityId, Transform(uid).Coordinates);
_metaSystem.SetEntityName(report, Loc.GetString("analysis-report-title", ("id", analyzer.LastAnalyzedNode.Id)));
var msg = GetArtifactScanMessage(analyzer);
if (msg == null)
return;
_popup.PopupEntity(Loc.GetString("analysis-console-print-popup"), uid);
if (TryComp<PaperComponent>(report, out var paperComp))
_paper.SetContent((report, paperComp), msg.ToMarkup());
UpdateUserInterface(uid, component);
}
private FormattedMessage? GetArtifactScanMessage(ArtifactAnalyzerComponent component)
{
var msg = new FormattedMessage();
if (component.LastAnalyzedNode == null)
return null;
var n = component.LastAnalyzedNode;
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-id", ("id", n.Id)));
msg.PushNewline();
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-depth", ("depth", n.Depth)));
msg.PushNewline();
var activated = n.Triggered
? "analysis-console-info-triggered-true"
: "analysis-console-info-triggered-false";
msg.AddMarkupOrThrow(Loc.GetString(activated));
msg.PushNewline();
msg.PushNewline();
var needSecondNewline = false;
var triggerProto = _prototype.Index<ArtifactTriggerPrototype>(n.Trigger);
if (triggerProto.TriggerHint != null)
{
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-trigger",
("trigger", Loc.GetString(triggerProto.TriggerHint))) + "\n");
needSecondNewline = true;
}
var effectproto = _prototype.Index<ArtifactEffectPrototype>(n.Effect);
if (effectproto.EffectHint != null)
{
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-effect",
("effect", Loc.GetString(effectproto.EffectHint))) + "\n");
needSecondNewline = true;
}
if (needSecondNewline)
msg.PushNewline();
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-edges", ("edges", n.Edges.Count)));
msg.PushNewline();
// Begin DeltaV - show glimmer multiplier
if (component.LastAnalyzerPointValue != null)
{
msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-value", ("value", (int) (component.LastAnalyzerPointValue * GetGlimmerMultiplier(component)))));
msg.PushNewline();
}
msg.AddMarkupOrThrow(Loc.GetString("old-analysis-console-glimmer-multiplier-text", ("mult", GetGlimmerMultiplier(component).ToString("N2"))));
// End DeltaV - show glimmer multiplier
return msg;
}
/// <summary>
/// Extracts points from the artifact and updates the server points
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnExtractButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleExtractButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (!_research.TryGetClientServer(uid, out var server, out var serverComponent))
return;
var artifact = GetArtifactForAnalysis(component.AnalyzerEntity);
if (artifact == null)
return;
var pointValue = _artifact.GetResearchPointValue(artifact.Value);
// Begin DeltaV Additions - extracting artifacts raises glimmer proportional to the points, floored,
// and artifact point output is proportional to glimmer.
if (TryComp<ArtifactAnalyzerComponent>(component.AnalyzerEntity.Value, out var analyzer) &&
analyzer != null)
{
// DeltaV - divide by multiplier to avoid insane glimmer nukes at high multiplier values. cast to float to avoid loss of fraction
_glimmerSystem.Glimmer += (int)(pointValue / (float)analyzer.ExtractRatio / GetGlimmerMultiplier(analyzer));
pointValue = (int) (pointValue * GetGlimmerMultiplier(analyzer));
}
// no new nodes triggered so nothing to add
if (pointValue == 0)
return;
_research.ModifyServerPoints(server.Value, pointValue, serverComponent);
_artifact.AdjustConsumedPoints(artifact.Value, pointValue);
// End DeltaV Additions
_audio.PlayPvs(component.ExtractSound, component.AnalyzerEntity.Value, AudioParams.Default.WithVolume(2f));
_popup.PopupEntity(Loc.GetString("analyzer-artifact-extract-popup"),
component.AnalyzerEntity.Value, PopupType.Large);
UpdateUserInterface(uid, component);
}
private void OnBiasButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleBiasButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (!TryComp<TraversalDistorterComponent>(component.AnalyzerEntity, out var trav))
return;
if (!_traversalDistorter.SetState(component.AnalyzerEntity.Value, trav, args.IsDown))
return;
UpdateUserInterface(uid, component);
}
/// <summary>
/// Cancels scans if the artifact changes nodes (is activated) during the scan.
/// </summary>
private void OnArtifactActivated(EntityUid uid, ActiveScannedArtifactComponent component, ArtifactActivatedEvent args)
{
CancelScan(uid);
}
/// <summary>
/// Stops the current scan
/// </summary>
[PublicAPI]
public void CancelScan(EntityUid artifact, ActiveScannedArtifactComponent? component = null, ArtifactAnalyzerComponent? analyzer = null)
{
if (!Resolve(artifact, ref component, false))
return;
if (!Resolve(component.Scanner, ref analyzer))
return;
_audio.PlayPvs(component.ScanFailureSound, component.Scanner, AudioParams.Default.WithVolume(3f));
RemComp<ActiveArtifactAnalyzerComponent>(component.Scanner);
if (analyzer.Console != null)
UpdateUserInterface(analyzer.Console.Value);
RemCompDeferred(artifact, component);
}
/// <summary>
/// Finishes the current scan.
/// </summary>
[PublicAPI]
public void FinishScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
{
if (!Resolve(uid, ref component, ref active))
return;
component.ReadyToPrint = true;
_audio.PlayPvs(component.ScanFinishedSound, uid);
component.LastAnalyzedArtifact = active.Artifact;
UpdateAnalyzerInformation(uid, component);
RemComp<ActiveScannedArtifactComponent>(active.Artifact);
RemComp(uid, active);
if (component.Console != null)
UpdateUserInterface(component.Console.Value);
}
[PublicAPI]
public void PauseScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
{
if (!Resolve(uid, ref component, ref active) || active.AnalysisPaused)
return;
active.AnalysisPaused = true;
// As we pause, we store what was already completed.
active.AccumulatedRunTime = (_timing.CurTime - active.StartTime) + active.AccumulatedRunTime;
if (Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
[PublicAPI]
public void ResumeScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
{
if (!Resolve(uid, ref component, ref active) || !active.AnalysisPaused)
return;
active.StartTime = _timing.CurTime;
active.AnalysisPaused = false;
if (Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
private void OnItemPlaced(EntityUid uid, ArtifactAnalyzerComponent component, ref ItemPlacedEvent args)
{
if (component.Console != null && Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
private void OnItemRemoved(EntityUid uid, ArtifactAnalyzerComponent component, ref ItemRemovedEvent args)
{
// Scanners shouldn't give permanent remove vision to an artifact, and the scanned artifact doesn't have any
// component to track analyzers that have scanned it for removal if the artifact gets deleted.
// So we always clear this on removal.
component.LastAnalyzedArtifact = null;
// cancel the scan if the artifact moves off the analyzer
CancelScan(args.OtherEntity);
if (Exists(component.Console))
UpdateUserInterface(component.Console.Value);
}
private void OnAnalyzeStart(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentStartup args)
{
_receiver.SetNeedsPower(uid, true);
_ambientSound.SetAmbience(uid, true);
}
private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args)
{
_receiver.SetNeedsPower(uid, false);
_ambientSound.SetAmbience(uid, false);
}
private void OnPowerChanged(EntityUid uid, ActiveArtifactAnalyzerComponent active, ref PowerChangedEvent args)
{
if (!args.Powered)
{
PauseScan(uid, null, active);
}
else
{
ResumeScan(uid, null, active);
}
}
// Begin DeltaV - glimmer mult calculation
// Exponential curve that reaches 1 + PointGlimmerMultiplier at 1000 glimmer.
private float GetGlimmerMultiplier(ArtifactAnalyzerComponent comp)
{
return 1 + (MathF.Pow(_glimmerSystem.Glimmer / 1000f, 2f) * comp.PointGlimmerMultiplier);
}
// End DeltaV - glimmer mult calculation
}

View File

@ -1,16 +1,15 @@
using Content.Server.Body.Systems;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Server.Storage.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Shared.Body.Components;
using Content.Shared.Damage;
using Content.Shared.Power;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Shared.Collections;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@ -22,7 +21,6 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly StackSystem _stack = default!;
@ -103,7 +101,6 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
{
ContainerSystem.Insert((stack, null, null, null), crusher.OutputContainer);
}
_artifact.ForceActivateArtifact(contained);
}
if (!TryComp<BodyComponent>(contained, out var body))

View File

@ -1,63 +0,0 @@
using Content.Server.Popups;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Shared.Interaction;
using Content.Shared.Timing;
using Content.Shared.Verbs;
namespace Content.Server.Xenoarchaeology.Equipment.Systems;
public sealed class NodeScannerSystem : EntitySystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<NodeScannerComponent, BeforeRangedInteractEvent>(OnBeforeRangedInteract);
SubscribeLocalEvent<NodeScannerComponent, GetVerbsEvent<UtilityVerb>>(AddScanVerb);
}
private void OnBeforeRangedInteract(EntityUid uid, NodeScannerComponent component, BeforeRangedInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target is not {} target)
return;
if (!TryComp<ArtifactComponent>(target, out var artifact) || artifact.CurrentNodeId == null)
return;
CreatePopup(uid, target, artifact);
args.Handled = true;
}
private void AddScanVerb(EntityUid uid, NodeScannerComponent component, GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess)
return;
if (!TryComp<ArtifactComponent>(args.Target, out var artifact) || artifact.CurrentNodeId == null)
return;
var verb = new UtilityVerb()
{
Act = () =>
{
CreatePopup(uid, args.Target, artifact);
},
Text = Loc.GetString("node-scan-tooltip")
};
args.Verbs.Add(verb);
}
private void CreatePopup(EntityUid uid, EntityUid target, ArtifactComponent artifact)
{
if (TryComp(uid, out UseDelayComponent? useDelay)
&& !_useDelay.TryResetDelay((uid, useDelay), true))
return;
_popupSystem.PopupEntity(Loc.GetString("node-scan-popup",
("id", $"{artifact.CurrentNodeId}")), target);
}
}

View File

@ -1,79 +0,0 @@
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Placeable;
using Robust.Shared.Timing;
namespace Content.Server.Xenoarchaeology.Equipment.Systems;
public sealed class TraversalDistorterSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<TraversalDistorterComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<TraversalDistorterComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<TraversalDistorterComponent, ItemPlacedEvent>(OnItemPlaced);
SubscribeLocalEvent<TraversalDistorterComponent, ItemRemovedEvent>(OnItemRemoved);
}
private void OnInit(EntityUid uid, TraversalDistorterComponent component, MapInitEvent args)
{
component.NextActivation = _timing.CurTime;
}
/// <summary>
/// Switches the state of the traversal distorter between up and down.
/// </summary>
/// <param name="uid">The distorter's entity</param>
/// <param name="component">The component on the entity</param>
/// <returns>If the distorter changed state</returns>
public bool SetState(EntityUid uid, TraversalDistorterComponent component, bool isDown)
{
if (!this.IsPowered(uid, EntityManager))
return false;
if (_timing.CurTime < component.NextActivation)
return false;
component.NextActivation = _timing.CurTime + component.ActivationDelay;
component.BiasDirection = isDown ? BiasDirection.Down : BiasDirection.Up;
return true;
}
private void OnExamine(EntityUid uid, TraversalDistorterComponent component, ExaminedEvent args)
{
string examine = string.Empty;
switch (component.BiasDirection)
{
case BiasDirection.Up:
examine = Loc.GetString("traversal-distorter-desc-up");
break;
case BiasDirection.Down:
examine = Loc.GetString("traversal-distorter-desc-down");
break;
}
args.PushMarkup(examine);
}
private void OnItemPlaced(EntityUid uid, TraversalDistorterComponent component, ref ItemPlacedEvent args)
{
var bias = EnsureComp<BiasedArtifactComponent>(args.OtherEntity);
bias.Provider = uid;
}
private void OnItemRemoved(EntityUid uid, TraversalDistorterComponent component, ref ItemRemovedEvent args)
{
var otherEnt = args.OtherEntity;
if (TryComp<BiasedArtifactComponent>(otherEnt, out var bias) && bias.Provider == uid)
RemComp(otherEnt, bias);
}
}

View File

@ -1,166 +0,0 @@
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
[RegisterComponent, Access(typeof(ArtifactSystem))]
public sealed partial class ArtifactComponent : Component
{
/// <summary>
/// Every node contained in the tree
/// </summary>
[DataField("nodeTree"), ViewVariables]
public List<ArtifactNode> NodeTree = new();
/// <summary>
/// The current node the artifact is on.
/// </summary>
[DataField("currentNodeId"), ViewVariables]
public int? CurrentNodeId;
#region Node Tree Gen
/// <summary>
/// Minimum number of nodes to generate, inclusive
/// </summary>
[DataField("nodesMin")]
public int NodesMin = 3;
/// <summary>
/// Maximum number of nodes to generate, exclusive
/// </summary>
[DataField("nodesMax")]
public int NodesMax = 9;
#endregion
/// <summary>
/// Cooldown time between artifact activations (in seconds).
/// </summary>
[DataField("timer"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan CooldownTime = TimeSpan.FromSeconds(5);
/// <summary>
/// Is this artifact under some suppression device?
/// f true, will ignore all trigger activations attempts.
/// </summary>
[DataField("isSuppressed"), ViewVariables(VVAccess.ReadWrite)]
public bool IsSuppressed;
/// <summary>
/// The last time the artifact was activated.
/// </summary>
[DataField("lastActivationTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastActivationTime;
/// <summary>
/// A multiplier applied to the calculated point value
/// to determine the monetary value of the artifact
/// </summary>
[DataField("priceMultiplier"), ViewVariables(VVAccess.ReadWrite)]
public float PriceMultiplier = 0.05f;
/// <summary>
/// The base amount of research points for each artifact node.
/// </summary>
[DataField("pointsPerNode"), ViewVariables(VVAccess.ReadWrite)]
public int PointsPerNode = 6500;
/// <summary>
/// Research points which have been "consumed" from the theoretical max value of the artifact.
/// </summary>
[DataField("consumedPoints"), ViewVariables(VVAccess.ReadWrite)]
public int ConsumedPoints;
/// <summary>
/// A multiplier that is raised to the power of the average depth of a node.
/// Used for calculating the research point value of an artifact node.
/// </summary>
[DataField("pointDangerMultiplier"), ViewVariables(VVAccess.ReadWrite)]
public float PointDangerMultiplier = 1.35f;
/// <summary>
/// The sound that plays when an artifact is activated
/// </summary>
[DataField("activationSound")]
public SoundSpecifier ActivationSound = new SoundCollectionSpecifier("ArtifactActivation")
{
Params = new()
{
Variation = 0.1f,
Volume = 3f
}
};
[DataField("activateActionEntity")] public EntityUid? ActivateActionEntity;
}
/// <summary>
/// A single "node" of an artifact that contains various data about it.
/// </summary>
[DataDefinition]
public sealed partial class ArtifactNode : ICloneable
{
/// <summary>
/// A numeric id corresponding to each node.
/// </summary>
[DataField("id"), ViewVariables]
public int Id;
/// <summary>
/// how "deep" into the node tree. used for generation and price/value calculations
/// </summary>
[DataField("depth"), ViewVariables]
public int Depth;
/// <summary>
/// A list of surrounding nodes. Used for tree traversal
/// </summary>
[DataField("edges"), ViewVariables]
public HashSet<int> Edges = new();
/// <summary>
/// Whether or not the node has been entered
/// </summary>
[DataField("discovered"), ViewVariables(VVAccess.ReadWrite)]
public bool Discovered;
/// <summary>
/// The trigger for the node
/// </summary>
[DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables]
public string Trigger = default!;
/// <summary>
/// Whether or not the node has been triggered
/// </summary>
[DataField("triggered"), ViewVariables(VVAccess.ReadWrite)]
public bool Triggered;
/// <summary>
/// The effect when the node is activated
/// </summary>
[DataField("effect", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactEffectPrototype>), required: true), ViewVariables]
public string Effect = default!;
/// <summary>
/// Used for storing cumulative information about nodes
/// </summary>
[DataField("nodeData"), ViewVariables]
public Dictionary<string, object> NodeData = new();
public object Clone()
{
return new ArtifactNode
{
Id = Id,
Depth = Depth,
Edges = Edges,
Discovered = Discovered,
Trigger = Trigger,
Triggered = Triggered,
Effect = Effect,
NodeData = NodeData
};
}
}

View File

@ -1,49 +0,0 @@
using Content.Server.Actions;
using Content.Server.Popups;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
public partial class ArtifactSystem
{
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[ValidatePrototypeId<EntityPrototype>] private const string ArtifactActivateActionId = "ActionArtifactActivate";
/// <summary>
/// Used to add the artifact activation action (hehe), which lets sentient artifacts activate themselves,
/// either through admemery or the sentience effect.
/// </summary>
public void InitializeActions()
{
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ArtifactComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<ArtifactComponent, ArtifactSelfActivateEvent>(OnSelfActivate);
}
private void OnMapInit(EntityUid uid, ArtifactComponent component, MapInitEvent args)
{
RandomizeArtifact(uid, component);
_actions.AddAction(uid, ref component.ActivateActionEntity, ArtifactActivateActionId);
}
private void OnRemove(EntityUid uid, ArtifactComponent component, ComponentRemove args)
{
_actions.RemoveAction(uid, component.ActivateActionEntity);
}
private void OnSelfActivate(EntityUid uid, ArtifactComponent component, ArtifactSelfActivateEvent args)
{
if (component.CurrentNodeId == null)
return;
var curNode = GetNodeFromId(component.CurrentNodeId.Value, component).Id;
_popup.PopupEntity(Loc.GetString("activate-artifact-popup-self", ("node", curNode)), uid, uid);
TryActivateArtifact(uid, uid, component);
args.Handled = true;
}
}

View File

@ -1,71 +0,0 @@
using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
public partial class ArtifactSystem
{
[Dependency] private readonly IConsoleHost _conHost = default!;
public void InitializeCommands()
{
_conHost.RegisterCommand("forceartifactnode", "Forces an artifact to traverse to a given node", "forceartifacteffect <uid> <node ID>",
ForceArtifactNode,
ForceArtifactNodeCompletions);
_conHost.RegisterCommand("getartifactmaxvalue", "Reports the maximum research point value for a given artifact", "forceartifacteffect <uid>",
GetArtifactMaxValue);
}
[AdminCommand(AdminFlags.Fun)]
private void ForceArtifactNode(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError("Argument length must be 2");
return;
}
if (!NetEntity.TryParse(args[0], out var uidNet) || !TryGetEntity(uidNet, out var uid) || !int.TryParse(args[1], out var id))
return;
if (!TryComp<ArtifactComponent>(uid, out var artifact))
return;
if (artifact.NodeTree.FirstOrDefault(n => n.Id == id) is { } node)
{
EnterNode(uid.Value, ref node);
}
}
private CompletionResult ForceArtifactNodeCompletions(IConsoleShell shell, string[] args)
{
if (args.Length == 2 && NetEntity.TryParse(args[0], out var uidNet) && TryGetEntity(uidNet, out var uid))
{
if (TryComp<ArtifactComponent>(uid, out var artifact))
{
return CompletionResult.FromHintOptions(artifact.NodeTree.Select(s => s.Id.ToString()), "<node id>");
}
}
return CompletionResult.Empty;
}
[AdminCommand(AdminFlags.Debug)]
private void GetArtifactMaxValue(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length != 1)
shell.WriteError("Argument length must be 1");
if (!NetEntity.TryParse(args[0], out var uidNet) || !TryGetEntity(uidNet, out var uid))
return;
if (!TryComp<ArtifactComponent>(uid, out var artifact))
return;
var pointSum = GetResearchPointValue(uid.Value, artifact, true);
shell.WriteLine($"Max point value for {ToPrettyString(uid.Value)} with {artifact.NodeTree.Count} nodes: {pointSum}");
}
}

View File

@ -1,244 +0,0 @@
using System.Linq;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Whitelist;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using JetBrains.Annotations;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
public sealed partial class ArtifactSystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private const int MaxEdgesPerNode = 4;
private readonly HashSet<int> _usedNodeIds = new();
/// <summary>
/// Generate an Artifact tree with fully developed nodes.
/// </summary>
/// <param name="artifact"></param>
/// <param name="allNodes"></param>
/// <param name="nodesToCreate">The amount of nodes it has.</param>
private void GenerateArtifactNodeTree(EntityUid artifact, List<ArtifactNode> allNodes, int nodesToCreate)
{
if (nodesToCreate < 1)
{
Log.Error($"nodesToCreate {nodesToCreate} is less than 1. Aborting artifact tree generation.");
return;
}
_usedNodeIds.Clear();
var uninitializedNodes = new List<ArtifactNode> { new(){ Id = GetValidNodeId() } };
var createdNodes = 1;
while (uninitializedNodes.Count > 0)
{
var node = uninitializedNodes[0];
uninitializedNodes.Remove(node);
node.Trigger = GetRandomTrigger(artifact, ref node);
node.Effect = GetRandomEffect(artifact, ref node);
var maxChildren = _random.Next(1, MaxEdgesPerNode - 1);
for (var i = 0; i < maxChildren; i++)
{
if (nodesToCreate <= createdNodes)
{
break;
}
var child = new ArtifactNode {Id = GetValidNodeId(), Depth = node.Depth + 1};
node.Edges.Add(child.Id);
child.Edges.Add(node.Id);
uninitializedNodes.Add(child);
createdNodes++;
}
allNodes.Add(node);
}
}
private int GetValidNodeId()
{
var id = _random.Next(100, 1000);
while (_usedNodeIds.Contains(id))
{
id = _random.Next(100, 1000);
}
_usedNodeIds.Add(id);
return id;
}
//yeah these two functions are near duplicates but i don't
//want to implement an interface or abstract parent
private string GetRandomTrigger(EntityUid artifact, ref ArtifactNode node)
{
var allTriggers = _prototype.EnumeratePrototypes<ArtifactTriggerPrototype>()
.Where(x => _whitelistSystem.IsWhitelistPassOrNull(x.Whitelist, artifact) &&
_whitelistSystem.IsBlacklistFailOrNull(x.Blacklist, artifact)).ToList();
var validDepth = allTriggers.Select(x => x.TargetDepth).Distinct().ToList();
var weights = GetDepthWeights(validDepth, node.Depth);
var selectedRandomTargetDepth = GetRandomTargetDepth(weights);
var targetTriggers = allTriggers
.Where(x => x.TargetDepth == selectedRandomTargetDepth).ToList();
return _random.Pick(targetTriggers).ID;
}
private string GetRandomEffect(EntityUid artifact, ref ArtifactNode node)
{
var allEffects = _prototype.EnumeratePrototypes<ArtifactEffectPrototype>()
.Where(x => _whitelistSystem.IsWhitelistPassOrNull(x.Whitelist, artifact) &&
_whitelistSystem.IsBlacklistFailOrNull(x.Blacklist, artifact)).ToList();
var validDepth = allEffects.Select(x => x.TargetDepth).Distinct().ToList();
var weights = GetDepthWeights(validDepth, node.Depth);
var selectedRandomTargetDepth = GetRandomTargetDepth(weights);
var targetEffects = allEffects
.Where(x => x.TargetDepth == selectedRandomTargetDepth).ToList();
return _random.Pick(targetEffects).ID;
}
/// <remarks>
/// The goal is that the depth that is closest to targetDepth has the highest chance of appearing.
/// The issue is that we also want some variance, so levels that are +/- 1 should also have a
/// decent shot of appearing. This function should probably get some tweaking at some point.
/// </remarks>
private Dictionary<int, float> GetDepthWeights(IEnumerable<int> depths, int targetDepth)
{
// this function is just a normal distribution with a
// mean of target depth and standard deviation of 0.75
var weights = new Dictionary<int, float>();
foreach (var d in depths)
{
var w = 10f / (0.75f * MathF.Sqrt(2 * MathF.PI)) * MathF.Pow(MathF.E, -MathF.Pow((d - targetDepth) / 0.75f, 2));
weights.Add(d, w);
}
return weights;
}
/// <summary>
/// Uses a weighted random system to get a random depth.
/// </summary>
private int GetRandomTargetDepth(Dictionary<int, float> weights)
{
var sum = weights.Values.Sum();
var accumulated = 0f;
var rand = _random.NextFloat() * sum;
foreach (var (key, weight) in weights)
{
accumulated += weight;
if (accumulated >= rand)
{
return key;
}
}
return _random.Pick(weights.Keys); //shouldn't happen
}
/// <summary>
/// Enter a node: attach the relevant components
/// </summary>
private void EnterNode(EntityUid uid, ref ArtifactNode node, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentNodeId != null)
{
ExitNode(uid, component);
}
component.CurrentNodeId = node.Id;
var trigger = _prototype.Index<ArtifactTriggerPrototype>(node.Trigger);
var effect = _prototype.Index<ArtifactEffectPrototype>(node.Effect);
var allComponents = effect.Components.Concat(effect.PermanentComponents).Concat(trigger.Components);
foreach (var (name, entry) in allComponents)
{
var reg = _componentFactory.GetRegistration(name);
if (node.Discovered && EntityManager.HasComponent(uid, reg.Type))
{
// Don't re-add permanent components unless this is the first time you've entered this node
if (effect.PermanentComponents.ContainsKey(name))
continue;
EntityManager.RemoveComponent(uid, reg.Type);
}
var comp = (Component)_componentFactory.GetComponent(reg);
var temp = (object)comp;
_serialization.CopyTo(entry.Component, ref temp);
EntityManager.RemoveComponent(uid, temp!.GetType());
EntityManager.AddComponent(uid, (Component)temp!);
}
node.Discovered = true;
RaiseLocalEvent(uid, new ArtifactNodeEnteredEvent(component.CurrentNodeId.Value));
}
/// <summary>
/// Exit a node: remove the relevant components.
/// </summary>
private void ExitNode(EntityUid uid, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentNodeId == null)
return;
var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
var trigger = _prototype.Index<ArtifactTriggerPrototype>(currentNode.Trigger);
var effect = _prototype.Index<ArtifactEffectPrototype>(currentNode.Effect);
var entityPrototype = MetaData(uid).EntityPrototype;
var toRemove = effect.Components.Keys.Concat(trigger.Components.Keys).ToList();
foreach (var name in toRemove)
{
// if the entity prototype contained the component originally
if (entityPrototype?.Components.TryGetComponent(name, out var entry) ?? false)
{
var comp = (Component)_componentFactory.GetComponent(name);
var temp = (object)comp;
_serialization.CopyTo(entry, ref temp);
EntityManager.RemoveComponent(uid, temp!.GetType());
EntityManager.AddComponent(uid, (Component)temp);
continue;
}
EntityManager.RemoveComponentDeferred(uid, _componentFactory.GetRegistration(name).Type);
}
component.CurrentNodeId = null;
}
[PublicAPI]
public ArtifactNode GetNodeFromId(int id, ArtifactComponent component)
{
return component.NodeTree.First(x => x.Id == id);
}
[PublicAPI]
public ArtifactNode GetNodeFromId(int id, IEnumerable<ArtifactNode> nodes)
{
return nodes.First(x => x.Id == id);
}
}

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