Compare commits

..
17 Commits
Author SHA1 Message Date
pre-commit-ci[bot] 4702b34eb5 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-08 20:59:17 +00:00
Vanessa 4d28fabc9d Added discord linking channel to config 2026-06-08 15:36:12 -05:00
Vanessa 474a6d358b More cleanup 2026-06-08 14:55:45 -05:00
Vanessa 5e6b27a860 Minor cleanup 2026-06-08 14:39:05 -05:00
Vanessa 008b84902a Added some consistency to the comments. Removed some more vestigial RMC stuff. 2026-06-08 14:23:32 -05:00
Vanessa 52d887faff Removed some RMC references. 2026-06-08 14:13:54 -05:00
Vanessa 097d8282c8 Removed references to DeltaV so our downstreams will like us more. 2026-06-08 13:56:29 -05:00
Vanessa 8919a88ea0 Some more cleanup 2026-06-08 12:40:11 -05:00
Vanessa 2dcf965e41 Discord Bot changes. Made it only a slash command. Might do more, but probably good enough. 2026-06-08 12:35:36 -05:00
Vanessa c776571860 Added documenation and improved discord bot for local development. Parameterized guild and context. 2026-06-08 10:11:08 -05:00
Vanessa 78dd74e5c1 Consolidate migrations. Allow bot to be given different DbContexts so I can test locally with Sqlite 2026-06-08 08:38:30 -05:00
Vanessa b3d15cdfd4 Update bot to dotnet10 2026-06-07 17:34:29 -05:00
DrSmugleaf 92883eb3ff Update Content.DiscordBot to .NET 9, wrap update loop in a try catch 2026-06-07 17:34:04 -05:00
DrSmugleaf 53773326df Fix expired code discord message not being ephemeral 2026-06-07 17:31:39 -05:00
DrSmugleaf 2281cdbf8d Make patron tiers auto refresh 2026-06-07 17:31:19 -05:00
Vanessa ff20b12532 More fixes and removing RMC specific references and perks 2026-06-07 17:31:07 -05:00
DrSmugleafandWhisper 8e62f86239 Add Discord account linking, patron perks (#2666)
Authored-by: Vanessa <vanessalouwagie@gmail.com>
Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
2026-06-07 17:00:42 -05:00
754 changed files with 75998 additions and 142181 deletions
+3
View File
@@ -312,3 +312,6 @@ Resources/MapImages
# Changelog package lock
package-lock.json
# Discord Bot Config
link-config.json
+1 -1
View File
@@ -269,7 +269,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | [[File:SL_BreakingAndEntering.png]]
! style="border: 1px solid black;" | {{anchor|Breaking and Entering}}Breaking and Entering
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To break and enter into an area where one is not authorised nor invited.
| style="border: 1px solid black;" | To break and enter into a high security area where one is not authorised nor invited, with intent to commit a crime within.
|-
| style="border: 1px solid black;" | 203
| style="border: 1px solid black;" | [[File:SL_Rioting.png]]
+18 -78
View File
@@ -73,88 +73,28 @@ If you are adding a lot of C# code, then take advantage of partial classes. Put
Otherwise, **add comments on or around any changed lines.**
### Single-Line Changes
Format should look like this.
A comment on a new imported namespace:
```cs
/* Importing Namespaces - Include optional comment if its not obvious what its being used for. */
using Content.Server._DV.Psionics.Glimmer; // DeltaV
using Content.Shared.Damage.Systems; // DeltaV - Addition of HandHeldArmor
/* Changing an upstream line - Same line as the change */
if (!TryComp<EyeComponent>(ent, out var eye) || _disabled) // DeltaV - check if disabled
/* Adding - Either same line or above the line. */
EnsureComp<PotentialPsionicComponent>(entity); // Deltav - Psionics
/* "Deleting" - Don't actually delete, just comment out and say why. This only applies to upstream code. */
// args.StatusIcons.Add(_prototype.Index(component.Icon)); // DeltaV - commented out. status icon now added above
using Content.Server.Psionics.Glimmer; // DeltaV
```
> * Its pretty obvious in the example above that importing `Content.Server._DV.Psionics.Glimmer` means we'll be interacting with glimmer so putting `// DeltaV - Add Glimmer` is needlessly redundant.
> * It's not as obvious what the `Content.Shared.Damage.Systems` namespace is used for, since its so broad, so adding a comment what feature is using it helps.
> * Actual code changes should almost always include the comment after ``// DeltaV`.
### Multi-Line Changes
Depending on how much you are editing, putting a comment on EACH line may be excessive, so if you have a larger block of code you are changing, denote it like so:
A pair of comments enclosing a block of added code:
```cs
// BEGIN DeltaV - Remove innate radio and radios from pockets
for (var i = 1; i <= 4; i++) // Arachnids have 4 pockets
private EntityUid Slice(...)
{
if (_inventory.TryGetSlotEntity(target, $"pocket{i}", out var headset) && HasComp<HeadsetComponent>(headset))
_inventory.TryUnequip(target, $"pocket{i}", true, true);
...
_transform.SetLocalRotation(sliceUid, 0);
// DeltaV - start of deep frier stuff
var slicedEv = new FoodSlicedEvent(user, uid, sliceUid);
RaiseLocalEvent(uid, ref slicedEv);
// DeltaV - end of deep frier stuff
...
}
RemComp<ActiveRadioComponent>(target); // If the zombie has an innate radio, get rid of it.
// END DeltaV
```
> * Denoting these with a BEGIN and END clearly shows they are block of code without having to read the entire comment. This makes it easier to tell when you're dealing with single-line comments versus a block with merging in conflicts.
> * Case and order of the first two words is less of a concern. `// DeltaV Begin` or `// Begin DeltaV` will work fine too.
> * Try to make your blocks as small as possible, but use your discretion.
> * If you deleting multiple lines, use line comments (``//``) if its a few lines but if its a larger block (like commenting out an entire function), it is preferable to use block comments (`/* */`).
#### Soft Exceptions to the Multi-Line "Rules"
Some multi-line changes can use a single-line comment in certain scenarios. But if you are UNSURE, just use `// BEGIN DeltaV` and `// END DeltaV` comments like the previous section does and it'll be fine.
I'll give some examples.
```cs
/* This change comments out 3 lines but only needs a single line comment because commenting out the if statement implies that its logic will be commented out too. */
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
// {
// _profileEditor.RefreshTraits();
// }
/* Same principle here. This adds two lines but the if statement implies the next line so commenting both lines isn't really needed. */
if (_flight.IsFlying(entity.Owner)) // DeltaV - Harpy Flight
return true;
```
### New Methods or Component Variables
Sometimes, you'll need to implement a whole new method or component variable and instead of wrapping it in `// BEGIN DeltaV` and `// END DeltaV`, you can just denote that it's a DeltaV function in the summary block before the function. This denotes the WHOLE function as a DeltaV addition.
```cs
/* New Method Example */
/// <summary>
/// DeltaV - Handle revealing ninja if cloaked when attacked by a hitscan attack.
/// </summary>
private void OnNinjaAttacked(Entity<SpaceNinjaComponent> ent, ref DamageChangedEvent args)
{
...
}
/* New Component Variable Example */
/// <summary>
/// DeltaV - If disabled the action will not disable when no charges remain. Use if you want to handle no charges differently.
/// </summary>
[DataField]
public bool DisableWhenEmpty = true;
```
In short:
* Use `// BEGIN DeltaV` and `// END Delta` to denote a *block* of changes.
* Keep blocks as small as possible.
* Use `// DeltaV` on or before the line if its not a block of changes.
* Use exceptions when they make sense.
### Changing Upstream Localization Fluent .ftl files
**Move all changed locale strings to a new DeltaV file** - use a `.ftl` file in the `_DV` folder. Comment out the old strings in the upstream file, and explain that they were moved.
@@ -164,10 +104,10 @@ Example:
Commented out old string in `Resources\Locale\en-US\xenoarchaeology\artifact-analyzer.ftl`
```
# DeltaV - moved to _DV file
# analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
# }[/color][/font]
#analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
#}[/color][/font]
```
The new version of the string in `Resources\Locale\en-US\_DV\xenoarchaeology\artifact-analyzer.ftl`
@@ -237,12 +237,11 @@ internal sealed class AdminNameOverlay : Overlay
}
// DeltaV - SSD Time START
if (_entityManager.TryGetComponent<SSDIndicatorComponent>(entity, out var ssdIndicator)
&& ssdIndicator.SsdSince is {} ssdSince)
if (_entityManager.TryGetComponent<SSDIndicatorComponent>(entity, out var ssdIndicator) && ssdIndicator.IsSSD)
{
color = Color.MediumPurple;
color.A = alpha;
var ssdText = Loc.GetString("admin-overlay-ssd-time", ("time", (_timing.CurTime - ssdSince).ToString("%hh':'mm':'ss")));
var ssdText = Loc.GetString("admin-overlay-ssd-time", ("time", (_timing.CurTime - ssdIndicator.SsdSince).ToString("%hh':'mm':'ss")));
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, ssdText, uiScale, color);
currentOffset += lineoffset;
}
-10
View File
@@ -7,7 +7,6 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Input;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Radio.Components; // DeltaV - Client-side Radio Colors
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -159,15 +158,6 @@ namespace Content.Client.Examine
// since there's probably one open already if it's coming in from the server.
var entity = GetEntity(ev.EntityUid);
// BEGIN DeltaV - Client-Side Radio Colors
// This *seems* a bit hacky, but we basically don't want to overwrite what's been pushed on the client-side,
// because the server will send us the server-side colors (default). I've yet to find a scenario where the
// server and client don't match, so until then, this is probably fine, if a little hacky.
if (HasComp<EncryptionKeyHolderComponent>(entity) || HasComp<EncryptionKeyComponent>(entity))
return;
// END DeltaV
OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget);
UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs, getVerbs: false);
}
+13
View File
@@ -1,4 +1,5 @@
using Content.Client.Changelog;
using Content.Client._DV.AccountLinking; // DeltaV
using Content.Client.UserInterface.Systems.EscapeMenu;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.CCVar;
@@ -48,6 +49,18 @@ namespace Content.Client.Info
changelogButton.OnPressed += args => UserInterfaceManager.GetUIController<ChangelogUIController>().ToggleWindow();
buttons.AddChild(changelogButton);
// BEGIN DeltaV - Discord / Patreon Account Linking
var linkAccount = UserInterfaceManager.GetUIController<LinkAccountUIController>();
var linkAccountButton = new Button
{
Text = Loc.GetString("ui-link-discord-account"),
};
linkAccountButton.OnPressed += _ => linkAccount.ToggleWindow();
buttons.AddChild(linkAccountButton);
AddInfoButton("ui-patreon", CCVars.InfoLinksPatreon);
// END DeltaV
void AddInfoButton(string loc, CVarDef<string> cVar)
{
var button = new Button { Text = Loc.GetString(loc) };
+2
View File
@@ -1,3 +1,4 @@
using Content.Client._DV.AccountLinking; // DeltaV
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -62,6 +63,7 @@ namespace Content.Client.IoC
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
collection.Register<ClientsidePlaytimeTrackingManager>();
collection.Register<LinkAccountManager>(); // DeltaV - Discord Linking
}
}
}
+2
View File
@@ -1,3 +1,4 @@
using Content.Client._DV.AccountLinking; // DeltaV
using Content.Client.Audio;
using Content.Client.GameTicking.Managers;
using Content.Client.LateJoin;
@@ -29,6 +30,7 @@ namespace Content.Client.Lobby
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly ClientsidePlaytimeTrackingManager _playtimeTracking = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!; // DeltaV - Discord Linking
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
@@ -1,4 +1,5 @@
using System.Linq;
using Content.Client._DV.AccountLinking; // DeltaV
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Inventory;
@@ -38,6 +39,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly JobRequirementsManager _requirements = default!;
[Dependency] private readonly MarkingManager _markings = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!; // DeltaV - Discord Account Linking
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
@@ -75,6 +77,8 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_configurationManager.OnValueChanged(CCVars.GameRoleLoadoutTimers, _ => RefreshProfileEditor());
_configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
_linkAccount.Updated += RefreshProfileEditor; // DeltaV - Discord Account Linking
}
private LobbyCharacterPreviewPanel? GetLobbyPreview()
@@ -10,7 +10,6 @@
<tabs:AudioTab Name="AudioTab" />
<tabs:AccessibilityTab Name="AccessibilityTab" />
<tabs:AdminOptionsTab Name="AdminOptionsTab" />
<dtabs:RadioTab Name="RadioTab" /> <!-- DeltaV - Client-side Radio Options -->
<dtabs:DeltaTab Name="DeltaTab" /> <!-- DeltaV -->
</TabContainer>
</DefaultWindow>
@@ -21,8 +21,7 @@ namespace Content.Client.Options.UI
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-accessibility"));
Tabs.SetTabTitle(5, Loc.GetString("ui-options-tab-admin"));
Tabs.SetTabTitle(6, Loc.GetString("ui-options-tab-radio")); // DeltaV - Radio Channel Colors
Tabs.SetTabTitle(7, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
Tabs.SetTabTitle(6, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
UpdateTabs();
}
@@ -37,7 +36,6 @@ namespace Content.Client.Options.UI
AccessibilityTab.Control.ReloadValues();
AudioTab.Control.ReloadValues();
AdminOptionsTab.Control.ReloadValues();
RadioTab.Control.ReloadValues(); // DeltaV - Radio Channel Colors
DeltaTab.Control.ReloadValues(); // DeltaV
}
}
@@ -20,7 +20,6 @@
<CheckBox Name="ShowLoocAboveHeadCheckBox" Text="{Loc 'ui-options-show-looc-on-head'}" />
<CheckBox Name="FancySpeechBubblesCheckBox" Text="{Loc 'ui-options-fancy-speech'}" />
<CheckBox Name="FancyNameBackgroundsCheckBox" Text="{Loc 'ui-options-fancy-name-background'}" />
<CheckBox Name="ChatFollowButton" Text="{Loc 'ui-options-chat-follow-button'}" />
<Label Text="{Loc 'ui-options-general-cursor'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="ShowHeldItemCheckBox" Text="{Loc 'ui-options-show-held-item'}" />
@@ -1,4 +1,5 @@
using System.Linq;
using Content.Client._DV.AccountLinking; // DeltaV
using Content.Client.UserInterface.Screens;
using Content.Shared.CCVar;
using Content.Shared._EE.CCVars; // EE - chat stack
@@ -17,6 +18,7 @@ public sealed partial class MiscTab : Control
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!; // DeltaV - Discord Account Linking
public MiscTab()
{
@@ -48,7 +50,7 @@ public sealed partial class MiscTab : Control
// Channel can be null in replays so.
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
ShowOocPatronColor.Visible = _playerManager.LocalSession?.Channel?.UserData.PatronTier is { };
ShowOocPatronColor.Visible = _linkAccount.Tier != null; // DeltaV - Discord Account Linking
Control.AddOptionDropDown(CVars.InterfaceTheme, DropDownHudTheme, themeEntries);
Control.AddOptionDropDown(CCVars.UILayout, DropDownHudLayout, layoutEntries);
@@ -63,7 +65,6 @@ public sealed partial class MiscTab : Control
Control.AddOptionCheckBox(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox);
Control.AddOptionCheckBox(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox);
Control.AddOptionCheckBox(CCVars.StaticStorageUI, StaticStorageUI);
Control.AddOptionCheckBox(CCVars.InterfaceChatFollowButton, ChatFollowButton);
Control.Initialize();
}
+111
View File
@@ -159,6 +159,35 @@ namespace Content.Client.Stylesheets
public static readonly Color ChatBackgroundColor = Color.FromHex("#25252ADD");
// DeltaV - AAC button variables
public static readonly string CommandButtonClass = "CommandButton";
public static readonly string EngineeringButtonClass = "EngineeringButton";
public static readonly string EpistemicsButtonClass = "EpistemicsButton";
public static readonly string JusticeButtonClass = "JusticeButton";
public static readonly string LogisticsButtonClass = "LogisticsButton";
public static readonly string MedicalButtonClass = "MedicalButton";
public static readonly string SecurityButtonClass = "SecurityButton";
public static readonly string ServiceButtonClass = "ServiceButton";
// DeltaV - AAC button colors
public static readonly Color CommandButtonColorDefault = Color.FromHex("#404A58");
public static readonly Color CommandColorHovered = Color.FromHex("#4F587B");
public static readonly Color EngineeringButtonColorDefault = Color.FromHex("#77684B");
public static readonly Color EngineeringColorHovered = Color.FromHex("#776D71");
public static readonly Color EpistemicsButtonColorDefault = Color.FromHex("#6F5973");
public static readonly Color EpistemicsColorHovered = Color.FromHex("#71638E");
public static readonly Color LogisticsButtonColorDefault = Color.FromHex("#61503A");
public static readonly Color LogisticsColorHovered = Color.FromHex("#675C64");
public static readonly Color JusticeButtonColorDefault = Color.FromHex("#4F3D4C");
public static readonly Color JusticeColorHovered = Color.FromHex("#5C4B5A");
public static readonly Color MedicalButtonColorDefault = Color.FromHex("#49687D");
public static readonly Color MedicalColorHovered = Color.FromHex("#556E95");
public static readonly Color SecurityButtonColorDefault = Color.FromHex("#724449");
public static readonly Color SecurityColorHovered = Color.FromHex("#745370");
public static readonly Color ServiceButtonColorDefault = Color.FromHex("#607952");
public static readonly Color ServiceColorHovered = Color.FromHex("#667A76");
// End DeltaV
public const string StyleClassCrewManifestGender = "CrewManifestGender"; //Delta-V - Manifest pronouns
// i'm not sure what the missing symbols were referencing, and this is getting obseleted anyway so:
@@ -1648,6 +1677,88 @@ namespace Content.Client.Stylesheets
BackgroundColor = FancyTreeSelectedRowColor,
}),
// DeltaV - AAC button styles
Element<ContainerButton>()
.Class(CommandButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, CommandButtonColorDefault),
Element<ContainerButton>()
.Class(CommandButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, CommandColorHovered),
Element<ContainerButton>()
.Class(EngineeringButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, EngineeringButtonColorDefault),
Element<ContainerButton>()
.Class(EngineeringButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, EngineeringColorHovered),
Element<ContainerButton>()
.Class(EpistemicsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, EpistemicsButtonColorDefault),
Element<ContainerButton>()
.Class(EpistemicsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, EpistemicsColorHovered),
Element<ContainerButton>()
.Class(LogisticsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, LogisticsButtonColorDefault),
Element<ContainerButton>()
.Class(LogisticsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, LogisticsColorHovered),
Element<ContainerButton>()
.Class(MedicalButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, MedicalButtonColorDefault),
Element<ContainerButton>()
.Class(MedicalButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, MedicalColorHovered),
Element<ContainerButton>()
.Class(SecurityButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, SecurityButtonColorDefault),
Element<ContainerButton>()
.Class(SecurityButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, SecurityColorHovered),
Element<ContainerButton>()
.Class(ServiceButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, ServiceButtonColorDefault),
Element<ContainerButton>()
.Class(ServiceButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, ServiceColorHovered),
Element<ContainerButton>()
.Class(JusticeButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, JusticeButtonColorDefault),
Element<ContainerButton>()
.Class(JusticeButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, JusticeColorHovered),
// End DeltaV
// Silicon law edit ui
Element<Label>().Class(SiliconLawContainer.StyleClassSiliconLawPositionLabel)
.Prop(Label.StylePropertyFontColor, NanoGold),
@@ -1,7 +1,6 @@
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions; // DeltaV - Client-Side Radio Channel Colors
using Content.Client.Administration.Managers;
using Content.Client.Chat;
using Content.Client.Chat.Managers;
@@ -840,38 +839,8 @@ public sealed partial class ChatUIController : UIController
}
}
/// <summary>
/// DeltaV - Regex expression to match on the chat message text in order to replace it with another color.
/// </summary>
private static readonly Regex RadioChatColorRegex = new(@"color=#?([a-fA-F0-9]{8})", RegexOptions.Compiled);
/// <summary>
/// DeltaV - Replaces the channel color hex code in a message with the client-side hex color
/// overridden by <see cref="Content.Client._DV.Radio.RadioChannelColorSystem"/> .
/// </summary>
/// <param name="message">The message to find and replace the color hex code in.</param>
/// <param name="channelProtoId">The proto ID of the channel. Basically the replacement color.</param>
/// <returns>The message with the color hex code replaced, if it exists.</returns>
private string ReplaceRadioChannelColorFromServer(string message, string channelProtoId)
{
if (_prototypeManager.Resolve<RadioChannelPrototype>(channelProtoId, out var channel))
{
return RadioChatColorRegex.Replace(message, $"color={channel.Color.ToHex()}");
}
return message;
}
public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
{
// BEGIN DeltaV
// Needs to happen before ANY other color or pattern matching so that we don't screw up the other colors in the message.
if (msg.Channel == ChatChannel.Radio && msg.RadioChannelProtoId is { } channel)
{
msg.WrappedMessage = ReplaceRadioChannelColorFromServer(msg.WrappedMessage, channel);
}
// END DeltaV
// color the name unless it's something like "the old man"
if ((msg.Channel == ChatChannel.Local || msg.Channel == ChatChannel.Whisper) && _chatNameColorsEnabled)
{
@@ -12,19 +12,6 @@ namespace Content.Client._DV.AACTablet.UI;
[CommonSheetlet]
public sealed class AACSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, IButtonConfig
{
private static readonly (string StyleClass, Color Normal, Color Hover)[] SubjectButtonColors =
[
// Department colors
("CommandButton", Color.FromHex("#404A58"), Color.FromHex("#4F587B")),
("EngineeringButton", Color.FromHex("#77684B"), Color.FromHex("#776D71")),
("EpistemicsButton", Color.FromHex("#6F5973"), Color.FromHex("#71638E")),
("JusticeButton", Color.FromHex("#4F3D4C"), Color.FromHex("#5C4B5A")),
("LogisticsButton", Color.FromHex("#61503A"), Color.FromHex("#675C64")),
("MedicalButton", Color.FromHex("#49687D"), Color.FromHex("#556E95")),
("SecurityButton", Color.FromHex("#724449"), Color.FromHex("#745370")),
("ServiceButton", Color.FromHex("#607952"), Color.FromHex("#667A76")),
];
public override StyleRule[] GetRules(T sheet, object config)
{
// Create style boxes
@@ -94,21 +81,6 @@ public sealed class AACSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, I
.Prop(Control.StylePropertyModulateSelf, Color.DarkRed),
]);
SubjectColorRules(rules);
return rules.ToArray();
}
// AAC - Makes sure colors are applied to subject buttons
private static void SubjectColorRules(List<StyleRule> rules)
{
foreach (var (styleClass, normal, hover) in SubjectButtonColors)
{
rules.AddRange([
E<ContainerButton>().Class(ContainerButton.StyleClassButton).Class(styleClass)
.PseudoNormal().Prop(Control.StylePropertyModulateSelf, normal),
E<ContainerButton>().Class(ContainerButton.StyleClassButton).Class(styleClass)
.PseudoHovered().Prop(Control.StylePropertyModulateSelf, hover),
]);
}
}
}
@@ -218,7 +218,7 @@ public sealed partial class AACWindow : FancyWindow
MaxSize = new Vector2(ButtonWidth, ButtonWidth),
ClipText = false,
HorizontalExpand = true,
StyleClasses = { ContainerButton.StyleClassButton, styleClass }
StyleClasses = { styleClass }
};
var buttonLabel = new RichTextLabel
@@ -0,0 +1,48 @@
using Content.Shared._DV.AccountLinking;
using Robust.Shared.Network;
namespace Content.Client._DV.AccountLinking;
public sealed class LinkAccountManager : IPostInjectInit
{
[Dependency] private readonly INetManager _net = default!;
private readonly List<SharedPatron> _allPatrons = [];
public SharedPatronTier? Tier { get; private set; }
public bool Linked { get; private set; }
public event Action<Guid>? CodeReceived;
public event Action? Updated;
private void OnCode(LinkAccountCodeMsg message)
{
CodeReceived?.Invoke(message.Code);
}
private void OnStatus(LinkAccountStatusMsg ev)
{
Tier = ev.Patron?.Tier;
Linked = ev.Patron?.Linked ?? false;
Updated?.Invoke();
}
private void OnPatronList(PatronListMsg ev)
{
_allPatrons.Clear();
_allPatrons.AddRange(ev.Patrons);
}
public IReadOnlyList<SharedPatron> GetPatrons()
{
return _allPatrons;
}
void IPostInjectInit.PostInject()
{
_net.RegisterNetMessage<LinkAccountCodeMsg>(OnCode);
_net.RegisterNetMessage<LinkAccountRequestMsg>();
_net.RegisterNetMessage<LinkAccountStatusMsg>(OnStatus);
_net.RegisterNetMessage<PatronListMsg>(OnPatronList);
}
}
@@ -0,0 +1,5 @@
using Content.Shared._DV.AccountLinking;
namespace Content.Client._DV.AccountLinking;
public sealed class LinkAccountSystem : EntitySystem;
@@ -0,0 +1,107 @@
using Content.Client.Lobby.UI;
using Content.Client.Message;
using Content.Shared._DV.CCVars;
using Content.Shared._DV.AccountLinking;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Client._DV.AccountLinking;
public sealed class LinkAccountUIController : UIController
{
[Dependency] private readonly IClipboardManager _clipboard = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IUriOpener _uriOpener = default!;
private LinkAccountWindow? _window;
private TimeSpan _disableUntil;
private Guid _code;
public override void Initialize()
{
_linkAccount.CodeReceived += OnCode;
_linkAccount.Updated += OnUpdated;
}
private void OnCode(Guid code)
{
_code = code;
if (_window == null)
return;
_window.CopyButton.Disabled = false;
}
private void OnUpdated()
{
if (UIManager.ActiveScreen is not LobbyGui gui)
return;
}
public void ToggleWindow()
{
if (_window == null)
{
_window = new LinkAccountWindow();
_window.OnClose += () => _window = null;
_window.Label.SetMarkupPermissive($"{Loc.GetString("ui-link-discord-account-text")}");
if (_linkAccount.Linked)
_window.Label.SetMarkupPermissive($"{Loc.GetString("ui-link-discord-account-already-linked")}\n\n{Loc.GetString("ui-link-discord-account-text")}");
_window.CopyButton.OnPressed += _ =>
{
_clipboard.SetText(_code.ToString());
_window.CopyButton.Text = Loc.GetString("ui-link-discord-account-copied");
_window.CopyButton.Disabled = true;
_disableUntil = _timing.RealTime.Add(TimeSpan.FromSeconds(3));
};
var messageLink = _config.GetCVar(DCCVars.DiscordAccountLinkingMessageLink);
if (string.IsNullOrEmpty(messageLink))
{
_window.LinkButton.Visible = false;
_window.CopyButton.RemoveStyleClass("OpenRight");
}
else
{
_window.LinkButton.Visible = true;
_window.LinkButton.OnPressed += _ => _uriOpener.OpenUri(messageLink);
_window.CopyButton.AddStyleClass("OpenRight");
}
_window.OpenCentered();
if (_code == default)
_window.CopyButton.Disabled = true;
_net.ClientSendMessage(new LinkAccountRequestMsg());
return;
}
_window.Close();
_window = null;
}
public override void FrameUpdate(FrameEventArgs args)
{
if (_window == null)
return;
var time = _timing.RealTime;
if (_disableUntil != default && time > _disableUntil)
{
_disableUntil = default;
_window.CopyButton.Text = Loc.GetString("ui-link-discord-account-copy");
_window.CopyButton.Disabled = false;
}
}
}
@@ -0,0 +1,16 @@
<linkAccount:LinkAccountWindow
xmlns="https://spacestation14.io"
xmlns:linkAccount="clr-namespace:Content.Client._DV.AccountLinking"
Title="{Loc 'ui-link-discord-account'}">
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="Label" Access="Public" Margin="0 0 0 5" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center"
HorizontalExpand="True">
<Button Name="CopyButton" Access="Public" StyleClasses="OpenRight"
Text="{Loc 'ui-link-discord-account-copy'}" HorizontalExpand="True" />
<Button Name="LinkButton" Access="Public" StyleClasses="OpenLeft"
Text="{Loc 'ui-link-discord-account-open-channel'}"
HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
</linkAccount:LinkAccountWindow>
@@ -0,0 +1,15 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.AccountLinking;
[GenerateTypedNameReferences]
public sealed partial class LinkAccountWindow : DefaultWindow
{
public LinkAccountWindow()
{
RobustXamlLoader.Load(this);
}
}
@@ -1,87 +0,0 @@
<tabs:RadioTab xmlns="https://spacestation14.io"
xmlns:tabs="clr-namespace:Content.Client._DV.Options.UI.Tabs"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Margin="8">
<!-- Common -->
<Label Text="{Loc 'ui-options-radio-header-common'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioCommonColorSlider"
Example="{Loc 'ui-options-radio-example-common'}"/>
<Button Name="ResetCommonColorButton" Text="{Loc 'ui-options-radio-reset-common'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Command -->
<Label Text="{Loc 'ui-options-radio-header-command'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioCommandColorSlider"
Example="{Loc 'ui-options-radio-example-command'}"/>
<Button Name="ResetCommandColorButton" Text="{Loc 'ui-options-radio-reset-command'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Security -->
<Label Text="{Loc 'ui-options-radio-header-security'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioSecurityColorSlider"
Example="{Loc 'ui-options-radio-example-security'}"/>
<Button Name="ResetSecurityColorButton" Text="{Loc 'ui-options-radio-reset-security'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Medical -->
<Label Text="{Loc 'ui-options-radio-header-medical'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioMedicalColorSlider"
Example="{Loc 'ui-options-radio-example-medical'}"/>
<Button Name="ResetMedicalColorButton" Text="{Loc 'ui-options-radio-reset-medical'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Engineering -->
<Label Text="{Loc 'ui-options-radio-header-engineering'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioEngineeringColorSlider"
Example="{Loc 'ui-options-radio-example-engineering'}"/>
<Button Name="ResetEngineeringColorButton" Text="{Loc 'ui-options-radio-reset-engineering'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Science -->
<Label Text="{Loc 'ui-options-radio-header-science'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioScienceColorSlider"
Example="{Loc 'ui-options-radio-example-science'}"/>
<Button Name="ResetScienceColorButton" Text="{Loc 'ui-options-radio-reset-science'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Cargo -->
<Label Text="{Loc 'ui-options-radio-header-cargo'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioCargoColorSlider"
Example="{Loc 'ui-options-radio-example-cargo'}"/>
<Button Name="ResetCargoColorButton" Text="{Loc 'ui-options-radio-reset-cargo'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Service -->
<Label Text="{Loc 'ui-options-radio-header-service'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioServiceColorSlider"
Example="{Loc 'ui-options-radio-example-service'}"/>
<Button Name="ResetServiceColorButton" Text="{Loc 'ui-options-radio-reset-service'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Binary -->
<Label Text="{Loc 'ui-options-radio-header-binary'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioBinaryColorSlider"
Example="{Loc 'ui-options-radio-example-binary'}"/>
<Button Name="ResetBinaryColorButton" Text="{Loc 'ui-options-radio-reset-binary'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- DeltaV - Justice -->
<Label Text="{Loc 'ui-options-radio-header-justice'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioJusticeColorSlider"
Example="{Loc 'ui-options-radio-example-justice'}"/>
<Button Name="ResetJusticeColorButton" Text="{Loc 'ui-options-radio-reset-justice'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- DeltaV - Prison -->
<Label Text="{Loc 'ui-options-radio-header-prison'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioPrisonColorSlider"
Example="{Loc 'ui-options-radio-example-prison'}"/>
<Button Name="ResetPrisonColorButton" Text="{Loc 'ui-options-radio-reset-prison'}" MaxWidth="300" Margin="0 4 0 0" />
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
</tabs:RadioTab>
@@ -1,55 +0,0 @@
using Content.Client.Options.UI;
using Content.Shared._DV.CCVars;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class RadioTab : Control
{
[Dependency] private readonly IConfigurationManager _config = default!; // DeltaV
public RadioTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
// Having so many color sliders on one page is a lot. We should only really try
// to have common/departmental channels here, or it just becomes WAAAAY too much.
Control.AddOptionColorSlider(DCCVars.RadioColorCommon, RadioCommonColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorCommand, RadioCommandColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorSecurity, RadioSecurityColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorMedical, RadioMedicalColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorEngineering, RadioEngineeringColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorScience, RadioScienceColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorCargo, RadioCargoColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorService, RadioServiceColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorBinary, RadioBinaryColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorJustice, RadioJusticeColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorPrison, RadioPrisonColorSlider);
ResetCommonColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorCommon);
ResetCommandColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorCommand);
ResetSecurityColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorSecurity);
ResetMedicalColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorMedical);
ResetEngineeringColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorEngineering);
ResetScienceColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorScience);
ResetCargoColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorCargo);
ResetServiceColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorService);
ResetBinaryColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorBinary);
ResetJusticeColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorJustice);
ResetPrisonColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorPrison);
Control.Initialize();
}
private void OnResetColorButton(CVarDef<string> cvar)
{
_config.SetCVar(cvar, cvar.DefaultValue);
Control.ReloadValues();
}
}
@@ -0,0 +1,6 @@
using Content.Shared._DV.Psionics.Systems.PsionicPowers;
namespace Content.Client._DV.Psionics.Systems.PsionicPowers;
// This does nothing here. The code is all in the shared/server version.
public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem;
@@ -1,49 +0,0 @@
using Content.Shared._DV.CCVars;
using Content.Shared.Radio;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Radio;
public sealed partial class RadioChannelColorSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
// Feels like there should be a better way to do this but I'd rather not add a client-side cvar ID or reference
// to a prototype in Content.Shared, since I don't want people thinking they can ever use it on the server-side.
private static readonly ProtoId<RadioChannelPrototype> CommonChannelProto = "Common";
private static readonly ProtoId<RadioChannelPrototype> CommandChannelProto = "Command";
private static readonly ProtoId<RadioChannelPrototype> SecurityChannelProto = "Security";
private static readonly ProtoId<RadioChannelPrototype> MedicalChannelProto = "Medical";
private static readonly ProtoId<RadioChannelPrototype> EngineeringChannelProto = "Engineering";
private static readonly ProtoId<RadioChannelPrototype> ScienceChannelProto = "Science";
private static readonly ProtoId<RadioChannelPrototype> CargoChannelProto = "Supply";
private static readonly ProtoId<RadioChannelPrototype> ServiceChannelProto = "Service";
private static readonly ProtoId<RadioChannelPrototype> BinaryChannelProto = "Binary";
private static readonly ProtoId<RadioChannelPrototype> JusticeChannelProto = "Justice";
private static readonly ProtoId<RadioChannelPrototype> PrisonChannelProto = "Prison";
public override void Initialize()
{
base.Initialize();
_config.OnValueChanged(DCCVars.RadioColorCommon, (value) => { SetChannelColor(CommonChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorCommand, (value) => { SetChannelColor(CommandChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorSecurity, (value) => { SetChannelColor(SecurityChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorMedical, (value) => { SetChannelColor(MedicalChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorEngineering, (value) => { SetChannelColor(EngineeringChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorScience, (value) => { SetChannelColor(ScienceChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorCargo, (value) => { SetChannelColor(CargoChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorService, (value) => { SetChannelColor(ServiceChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorBinary, (value) => { SetChannelColor(BinaryChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorJustice, (value) => { SetChannelColor(JusticeChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorPrison, (value) => { SetChannelColor(PrisonChannelProto, value); }, true);
}
private void SetChannelColor(ProtoId<RadioChannelPrototype> channel, string newColor)
{
if (Color.TryParse(newColor, out var color))
_proto.Index(channel).Color = color;
}
}
@@ -108,70 +108,6 @@ public sealed partial class TraitCategory : BoxContainer
{
CategoryPointsLabel.Visible = false;
}
// Lock unselected entries whenever the category cap is reached so the
// player gets a clear visual cue instead of silently failing to equip.
UpdateCategoryFullState();
}
/// <summary>
/// Propagates category-cap lock state to every entry.
/// <para>
/// Selected entries are never locked - they are the traits filling the cap
/// and must stay interactive so the player can deselect them.
/// </para>
/// <para>
/// Unselected entries are locked when:
/// <list type="bullet">
/// <item><see cref="TraitCategoryPrototype.MaxTraits"/> is defined and already reached, OR</item>
/// <item><see cref="TraitCategoryPrototype.MaxPoints"/> is defined and this specific
/// trait's cost would push spending over the limit.</item>
/// </list>
/// </para>
/// </summary>
private void UpdateCategoryFullState()
{
var traitCapReached = _category.MaxTraits.HasValue && SelectedCount >= _category.MaxTraits.Value;
foreach (var (_, entry) in _traitEntries)
{
if (entry.IsSelected)
{
// Selected traits are never locked by category state.
entry.SetLockedByCategory(false);
continue;
}
var locked = traitCapReached;
// Even when the trait count cap hasn't been hit, lock this specific entry
// if adding its cost would exceed the category's point budget.
if (!locked && _category.MaxPoints.HasValue)
locked = PointsSpent + entry.TraitCost > _category.MaxPoints.Value;
entry.SetLockedByCategory(locked);
}
}
/// <summary>
/// Called by <see cref="TraitsTab"/> whenever the player's global point budget changes.
/// Locks any unselected entry whose cost the player can no longer afford.
/// <para>
/// Free (0-cost) and negative-cost traits are never locked - they cost nothing or
/// actually give points back. Selected traits are also never locked here; they
/// were affordable when picked and must stay interactive to be deselected.
/// </para>
/// </summary>
public void UpdateGlobalPointsLock(int remainingPoints)
{
foreach (var (_, entry) in _traitEntries)
{
// A selected trait is always unlocked here - same reasoning as category cap:
// it was paid for at selection time and must stay interactive.
// Traits that cost nothing or give points back are never locked by budget.
var shouldLock = !entry.IsSelected && entry.TraitCost > remainingPoints;
entry.SetLockedByPoints(shouldLock);
}
}
public void SetTraitSelected(ProtoId<TraitPrototype> traitId, bool selected)
@@ -235,11 +171,11 @@ public sealed partial class TraitCategory : BoxContainer
/// Updates condition states for all trait entries based on current job/species.
/// Traits that don't meet conditions are disabled but still visible.
/// </summary>
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
{
foreach (var (_, entry) in _traitEntries)
{
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences, selectedTraits);
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences);
}
// Update stats since some traits may have been deselected
+37 -158
View File
@@ -27,9 +27,6 @@ public sealed partial class TraitEntry : PanelContainer
private bool _isUpdating;
private readonly List<string> _failedConditionTooltips = new();
private bool _isLockedByCategory;
private bool _isLockedByPoints;
public TraitEntry(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
@@ -62,17 +59,9 @@ public sealed partial class TraitEntry : PanelContainer
foreach (var condition in _trait.Conditions)
{
if (condition is TraitDependencyCondition depCond)
{
// Use GetTooltips() to get individual lines without duplication
tooltips.AddRange(depCond.GetTooltips(_prototype, _loc));
}
else
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
}
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
}
if (tooltips.Count > 0)
@@ -102,92 +91,39 @@ public sealed partial class TraitEntry : PanelContainer
/// <summary>
/// Updates whether conditions are met based on current job/species.
/// </summary>
public void UpdateConditionsMet(
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
public void UpdateConditionsMet(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
{
_failedConditionTooltips.Clear();
MeetsConditions = true;
foreach (var condition in _trait.Conditions)
{
bool result;
if (condition is TraitDependencyCondition depCond)
var result = condition switch
{
result = CheckDependencyCondition(depCond, selectedTraits);
// CheckDependencyCondition adds its own tooltips directly
}
else
{
result = condition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences),
_ => true,
};
// Apply inversion for non-dependency conditions
result ^= condition.Invert;
if (!result)
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
}
}
// Apply inversion
result ^= condition.Invert;
if (!result)
{
MeetsConditions = false;
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
}
}
UpdateDisabledState();
}
/// <summary>
/// Checks a TraitDependencyCondition and adds failure tooltips directly.
/// Returns true if the condition passes.
/// </summary>
private bool CheckDependencyCondition(TraitDependencyCondition condition, IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
var passed = true;
foreach (var conflict in condition.Conflicts)
{
if (selectedTraits == null || !selectedTraits.Contains(conflict))
continue;
if (_prototype.TryIndex(conflict, out var conflictProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-conflict",
("trait", _loc.GetString(conflictProto.Name))));
}
passed = false;
}
foreach (var required in condition.Requires)
{
if (selectedTraits != null && selectedTraits.Contains(required))
continue;
if (_prototype.TryIndex(required, out var requiredProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-required",
("trait", _loc.GetString(requiredProto.Name))));
}
passed = false;
}
return passed;
}
private bool CheckSpeciesCondition(IsSpeciesCondition condition, ProtoId<SpeciesPrototype>? speciesId)
{
if (!_prototype.TryIndex(speciesId, out var species))
@@ -223,12 +159,7 @@ public sealed partial class TraitEntry : PanelContainer
return antagPreferences.Contains(condition.Antag);
}
private bool CheckAnyOfCondition(
AnyOfCondition condition,
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
{
if (condition.Conditions.Count == 0)
return false;
@@ -236,27 +167,19 @@ public sealed partial class TraitEntry : PanelContainer
// Return true if ANY child condition evaluates to true
foreach (var childCondition in condition.Conditions)
{
bool result;
if (childCondition is TraitDependencyCondition depCond)
var result = childCondition switch
{
result = CheckDependencyCondition(depCond, selectedTraits);
}
else
{
result = childCondition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences), // Recursive!
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
_ => true,
};
result ^= childCondition.Invert;
}
// Apply child's inversion
result ^= childCondition.Invert;
// If any child passes, the AnyOf passes
if (result)
@@ -267,50 +190,16 @@ public sealed partial class TraitEntry : PanelContainer
return false;
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the category's trait or
/// points-cap state changes. Locking only applies to <em>unselected</em>
/// entries - the selected traits are the ones filling the cap and must
/// remain interactive so the player can deselect them.
/// </summary>
public void SetLockedByCategory(bool locked)
{
if (_isLockedByCategory == locked)
return;
_isLockedByCategory = locked;
UpdateDisabledState();
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the player's global point budget
/// changes. Locks unselected entries whose cost the player can no longer afford.
/// Free (0-cost) and negative-cost traits are never locked by this.
/// </summary>
public void SetLockedByPoints(bool locked)
{
if (_isLockedByPoints == locked)
return;
_isLockedByPoints = locked;
UpdateDisabledState();
}
private void UpdateDisabledState()
{
var isSelected = TraitCheckbox.Pressed;
var conditionLocked = !MeetsConditions;
var categoryLocked = _isLockedByCategory && !isSelected;
var pointsLocked = _isLockedByPoints && !isSelected;
var isDisabled = conditionLocked || categoryLocked || pointsLocked;
if (isDisabled)
if (!MeetsConditions)
{
// Hide checkbox, show lock icon
TraitCheckbox.Visible = false;
LockIcon.Visible = true;
if (isSelected && conditionLocked)
// Deselect if conditions no longer met
if (TraitCheckbox.Pressed)
{
_isUpdating = true;
TraitCheckbox.Pressed = false;
@@ -322,24 +211,14 @@ public sealed partial class TraitEntry : PanelContainer
// Add disabled styling
AddStyleClass("TraitsEntryDisabled");
// Tooltip priority for conditions: condition failures > category full > insufficient points.
if (conditionLocked && _failedConditionTooltips.Count > 0)
// Update tooltip to show failed conditions
if (_failedConditionTooltips.Count > 0)
{
var tooltipText = Loc.GetString("trait-conditions-not-met-tooltip",
("requirements", string.Join("\n", _failedConditionTooltips)));
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (categoryLocked)
{
var tooltipText = Loc.GetString("trait-category-full-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (pointsLocked)
{
var tooltipText = Loc.GetString("trait-insufficient-points-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
}
else
{
+8 -18
View File
@@ -154,18 +154,14 @@ public sealed partial class TraitsTab : BoxContainer
}
}
// Check conflicts via TraitDependencyCondition
var depCondition = trait.Conditions.OfType<Content.Shared._DV.Traits.Conditions.TraitDependencyCondition>().FirstOrDefault();
if (depCondition != null)
// Check conflicts
foreach (var conflict in trait.Conflicts)
{
foreach (var conflict in depCondition.Conflicts)
{
if (!_selectedTraits.Contains(conflict))
continue;
if (!_selectedTraits.Contains(conflict))
continue;
RevertTraitToggle(traitId);
return;
}
RevertTraitToggle(traitId);
return;
}
_selectedTraits.Add(traitId);
@@ -180,7 +176,6 @@ public sealed partial class TraitsTab : BoxContainer
}
UpdateGlobalStats();
UpdateAllConditions();
UpdateCategoryStats(trait.Category);
OnTraitsChanged?.Invoke(_selectedTraits);
}
@@ -216,7 +211,7 @@ public sealed partial class TraitsTab : BoxContainer
// If parent width is 0 (not laid out yet), defer until layout happens
if (parentWidth > 0)
{
GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
GlobalPointsBar.SetWidth = (int)((parentWidth - 2 ) * percentage);
_awaitingLayoutUpdate = false;
}
else if (!_awaitingLayoutUpdate)
@@ -240,11 +235,6 @@ public sealed partial class TraitsTab : BoxContainer
> 0f => "TraitsProgressBarLow",
_ => "TraitsProgressBarEmpty"
});
foreach (var (_, categoryUi) in _categoryUis)
{
categoryUi.UpdateGlobalPointsLock(remainingPoints);
}
}
private void OnProgressBarParentResized()
@@ -291,7 +281,7 @@ public sealed partial class TraitsTab : BoxContainer
foreach (var (_, categoryUi) in _categoryUis)
{
// If some fork wants to use the top selected job as well, just add that to the UpdateConditions method in the editor
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences, _selectedTraits);
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences);
}
RecalculateStats();
@@ -20,11 +20,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
private const string AnimateKey = "particle-animation";
private static readonly Dictionary<StellarInteractionParticleType, EntProtoId> InteractionParticleIds = new ()
{
{ StellarInteractionParticleType.Use, "StellarInteractionParticleUse" },
{ StellarInteractionParticleType.Pull, "StellarInteractionParticlePull" },
};
private static readonly EntProtoId InteractionParticleId = "StellarInteractionParticle";
public override void Initialize()
{
@@ -42,11 +38,6 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
if (!Exists(performer) || !Exists(target))
return;
if (ev.Type == StellarInteractionParticleType.Pull)
{
(performer, target) = (target, performer);
}
var performerXform = Transform(performer);
var targetXform = Transform(target);
if (performerXform.MapID == MapId.Nullspace || targetXform.MapID == MapId.Nullspace)
@@ -56,7 +47,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
return;
var performerTargetDelta = targetXform.LocalPosition - performerXform.LocalPosition;
var particle = Spawn(InteractionParticleIds[ev.Type], performerXform.Coordinates);
var particle = Spawn(InteractionParticleId, performerXform.Coordinates);
if (used is { } usedEntity && Exists(usedEntity) && TryComp<SpriteComponent>(usedEntity, out var usedSprite))
{
@@ -67,16 +58,10 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
}
var spriteColor = Comp<SpriteComponent>(particle).Color;
var animation = ev.Type switch
{
StellarInteractionParticleType.Use => GetUseAnimation(performerTargetDelta, spriteColor),
StellarInteractionParticleType.Pull => GetPullAnimation(performerTargetDelta, spriteColor),
_ => throw new ArgumentOutOfRangeException(nameof(ev), $"Interaction particle event has unknown particle type {ev.Type}"),
};
_animation.Play(particle, animation, AnimateKey);
_animation.Play(particle, GetAnimation(performerTargetDelta, spriteColor), AnimateKey);
}
private Animation GetUseAnimation(Vector2 endOffset, Color color)
private Animation GetAnimation(Vector2 endOffset, Color color)
{
var startRotation = _random.NextAngle(Angle.FromDegrees(-40), Angle.FromDegrees(40));
var endRotation = Angle.Zero;
@@ -141,44 +126,4 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
},
};
}
private Animation GetPullAnimation(Vector2 endOffset, Color color)
{
var rotationLength = TimeSpan.FromMilliseconds(8f * (1000f / 12f));
var startOffset = new Vector2();
var offsetLength = TimeSpan.FromMilliseconds(4f * (1000f / 12f));
var endColor = color.WithAlpha(0f);
return new Animation
{
Length = rotationLength,
AnimationTracks =
{
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Offset),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startOffset, 0f),
new AnimationTrackProperty.KeyFrame(endOffset, (float)rotationLength.TotalSeconds, Easings.InOutCirc),
},
},
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Color),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(color, 0f),
new AnimationTrackProperty.KeyFrame(color, (float)offsetLength.TotalSeconds),
new AnimationTrackProperty.KeyFrame(endColor, (float)rotationLength.TotalSeconds, Easings.InOutCirc),
},
},
},
};
}
}
+279
View File
@@ -0,0 +1,279 @@
using System.Collections.Immutable;
using System.Reflection;
using Content.DiscordBot.Modules;
using Content.Server.Database;
using Discord;
using Discord.Commands;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
namespace Content.DiscordBot;
public sealed class CommandHandler(DiscordSocketClient client, InteractionService interaction, ServerDbContext db, Config config)
{
private ImmutableDictionary<ulong, PatronTier>? _patronTiers;
private ImmutableArray<PatronTier> _tierPriority;
private Task? _refreshPatronsTask;
public int Running = 1;
public async Task InstallCommandsAsync()
{
var patronTiers = await db.PatronTiers.ToListAsync();
_tierPriority = [..patronTiers.OrderBy(t => t.Priority)];
_patronTiers = patronTiers.ToImmutableDictionary(t => t.DiscordRole, t => t);
client.Ready += ReadyAsync;
client.SlashCommandExecuted += HandleCommandAsync;
client.ButtonExecuted += HandleButtonAsync;
client.ModalSubmitted += HandleModalAsync;
client.GuildMemberUpdated += HandleGuildMemberUpdated;
// If you get a log that says '[ERROR]' here, you probably don't have the corrent intents
await client.LoginAsync(TokenType.Bot, config.Token);
await client.StartAsync();
interaction.AddModalInfo<LinkAccountModal>();
_refreshPatronsTask = Task.Run(async () => await RefreshPatrons());
}
private async Task ReadyAsync()
{
await interaction.AddModulesAsync(Assembly.GetEntryAssembly(), null);
await interaction.RegisterCommandsToGuildAsync(config.Guild);
}
private async Task HandleGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> old, SocketGuildUser user)
{
if (_patronTiers == null)
return;
var rolesChanged = !old.HasValue || old.Value.Roles.Count != user.Roles.Count || !old.Value.Roles.SequenceEqual(user.Roles);
if (!rolesChanged)
return;
var wasPatron = old.HasValue || old.Value.Roles.Any(r => _patronTiers.ContainsKey(r.Id));
var isPatron = user.Roles.Any(r => _patronTiers.ContainsKey(r.Id));
if (wasPatron && !isPatron)
{
var linked = await db.DiscordLinkedAccounts
.Include(l => l.Player)
.ThenInclude(p => p.Patron)
.ThenInclude(p => p!.Tier)
.Where(l => l.Player.Patron != null)
.FirstOrDefaultAsync(p => p.DiscordId == user.Id);
if (linked?.Player.Patron is { } patron)
{
db.Patrons.Remove(patron);
await db.SaveChangesAsync();
await Logger.Info($"Removed patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName} with tier {patron.Tier.Name}");
}
return;
}
if (isPatron)
{
foreach (var tier in _tierPriority)
{
if (user.Roles.Any(r => r.Id == tier.DiscordRole))
{
var linked = await db.DiscordLinkedAccounts
.Include(l => l.Player)
.ThenInclude(p => p.Patron)
.FirstOrDefaultAsync(p => p.DiscordId == user.Id);
if (linked?.Player is not { } player)
return;
player.Patron ??= db.Patrons.Add(new Patron { PlayerId = player.UserId }).Entity;
player.Patron.TierId = tier.Id;
await db.SaveChangesAsync();
await Logger.Info($"Updated patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName} with tier {tier.Name}");
}
}
}
}
private async Task HandleCommandAsync(SocketSlashCommand message)
{
if (message.GuildId != config.Guild)
return;
// Create a WebSocket-based command context based on the message
var context = new SocketInteractionContext(client, message);
// Execute the command with the command context we just
// created, along with the service provider for precondition checks.
await interaction.ExecuteCommandAsync(context, null);
}
private async Task HandleButtonAsync(SocketMessageComponent component)
{
switch (component.Data.CustomId)
{
case "link-ss14-account":
await component.RespondWithModalAsync<LinkAccountModal>("link-ss14-account");
break;
}
}
private async Task HandleModalAsync(SocketModal modal)
{
switch (modal.Data.CustomId)
{
case "link-ss14-account":
if (modal.GuildId is not { } guildId)
break;
var codeStr = modal.Data.Components.First(c => c.CustomId == "account_code").Value.Trim();
if (string.IsNullOrWhiteSpace(codeStr))
break;
await modal.DeferAsync(true);
if (!Guid.TryParse(codeStr, out var code))
{
await modal.FollowupAsync($"{codeStr} isn't a valid code! Get one in-game from the lobby at the top left of the screen.", ephemeral: true);
}
var author = modal.User;
var authorId = author.Id;
var discord = await db.DiscordAccounts
.Include(d => d.LinkedAccount)
.ThenInclude(l => l.Player)
.ThenInclude(p => p.Patron)
.FirstOrDefaultAsync(a => a.Id == authorId);
var codes = await db.DiscordLinkingCodes
.Include(l => l.Player)
.ThenInclude(player => player.Patron)
.FirstOrDefaultAsync(p => p.Code == code);
if (codes == null)
{
await modal.FollowupAsync($"No player found with code {codeStr}, join the game server and get another code before trying again, or ask for help in another channel.", ephemeral: true);
break;
}
if (codes.CreationTime < DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)))
{
await modal.FollowupAsync($"Code {codeStr} were generated too long ago, join the game server and get another code before trying again.", ephemeral: true);
}
if (discord?.LinkedAccount is { } linked)
{
if (linked.Player.Patron is { } patron)
db.Patrons.Remove(patron);
linked.Player.Patron = null;
db.DiscordLinkedAccounts.Remove(linked);
}
discord ??= db.DiscordAccounts.Add(new DiscordAccount { Id = authorId }).Entity;
discord.LinkedAccount = db.DiscordLinkedAccounts.Add(new DiscordLinkedAccount { Discord = discord }).Entity;
discord.LinkedAccount.Player = codes.Player;
var roles = client.GetGuild(guildId).GetUser(authorId).Roles.Select(r => r.Id).ToArray();
var tiers = await db.PatronTiers
.Where(t => roles.Contains(t.DiscordRole))
.ToListAsync();
if (tiers.Count == 0)
{
discord.LinkedAccount.Player.Patron = null;
}
else
{
tiers.Sort((a, b) => a.Priority.CompareTo(b.Priority));
var tier = tiers[0];
discord.LinkedAccount.Player.Patron = db.Patrons.Add(new Patron { Tier = tier }).Entity;
discord.LinkedAccount.Player.Patron.Tier = tier;
}
db.DiscordLinkedAccountLogs.Add(new DiscordLinkedAccountLogs
{
Discord = discord,
Player = discord.LinkedAccount.Player,
});
db.ChangeTracker.DetectChanges();
await db.SaveChangesAsync();
var msg = $"Linked SS14 account with name {codes.Player.LastSeenUserName}";
if (codes.Player.Patron != null)
msg += $" and tier {codes.Player.Patron.Tier.Name}";
await modal.FollowupAsync(msg, ephemeral: true);
break;
}
}
private async Task RefreshPatrons()
{
while (Interlocked.CompareExchange(ref Running, 1, 1) == 1)
{
try
{
var patrons = await db.DiscordLinkedAccounts
.Include(l => l.Player)
.ThenInclude(p => p.Patron)
.ThenInclude(p => p!.Tier)
.ToListAsync();
foreach (var linked in patrons)
{
try
{
var user = await client.Rest.GetGuildUserAsync(config.Guild, linked.DiscordId);
if (user == null)
{
if (linked.Player.Patron != null)
{
linked.Player.Patron = null;
await Logger.Info($"Removed patron {linked.DiscordId}:{linked.Player.LastSeenUserName}");
}
continue;
}
var isPatron = false;
foreach (var tier in _tierPriority)
{
if (user.RoleIds.Contains(tier.DiscordRole))
{
isPatron = true;
if (linked.Player.Patron?.Tier.DiscordRole == tier.DiscordRole)
break;
linked.Player.Patron ??= db.Patrons.Add(new Patron { PlayerId = linked.PlayerId })
.Entity;
linked.Player.Patron.TierId = tier.Id;
await Logger.Info($"Updated patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName} with tier {tier.Name}");
break;
}
}
if (!isPatron && linked.Player.Patron != null)
{
linked.Player.Patron = null;
await Logger.Info($"Removed patron {user.Username}:{linked.DiscordId}:{linked.Player.LastSeenUserName}");
}
}
catch (Exception e)
{
await Logger.Error($"Error updating patron with discord id {linked.DiscordId} and player id {linked.PlayerId}", e);
}
}
await db.SaveChangesAsync();
await Task.Delay(60000); // 60 seconds
}
catch (Exception e)
{
await Logger.Error("Error refreshing patrons", e);
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace Content.DiscordBot;
public sealed class Config
{
public string Token { get; set; } = string.Empty;
public string DatabaseString { get; set; } = string.Empty;
public string DatabaseContext { get; set; } = "postgres";
/// <summary>
/// Discord "guild" ID (which is just the server ID). Default is DeltaV's server ID.
/// </summary>
public ulong Guild { get; set; } = 968983104247185448UL;
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputPath>..\bin\Content.DiscordBot\</OutputPath>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<RootNamespace>Content.DiscordBot</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.15.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />
</ItemGroup>
</Project>
+21
View File
@@ -0,0 +1,21 @@
using Discord;
namespace Content.DiscordBot;
public static class Logger
{
public static Task Log(LogMessage msg)
{
return Console.Out.WriteLineAsync($"[{msg.Severity.ToString().ToUpper()}] {msg.Message}");
}
public static Task Info(string msg)
{
return Console.Out.WriteLineAsync($"[INFO] {msg}");
}
public static Task Error(string msg, Exception e)
{
return Console.Out.WriteLineAsync($"[ERROR] {msg}\n{e}");
}
}
@@ -0,0 +1,18 @@
using Discord;
using Discord.Interactions;
namespace Content.DiscordBot.Modules;
public sealed class AccountLinkingModule : InteractionModuleBase<SocketInteractionContext>
{
[SlashCommand("discordlink", "Creates the message with the linking button.")]
[RequireTeam]
public Task CreateAsync()
{
var component = new ComponentBuilder()
.WithButton("Link your SS14 account here!", "link-ss14-account")
.Build();
return RespondAsync(String.Empty, components: component);
}
}
@@ -0,0 +1,13 @@
using Discord.Interactions;
namespace Content.DiscordBot.Modules;
public class LinkAccountModal : IModal
{
public string Title => "Link SS14 Account";
[InputLabel("SS14 Linking Code (Top Left in the Lobby)")]
[RequiredInput]
[ModalTextInput("account_code")]
public string Code { get; set; } = string.Empty;
}
+87
View File
@@ -0,0 +1,87 @@
using System.Text.Json;
using Content.DiscordBot;
using Content.Server.Database;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
var client = new DiscordSocketClient(new DiscordSocketConfig { GatewayIntents = GatewayIntents.GuildMembers });
client.Log += Logger.Log;
Console.Out.WriteLine($"[INIT] Working Directory: {Directory.GetCurrentDirectory()}");
var config = new Config();
if (File.Exists("link-config.json"))
{
Console.Out.WriteLine("[INIT] link-config.json found. Deserializing.");
config = await JsonSerializer.DeserializeAsync<Config>(File.OpenRead("link-config.json")) ?? new Config();
}
#if DEBUG
if (Environment.GetEnvironmentVariable("DISCORD_TOKEN") is { } envToken)
config.Token = envToken;
if (Environment.GetEnvironmentVariable("DATABASE_STRING") is { } dbString)
config.DatabaseString = dbString;
if (Environment.GetEnvironmentVariable("DATABASE_CONTEXT") is { } context)
config.DatabaseContext = context;
if (Environment.GetEnvironmentVariable("GUILD") is { } guildId)
{
try
{
config.Guild = ulong.Parse("guildId");
}
catch (Exception ex)
{
Console.Out.WriteLine($"[INIT] Unable to parse guild ID. Defaulting to {config.Guild}. Exception: {ex.GetType()} {ex.Message}");
}
}
#endif
if (string.IsNullOrWhiteSpace(config.Token))
throw new ArgumentException("[INIT] No Discord Bot token found.");
if (string.IsNullOrWhiteSpace(config.DatabaseString))
throw new ArgumentException("[INIT] No database connection string found.");
// Override for local sqlite development
var usePostgres = true;
if (!string.IsNullOrWhiteSpace(config.DatabaseContext) && config.DatabaseContext.Equals("sqlite"))
usePostgres = false;
Console.Out.WriteLine($"[INIT] Guild ID: {config.Guild}");
ServerDbContext db;
if (usePostgres)
{
Console.Out.WriteLine("[INIT] Connecting to postgres...");
var builder = new DbContextOptionsBuilder<PostgresServerDbContext>();
builder.UseNpgsql(config.DatabaseString);
db = new PostgresServerDbContext(builder.Options);
// await db.Database.MigrateAsync();
}
else
{
Console.Out.WriteLine("[INIT] Connecting to sqlite...");
var builder = new DbContextOptionsBuilder<SqliteServerDbContext>();
builder.UseSqlite(new SqliteConnection(config.DatabaseString));
db = new SqliteServerDbContext(builder.Options);
}
Console.Out.WriteLine("[INIT] Database connection successful.");
var interaction = new InteractionService(client);
var handler = new CommandHandler(client, interaction, db, config);
AppDomain.CurrentDomain.ProcessExit += (_, _) => Interlocked.Decrement(ref handler.Running);
Console.Out.WriteLine("[INIT] Starting Discord Bot.");
await handler.InstallCommandsAsync();
// Block this task until the program is closed.
await Task.Delay(-1);
+32
View File
@@ -0,0 +1,32 @@
# Content.DiscordBot
### Configuration (Debug)
* `DISCORD_TOKEN`: Discord application token provided @ `https://discord.com/developers/applications/<APP_ID>/bot`
* `DATABASE_STRING`: Connection string to the database
* For local development, you'll want it to be `Data Source=./bin/Content.Server/data/preferences.db` if you're using sqlite.
* `DATABASE_CONTEXT`: Default is `postgres`. If testing locally with sqlite, use `sqlite`.
* `GUILD`: The Discord Server ID. DeltaV's discord is `968983104247185448`.
* You can find this by turning Developer mode on in Discord, right-clicking on a server, and copying the server ID.
### Configuration (Production)
Requires a `link-config.json` file in the working directory.
```json
{
"Token": "DISCORD_BOT_TOKEN",
"DatabaseString": "DB_CONNECTION_STRING"
}
```
Here's a quick bash command to create it in your current working directory. Windows users will have to fend for themselves.
```bash
cat << EOF > link-config.json
{
"Token": "DISCORD_BOT_TOKEN",
"DatabaseString": "DB_CONNECTION_STRING"
}
EOF
```
You can also override `Guild` and `DatabaseContext` if you wanted to by including those JSON properties.
@@ -245,7 +245,6 @@ namespace Content.IntegrationTests.Tests
"TimedDespawnDetailed", // DeltaV
// makes an announcement on mapInit.
"AnnounceOnSpawn",
"ESTimedDespawn" // DeltaV
};
Assert.That(server.CfgMan.GetCVar(CVars.NetPVS), Is.False);
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Content.Server.Database;
[Table("discord_accounts")]
public sealed class DiscordAccount
{
[Key]
public ulong Id { get; set; }
public DiscordLinkedAccount LinkedAccount { get; set; } = default!;
public List<DiscordLinkedAccountLogs> LinkedAccountLogs { get; set; } = default!;
}
[Table("discord_linked_accounts")]
public sealed class DiscordLinkedAccount
{
[Key]
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public ulong DiscordId { get; set; }
public DiscordAccount Discord { get; set; } = default!;
}
[Table("patreon_tiers")]
public sealed class PatronTier
{
[Key]
public int Id { get; set; }
public bool ShowOnCredits { get; set; }
public string Name { get; set; } = default!;
public ulong DiscordRole { get; set; }
public int Priority { get; set; }
public List<Patron> Patrons { get; set; } = default!;
}
[Table("patreon_patrons")]
[Index(nameof(TierId))]
public sealed class Patron
{
[Key]
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public int TierId { get; set; }
public PatronTier Tier { get; set; } = default!;
}
[Table("discord_linking_codes")]
[Index(nameof(Code))]
public sealed class DiscordLinkingCodes
{
[Key]
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public Guid Code { get; set; }
public DateTime CreationTime { get; set; }
}
[Table("discord_linked_accounts_logs")]
[Index(nameof(PlayerId))]
[Index(nameof(DiscordId))]
[Index(nameof(At))]
public sealed class DiscordLinkedAccountLogs
{
[Key]
public int Id { get; set; }
public Guid PlayerId { get; set; }
public Player Player { get; set; } = default!;
public ulong DiscordId { get; set; }
public DiscordAccount Discord { get; set; } = default!;
public DateTime At { get; set; }
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,207 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class DiscordLinking : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "cur_time",
table: "admin_log");
migrationBuilder.CreateTable(
name: "discord_accounts",
columns: table => new
{
discord_accounts_id = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_accounts", x => x.discord_accounts_id);
});
migrationBuilder.CreateTable(
name: "discord_linking_codes",
columns: table => new
{
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
code = table.Column<Guid>(type: "TEXT", nullable: false),
creation_time = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_linking_codes", x => x.player_id);
table.ForeignKey(
name: "FK_discord_linking_codes_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "patreon_tiers",
columns: table => new
{
patreon_tiers_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
show_on_credits = table.Column<bool>(type: "INTEGER", nullable: false),
name = table.Column<string>(type: "TEXT", nullable: false),
discord_role = table.Column<ulong>(type: "INTEGER", nullable: false),
priority = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_patreon_tiers", x => x.patreon_tiers_id);
});
migrationBuilder.CreateTable(
name: "discord_linked_accounts",
columns: table => new
{
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
discord_id = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_linked_accounts", x => x.player_id);
table.ForeignKey(
name: "FK_discord_linked_accounts_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "discord_accounts",
principalColumn: "discord_accounts_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_discord_linked_accounts_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "discord_linked_accounts_logs",
columns: table => new
{
discord_linked_accounts_logs_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
discord_id = table.Column<ulong>(type: "INTEGER", nullable: false),
at = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_linked_accounts_logs", x => x.discord_linked_accounts_logs_id);
table.ForeignKey(
name: "FK_discord_linked_accounts_logs_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "discord_accounts",
principalColumn: "discord_accounts_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_discord_linked_accounts_logs_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "patreon_patrons",
columns: table => new
{
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
tier_id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_patreon_patrons", x => x.player_id);
table.ForeignKey(
name: "FK_patreon_patrons_patreon_tiers_tier_id",
column: x => x.tier_id,
principalTable: "patreon_tiers",
principalColumn: "patreon_tiers_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_patreon_patrons_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_discord_id",
table: "discord_linked_accounts",
column: "discord_id",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_logs_at",
table: "discord_linked_accounts_logs",
column: "at");
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_logs_discord_id",
table: "discord_linked_accounts_logs",
column: "discord_id");
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_logs_player_id",
table: "discord_linked_accounts_logs",
column: "player_id");
migrationBuilder.CreateIndex(
name: "IX_discord_linking_codes_code",
table: "discord_linking_codes",
column: "code");
migrationBuilder.CreateIndex(
name: "IX_patreon_patrons_tier_id",
table: "patreon_patrons",
column: "tier_id");
migrationBuilder.CreateIndex(
name: "IX_patreon_tiers_discord_role",
table: "patreon_tiers",
column: "discord_role",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "discord_linked_accounts");
migrationBuilder.DropTable(
name: "discord_linked_accounts_logs");
migrationBuilder.DropTable(
name: "discord_linking_codes");
migrationBuilder.DropTable(
name: "patreon_patrons");
migrationBuilder.DropTable(
name: "discord_accounts");
migrationBuilder.DropTable(
name: "patreon_tiers");
migrationBuilder.AddColumn<long>(
name: "cur_time",
table: "admin_log",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
}
}
}
@@ -1,5 +1,6 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using Content.Server.Database;
@@ -20,7 +21,7 @@ namespace Content.Server.Database.Migrations.Postgres
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.1")
.HasAnnotation("ProductVersion", "10.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -739,6 +740,151 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("dv_seen_tips", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Patron", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("uuid")
.HasColumnName("player_id");
b.Property<int>("TierId")
.HasColumnType("integer")
.HasColumnName("tier_id");
b.HasKey("PlayerId")
.HasName("PK_patreon_patrons");
b.HasIndex("TierId")
.HasDatabaseName("IX_patreon_patrons_tier_id");
b.ToTable("patreon_patrons", (string)null);
});
modelBuilder.Entity("Content.Server.Database.PatronTier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("patreon_tiers_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("DiscordRole")
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_role");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<bool>("ShowOnCredits")
.HasColumnType("boolean")
.HasColumnName("show_on_credits");
b.HasKey("Id")
.HasName("PK_patreon_tiers");
b.HasIndex("DiscordRole")
.IsUnique();
b.ToTable("patreon_tiers", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordAccount", b =>
{
b.Property<decimal>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_accounts_id");
b.HasKey("Id")
.HasName("PK_discord_accounts");
b.ToTable("discord_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccount", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("uuid")
.HasColumnName("player_id");
b.Property<decimal>("DiscordId")
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_id");
b.HasKey("PlayerId")
.HasName("PK_discord_linked_accounts");
b.HasIndex("DiscordId")
.IsUnique();
b.ToTable("discord_linked_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccountLogs", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("discord_linked_accounts_logs_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("At")
.HasColumnType("timestamp with time zone")
.HasColumnName("at");
b.Property<decimal>("DiscordId")
.HasColumnType("numeric(20,0)")
.HasColumnName("discord_id");
b.Property<Guid>("PlayerId")
.HasColumnType("uuid")
.HasColumnName("player_id");
b.HasKey("Id")
.HasName("PK_discord_linked_accounts_logs");
b.HasIndex("At")
.HasDatabaseName("IX_discord_linked_accounts_logs_at");
b.HasIndex("DiscordId")
.HasDatabaseName("IX_discord_linked_accounts_logs_discord_id");
b.HasIndex("PlayerId")
.HasDatabaseName("IX_discord_linked_accounts_logs_player_id");
b.ToTable("discord_linked_accounts_logs", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkingCodes", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("uuid")
.HasColumnName("player_id");
b.Property<Guid>("Code")
.HasColumnType("uuid")
.HasColumnName("code");
b.Property<DateTime>("CreationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("creation_time");
b.HasKey("PlayerId")
.HasName("PK_discord_linking_codes");
b.HasIndex("Code")
.HasDatabaseName("IX_discord_linking_codes_code");
b.ToTable("discord_linking_codes", (string)null);
});
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
{
b.Property<int>("Id")
@@ -901,7 +1047,7 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("text")
.HasColumnName("admin_ooc_color");
b.PrimitiveCollection<string[]>("ConstructionFavorites")
b.PrimitiveCollection<List<string>>("ConstructionFavorites")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("construction_favorites");
@@ -1840,6 +1986,85 @@ namespace Content.Server.Database.Migrations.Postgres
.HasConstraintName("FK_dv_seen_tips_player_player_id");
});
modelBuilder.Entity("Content.Server.Database.Patron", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("Patron")
.HasForeignKey("Content.Server.Database.Patron", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_patreon_patrons_player_player_id1");
b.HasOne("Content.Server.Database.PatronTier", "Tier")
.WithMany("Patrons")
.HasForeignKey("TierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_patreon_patrons_patreon_tiers_tier_id");
b.Navigation("Player");
b.Navigation("Tier");
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccount", b =>
{
b.HasOne("Content.Server.Database.DiscordAccount", "Discord")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.DiscordLinkedAccount", "DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.DiscordLinkedAccount", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_player_player_id1");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccountLogs", b =>
{
b.HasOne("Content.Server.Database.DiscordAccount", "Discord")
.WithMany("LinkedAccountLogs")
.HasForeignKey("DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_logs_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("LinkedAccountLogs")
.HasForeignKey("PlayerId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_logs_player_player_id1");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkingCodes", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkingCodes")
.HasForeignKey("Content.Server.Database.DiscordLinkingCodes", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linking_codes_player_player_id1");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
@@ -2169,6 +2394,19 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("BanHits");
});
modelBuilder.Entity("Content.Server.Database.PatronTier", b =>
{
b.Navigation("Patrons");
});
modelBuilder.Entity("Content.Server.Database.DiscordAccount", b =>
{
b.Navigation("LinkedAccount")
.IsRequired();
b.Navigation("LinkedAccountLogs");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.Navigation("AdminLogs");
@@ -2206,6 +2444,14 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("AdminWatchlistsReceived");
b.Navigation("JobWhitelists");
b.Navigation("LinkedAccount");
b.Navigation("LinkedAccountLogs");
b.Navigation("LinkingCodes");
b.Navigation("Patron");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,207 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class DiscordLinking : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "cur_time",
table: "admin_log");
migrationBuilder.CreateTable(
name: "discord_accounts",
columns: table => new
{
discord_accounts_id = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_accounts", x => x.discord_accounts_id);
});
migrationBuilder.CreateTable(
name: "discord_linking_codes",
columns: table => new
{
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
code = table.Column<Guid>(type: "TEXT", nullable: false),
creation_time = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_linking_codes", x => x.player_id);
table.ForeignKey(
name: "FK_discord_linking_codes_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "patreon_tiers",
columns: table => new
{
patreon_tiers_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
show_on_credits = table.Column<bool>(type: "INTEGER", nullable: false),
name = table.Column<string>(type: "TEXT", nullable: false),
discord_role = table.Column<ulong>(type: "INTEGER", nullable: false),
priority = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_patreon_tiers", x => x.patreon_tiers_id);
});
migrationBuilder.CreateTable(
name: "discord_linked_accounts",
columns: table => new
{
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
discord_id = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_linked_accounts", x => x.player_id);
table.ForeignKey(
name: "FK_discord_linked_accounts_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "discord_accounts",
principalColumn: "discord_accounts_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_discord_linked_accounts_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "discord_linked_accounts_logs",
columns: table => new
{
discord_linked_accounts_logs_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
discord_id = table.Column<ulong>(type: "INTEGER", nullable: false),
at = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_discord_linked_accounts_logs", x => x.discord_linked_accounts_logs_id);
table.ForeignKey(
name: "FK_discord_linked_accounts_logs_discord_accounts_discord_id",
column: x => x.discord_id,
principalTable: "discord_accounts",
principalColumn: "discord_accounts_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_discord_linked_accounts_logs_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "patreon_patrons",
columns: table => new
{
player_id = table.Column<Guid>(type: "TEXT", nullable: false),
tier_id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_patreon_patrons", x => x.player_id);
table.ForeignKey(
name: "FK_patreon_patrons_patreon_tiers_tier_id",
column: x => x.tier_id,
principalTable: "patreon_tiers",
principalColumn: "patreon_tiers_id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_patreon_patrons_player_player_id1",
column: x => x.player_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_discord_id",
table: "discord_linked_accounts",
column: "discord_id",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_logs_at",
table: "discord_linked_accounts_logs",
column: "at");
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_logs_discord_id",
table: "discord_linked_accounts_logs",
column: "discord_id");
migrationBuilder.CreateIndex(
name: "IX_discord_linked_accounts_logs_player_id",
table: "discord_linked_accounts_logs",
column: "player_id");
migrationBuilder.CreateIndex(
name: "IX_discord_linking_codes_code",
table: "discord_linking_codes",
column: "code");
migrationBuilder.CreateIndex(
name: "IX_patreon_patrons_tier_id",
table: "patreon_patrons",
column: "tier_id");
migrationBuilder.CreateIndex(
name: "IX_patreon_tiers_discord_role",
table: "patreon_tiers",
column: "discord_role",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "discord_linked_accounts");
migrationBuilder.DropTable(
name: "discord_linked_accounts_logs");
migrationBuilder.DropTable(
name: "discord_linking_codes");
migrationBuilder.DropTable(
name: "patreon_patrons");
migrationBuilder.DropTable(
name: "discord_accounts");
migrationBuilder.DropTable(
name: "patreon_tiers");
migrationBuilder.AddColumn<long>(
name: "cur_time",
table: "admin_log",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
}
}
}
@@ -15,7 +15,7 @@ namespace Content.Server.Database.Migrations.Sqlite
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
modelBuilder.HasAnnotation("ProductVersion", "10.0.6");
modelBuilder.Entity("Content.Server.Database.Admin", b =>
{
@@ -698,6 +698,147 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("dv_seen_tips", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Patron", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.Property<int>("TierId")
.HasColumnType("INTEGER")
.HasColumnName("tier_id");
b.HasKey("PlayerId")
.HasName("PK_patreon_patrons");
b.HasIndex("TierId")
.HasDatabaseName("IX_patreon_patrons_tier_id");
b.ToTable("patreon_patrons", (string)null);
});
modelBuilder.Entity("Content.Server.Database.PatronTier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("patreon_tiers_id");
b.Property<ulong>("DiscordRole")
.HasColumnType("INTEGER")
.HasColumnName("discord_role");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("Priority")
.HasColumnType("INTEGER")
.HasColumnName("priority");
b.Property<bool>("ShowOnCredits")
.HasColumnType("INTEGER")
.HasColumnName("show_on_credits");
b.HasKey("Id")
.HasName("PK_patreon_tiers");
b.HasIndex("DiscordRole")
.IsUnique();
b.ToTable("patreon_tiers", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordAccount", b =>
{
b.Property<ulong>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("discord_accounts_id");
b.HasKey("Id")
.HasName("PK_discord_accounts");
b.ToTable("discord_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccount", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.Property<ulong>("DiscordId")
.HasColumnType("INTEGER")
.HasColumnName("discord_id");
b.HasKey("PlayerId")
.HasName("PK_discord_linked_accounts");
b.HasIndex("DiscordId")
.IsUnique();
b.ToTable("discord_linked_accounts", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccountLogs", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("discord_linked_accounts_logs_id");
b.Property<DateTime>("At")
.HasColumnType("TEXT")
.HasColumnName("at");
b.Property<ulong>("DiscordId")
.HasColumnType("INTEGER")
.HasColumnName("discord_id");
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.HasKey("Id")
.HasName("PK_discord_linked_accounts_logs");
b.HasIndex("At")
.HasDatabaseName("IX_discord_linked_accounts_logs_at");
b.HasIndex("DiscordId")
.HasDatabaseName("IX_discord_linked_accounts_logs_discord_id");
b.HasIndex("PlayerId")
.HasDatabaseName("IX_discord_linked_accounts_logs_player_id");
b.ToTable("discord_linked_accounts_logs", (string)null);
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkingCodes", b =>
{
b.Property<Guid>("PlayerId")
.HasColumnType("TEXT")
.HasColumnName("player_id");
b.Property<Guid>("Code")
.HasColumnType("TEXT")
.HasColumnName("code");
b.Property<DateTime>("CreationTime")
.HasColumnType("TEXT")
.HasColumnName("creation_time");
b.HasKey("PlayerId")
.HasName("PK_discord_linking_codes");
b.HasIndex("Code")
.HasDatabaseName("IX_discord_linking_codes_code");
b.ToTable("discord_linking_codes", (string)null);
});
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
{
b.Property<int>("Id")
@@ -1759,6 +1900,85 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasConstraintName("FK_dv_seen_tips_player_player_id");
});
modelBuilder.Entity("Content.Server.Database.Patron", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("Patron")
.HasForeignKey("Content.Server.Database.Patron", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_patreon_patrons_player_player_id1");
b.HasOne("Content.Server.Database.PatronTier", "Tier")
.WithMany("Patrons")
.HasForeignKey("TierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_patreon_patrons_patreon_tiers_tier_id");
b.Navigation("Player");
b.Navigation("Tier");
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccount", b =>
{
b.HasOne("Content.Server.Database.DiscordAccount", "Discord")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.DiscordLinkedAccount", "DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkedAccount")
.HasForeignKey("Content.Server.Database.DiscordLinkedAccount", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_player_player_id1");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkedAccountLogs", b =>
{
b.HasOne("Content.Server.Database.DiscordAccount", "Discord")
.WithMany("LinkedAccountLogs")
.HasForeignKey("DiscordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_logs_discord_accounts_discord_id");
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("LinkedAccountLogs")
.HasForeignKey("PlayerId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linked_accounts_logs_player_player_id1");
b.Navigation("Discord");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.DiscordLinkingCodes", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithOne("LinkingCodes")
.HasForeignKey("Content.Server.Database.DiscordLinkingCodes", "PlayerId")
.HasPrincipalKey("Content.Server.Database.Player", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_discord_linking_codes_player_player_id1");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
@@ -2088,6 +2308,19 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("BanHits");
});
modelBuilder.Entity("Content.Server.Database.PatronTier", b =>
{
b.Navigation("Patrons");
});
modelBuilder.Entity("Content.Server.Database.DiscordAccount", b =>
{
b.Navigation("LinkedAccount")
.IsRequired();
b.Navigation("LinkedAccountLogs");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.Navigation("AdminLogs");
@@ -2125,6 +2358,14 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("AdminWatchlistsReceived");
b.Navigation("JobWhitelists");
b.Navigation("LinkedAccount");
b.Navigation("LinkedAccountLogs");
b.Navigation("LinkingCodes");
b.Navigation("Patron");
});
modelBuilder.Entity("Content.Server.Database.Preference", b =>
+71
View File
@@ -48,6 +48,15 @@ namespace Content.Server.Database
public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
public DbSet<DVModel.SeenTip> DVSeenTips { get; set; } = null!; // DeltaV - Tips
// BEGIN DeltaV - Discord / Patreon Account Linking
public DbSet<DiscordAccount> DiscordAccounts { get; set; } = default!;
public DbSet<DiscordLinkedAccount> DiscordLinkedAccounts { get; set; } = default!;
public DbSet<PatronTier> PatronTiers { get; set; } = default!;
public DbSet<Patron> Patrons { get; set; } = default!;
public DbSet<DiscordLinkingCodes> DiscordLinkingCodes { get; set; } = default!;
public DbSet<DiscordLinkedAccountLogs> DiscordLinkedAccountLogs { get; set; } = default!;
// END DeltaV
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Preference>()
@@ -395,6 +404,61 @@ namespace Content.Server.Database
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
// BEGIN DeltaV - Discord / Patreon Account Linking
modelBuilder.Entity<DiscordLinkedAccount>()
.HasOne(l => l.Player)
.WithOne(p => p.LinkedAccount)
.HasForeignKey<DiscordLinkedAccount>(l => l.PlayerId)
.HasPrincipalKey<Player>(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DiscordLinkedAccount>()
.HasOne(l => l.Discord)
.WithOne(d => d.LinkedAccount)
.HasForeignKey<DiscordLinkedAccount>(l => l.DiscordId)
.HasPrincipalKey<DiscordAccount>(d => d.Id)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Patron>()
.HasOne(p => p.Player)
.WithOne(p => p.Patron)
.HasForeignKey<Patron>(p => p.PlayerId)
.HasPrincipalKey<Player>(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Patron>()
.HasOne(p => p.Tier)
.WithMany(t => t.Patrons)
.HasForeignKey(p => p.TierId)
.HasPrincipalKey(p => p.Id)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<PatronTier>()
.HasIndex(t => t.DiscordRole)
.IsUnique();
modelBuilder.Entity<DiscordLinkingCodes>()
.HasOne(l => l.Player)
.WithOne(p => p.LinkingCodes)
.HasForeignKey<DiscordLinkingCodes>(l => l.PlayerId)
.HasPrincipalKey<Player>(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DiscordLinkedAccountLogs>()
.HasOne(l => l.Player)
.WithMany(p => p.LinkedAccountLogs)
.HasForeignKey(l => l.PlayerId)
.HasPrincipalKey(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<DiscordLinkedAccountLogs>()
.HasOne(l => l.Discord)
.WithMany(p => p.LinkedAccountLogs)
.HasForeignKey(l => l.DiscordId)
.HasPrincipalKey(p => p.Id)
.OnDelete(DeleteBehavior.Cascade);
// END DeltaV
}
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -621,6 +685,13 @@ namespace Content.Server.Database
public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
// BEGIN DeltaV - Discord / Patreon Account Linking
public DiscordLinkedAccount? LinkedAccount { get; set; }
public Patron? Patron { get; set; }
public DiscordLinkingCodes? LinkingCodes { get; set; }
public List<DiscordLinkedAccountLogs> LinkedAccountLogs { get; set; } = default!;
// END DeltaV
}
[Table("whitelist")]
@@ -6,7 +6,6 @@ using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Player; // DeltaV
namespace Content.Server.AlertLevel;
@@ -192,8 +191,7 @@ public sealed class AlertLevelSystem : EntitySystem
{
if (detail.Sound != null)
{
//var filter = _stationSystem.GetInOwningStation(station); // DeltaV - Global Annoucements
var filter = Filter.Empty().AddAllPlayers(); // DeltaV - Global Annoucements
var filter = _stationSystem.GetInOwningStation(station);
_audio.PlayGlobal(detail.Sound, filter, true, detail.Sound.Params);
}
else
@@ -204,7 +202,7 @@ public sealed class AlertLevelSystem : EntitySystem
if (announce)
{
_chatSystem.DispatchGlobalAnnouncement(announcementFull, playSound: playDefault, // DeltaV - Global Annoucements
_chatSystem.DispatchStationAnnouncement(station, announcementFull, playDefaultSound: playDefault,
colorOverride: detail.Color, sender: stationName);
}
+4 -17
View File
@@ -87,7 +87,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
if (anomaly.Comp.CurrentBehavior is not null)
RemoveBehavior(anomaly, anomaly.Comp.CurrentBehavior.Value);
EndAnomaly(anomaly, spawnCore: false, forced: true);
EndAnomaly(anomaly, spawnCore: false);
}
private void OnStartCollide(Entity<AnomalyComponent> anomaly, ref StartCollideEvent args)
@@ -143,7 +143,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
return 0;
var multiplier = 1f;
if (component.AlwaysGrow || component.Stability > component.GrowthThreshold) // DeltaV - Add AlwaysGrow
if (component.Stability > component.GrowthThreshold)
multiplier = component.GrowingPointMultiplier; //more points for unstable
//penalty of up to 50% based on health
@@ -253,11 +253,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
else
{
string stateLoc;
// DeltaV - Colossus Additions START
if (anomalyComp.AlwaysGrow)
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
// DeltaV - Colossus Additions END
else if (anomalyComp.Stability < anomalyComp.DecayThreshold) // DeltaV - Add else
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
@@ -349,15 +345,6 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown"));
else
{
// DeltaV - Colossus Additions START
if (anomalyComp.AlwaysGrow)
{
msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-always-grow"));
if (anomalyComp.CurrentBehavior != null)
msg.PushNewline();
}
// DeltaV - Colossus Additions END
if (anomalyComp.CurrentBehavior != null)
{
var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value);
@@ -367,7 +354,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
var mod = Math.Floor((behavior.EarnPointModifier) * 100);
msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod)));
}
else if(!anomalyComp.AlwaysGrow) // DeltaV - Add condition, previously regular else
else
{
msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-balanced"));
}
@@ -7,7 +7,7 @@ using Robust.Shared.Player;
namespace Content.Server.Audio;
public sealed partial class ServerGlobalSoundSystem : SharedGlobalSoundSystem // DeltaV - Made Partial
public sealed class ServerGlobalSoundSystem : SharedGlobalSoundSystem
{
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
+6 -53
View File
@@ -5,7 +5,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.Discord.DiscordLink;
using Content.Server.Ghost;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Shared.Administration;
@@ -16,7 +15,6 @@ using Content.Shared.Mind;
using Content.Shared.Players; // DeltaV - OOC muting
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Replays;
@@ -49,7 +47,6 @@ internal sealed partial class ChatManager : IChatManager
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly DiscordChatLink _discordLink = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
private ISawmill _sawmill = default!;
@@ -92,10 +89,10 @@ internal sealed partial class ChatManager : IChatManager
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
}
public void DeleteMessagesBy(NetUserId uid)
{
if (!_players.TryGetValue(uid, out var user))
return;
public void DeleteMessagesBy(NetUserId uid)
{
if (!_players.TryGetValue(uid, out var user))
return;
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
_netManager.ServerSendToAll(msg);
@@ -345,35 +342,12 @@ internal sealed partial class ChatManager : IChatManager
#region Utility
private bool IsValidWarpDestination(EntityUid source)
{
if (!source.Valid)
return false;
if (!_entityManager.TryGetComponent(source, out TransformComponent? transform))
return false;
return transform.MapID != MapId.Nullspace;
}
public string PrependFollowButtonIfAppropriate(string wrappedMessage, EntityUid source, INetChannel recipient)
{
if (IsValidWarpDestination(source) && ShouldShowFollowButton(recipient))
{
var btnText = _localizationManager.GetString("chat-manager-follow-button");
return $"[cmdlink=\"{btnText}\" command=\"{GhostFollowEntityCommand.CommandName} {_entityManager.GetNetEntity(source)}\" /] " + wrappedMessage;
}
return wrappedMessage;
}
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
wrappedMessage = PrependFollowButtonIfAppropriate(wrappedMessage, source, client);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
@@ -396,12 +370,8 @@ internal sealed partial class ChatManager : IChatManager
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
foreach (var client in clients)
{
var customWrapMessage = PrependFollowButtonIfAppropriate(wrappedMessage, source, client);
var msg = new ChatMessage(channel, message, customWrapMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendMessage(new MsgChatMessage { Message = msg }, client);
}
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
if (!recordReplay)
return;
@@ -409,7 +379,6 @@ internal sealed partial class ChatManager : IChatManager
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_replay.RecordServerMessage(msg);
}
}
@@ -470,22 +439,6 @@ internal sealed partial class ChatManager : IChatManager
}
#endregion
private bool ShouldShowFollowButton(INetChannel recipient)
{
if (!_player.TryGetSessionByChannel(recipient, out var session))
return false;
if (_entityManager.TrySystem(out GhostSystem? ghost))
{
if (!ghost.CanGhostWarp(session, out _))
{
return false;
}
}
return _netConfigManager.GetClientCVar(recipient, CCVars.InterfaceChatFollowButton);
}
}
public enum OOCChatType : byte
@@ -49,7 +49,5 @@ namespace Content.Server.Chat.Managers
/// <param name="player">The player sending a chat message.</param>
/// <returns>False if the player has violated rate limits and should be blocked from sending further messages.</returns>
RateLimitStatus HandleRateLimit(ICommonSession player);
string PrependFollowButtonIfAppropriate(string wrappedMessage, EntityUid source, INetChannel recipient);
}
}
+53 -1
View File
@@ -16,7 +16,7 @@ using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Traits;
//using Content.Shared.Traits; // DV - Traits
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums;
using Robust.Shared.Network;
@@ -1924,8 +1924,60 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
.Where(s => s.PlayerUserId == player)
.ExecuteDeleteAsync();
}
#endregion
// BEGIN DeltaV - Discord / Patreon Account Linking
#region DeltaV - Account Linking
public async Task<Guid?> GetLinkingCode(Guid player)
{
await using var db = await GetDb();
var linking = await db.DbContext.DiscordLinkingCodes.FirstOrDefaultAsync(l => l.PlayerId == player);
return linking?.Code;
}
public async Task SetLinkingCode(Guid player, Guid code)
{
await using var db = await GetDb();
var linking = await db.DbContext.DiscordLinkingCodes.FirstOrDefaultAsync(l => l.PlayerId == player);
if (linking == null)
{
linking = new DiscordLinkingCodes { PlayerId = player };
db.DbContext.DiscordLinkingCodes.Add(linking);
}
linking.Code = code;
linking.CreationTime = DateTime.UtcNow;
await db.DbContext.SaveChangesAsync();
}
public async Task<bool> HasLinkedAccount(Guid player, CancellationToken cancel)
{
await using var db = await GetDb(cancel);
return await db.DbContext.DiscordLinkedAccounts.AnyAsync(l => l.PlayerId == player, cancel);
}
public async Task<Patron?> GetPatron(Guid player, CancellationToken cancel)
{
await using var db = await GetDb(cancel);
var patron = await db.DbContext.Patrons
.Include(p => p.Tier)
.FirstOrDefaultAsync(p => p.PlayerId == player, cancellationToken: cancel);
return patron;
}
public async Task<List<Patron>> GetAllPatrons()
{
await using var db = await GetDb();
return await db.DbContext.Patrons
.Include(p => p.Player)
.Include(p => p.Tier)
.ToListAsync();
}
#endregion
// END DeltaV
public abstract Task SendNotification(DatabaseNotification notification);
@@ -354,6 +354,21 @@ namespace Content.Server.Database
#endregion
// BEGIN DeltaV - Discord / Patreon Account Linking
#region DeltaV - Discord Linking
Task<Guid?> GetLinkingCode(Guid player);
Task SetLinkingCode(Guid player, Guid code);
Task<bool> HasLinkedAccount(Guid player, CancellationToken cancel);
Task<Patron?> GetPatron(Guid player, CancellationToken cancel);
Task<List<Patron>> GetAllPatrons();
#endregion
// END DeltaV
#region DB Notifications
void SubscribeToNotifications(Action<DatabaseNotification> handler);
@@ -1127,6 +1142,41 @@ namespace Content.Server.Database
}
}
// BEGIN DeltaV - Discord Linking
#region DeltaV - Discord Linking
public Task<Guid?> GetLinkingCode(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetLinkingCode(player));
}
public Task SetLinkingCode(Guid player, Guid code)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetLinkingCode(player, code));
}
public Task<bool> HasLinkedAccount(Guid player, CancellationToken cancel)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.HasLinkedAccount(player, cancel));
}
public Task<Patron?> GetPatron(Guid player, CancellationToken cancel)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPatron(player, cancel));
}
public Task<List<Patron>> GetAllPatrons()
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAllPatrons());
}
#endregion
// END DeltaV
// Wrapper functions to run DB commands from the thread pool.
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
// For SQLite, this will also enable read parallelization (within limits).
@@ -382,6 +382,11 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
var visualEnt = CreateExplosionVisualEntity(pos, queued.Proto.ID, spaceMatrix, spaceData, gridData.Values, iterationIntensity);
// camera shake
// ES START
// CameraShake(iterationIntensity.Count * 4f, pos, queued.TotalIntensity);
// ES END
//For whatever bloody reason, sound system requires ENTITY coordinates.
var mapEntityCoords = _transformSystem.ToCoordinates(_map.GetMap(pos.MapId), pos);
@@ -411,7 +416,12 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
? queued.Proto.SmallSoundFar
: queued.Proto.SoundFar;
CameraShake(iterationIntensity, pos, queued); // Starlight
// ES START
var farTranslationShake = iterationIntensity.Count < queued.Proto.SmallSoundIterationThreshold
? new ESScreenshakeParameters() { Trauma = 0.4f, DecayRate = 0.2f, Frequency = 0.014f }
: new ESScreenshakeParameters() { Trauma = 0.6f, DecayRate = 0.05f, Frequency = 0.014f };
_shake.Screenshake(filter, farTranslationShake, null);
// ES END
_audio.PlayGlobal(farSound, farFilter, true, farSound.Params);
@@ -434,13 +444,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
_damageableSystem);
}
private void CameraShake(List<float> rangeList, MapCoordinates epicenter, QueuedExplosion queued) // Starlight - replace range with rangeList, totalIntensity with queued
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity)
{
// Starlight BEGIN
var range = rangeList.Count * 4f;
var totalIntensity = queued.TotalIntensity * 10f;
// Starlight END
var players = Filter.Empty();
players.AddInRange(epicenter, range, _playerManager, EntityManager);
@@ -458,40 +463,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
var distance = delta.Length();
var effect = 5 * MathF.Pow(totalIntensity, 0.5f) * (1 - distance / range);
if (effect > 0.01f)
{
// DeltaV - Camera kick falloff START
// _recoilSystem.KickCamera(uid, -delta.Normalized() * effect);
// Exponential decay: N(t) = N₀ ⋅ e^(-λ ⋅ t)
// In this case, N = effect and we aren't decaying over time but with increasing distance from the epicenter,
// so t = distance / range. Higher values of λ lead to faster decay, lower values to slower decay.
//
// severity is a fixed factor used to decrease the overall severity of the camera kick,
// as the values were so high by default that decay was only becoming really noticeable near range.
//
// these changes are made here instead of directly assigning to the effect variable
// above to maintain the same overall effect range as before.
const float severity = 0.033f;
const float lambda = 4f;
_recoilSystem.KickCamera(uid, -delta.Normalized() * effect * severity * MathF.Exp(-lambda * (distance / range)));
// DeltaV END
// Starlight START
var shakeParams = rangeList.Count < queued.Proto.SmallSoundIterationThreshold
? new ESScreenshakeParameters() { Trauma = 0.4f, DecayRate = 0.2f, Frequency = 0.014f }
: new ESScreenshakeParameters() { Trauma = 0.6f, DecayRate = 0.05f, Frequency = 0.014f };
// DeltaV - Screenshake falloff START
// Linear falloff with increasing distance from epicenter,
// capped at certain values to avoid diminishing the effect completely near range.
shakeParams.DecayRate *= MathF.Min(1 + distance / range, 1.75f);
shakeParams.Frequency *= MathF.Max(1 - distance / range, 0.33f);
shakeParams.Trauma *= MathF.Max(1 - distance / range, 0.33f);
// DeltaV END
_shake.Screenshake(players, shakeParams, null);
// Starlight END
}
_recoilSystem.KickCamera(uid, -delta.Normalized() * effect);
}
}
}
+1 -7
View File
@@ -12,7 +12,6 @@ using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.DeviceNetwork.Systems; // DeltaV - map init ordering
using Content.Shared.Emag.Systems;
using Content.Shared.Fax;
using Content.Shared.Fax.Components;
@@ -65,7 +64,7 @@ public sealed class FaxSystem : EntitySystem
// Hooks
SubscribeLocalEvent<FaxMachineComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<FaxMachineComponent, MapInitEvent>(OnMapInit, after: [typeof(SharedDeviceNetworkSystem)]); // DeltaV - map init order, we need address assigned first
SubscribeLocalEvent<FaxMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<FaxMachineComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<FaxMachineComponent, EntInsertedIntoContainerMessage>(OnItemSlotChanged);
@@ -302,11 +301,6 @@ public sealed class FaxSystem : EntitySystem
if (!args.Data.TryGetValue(FaxConstants.FaxPaperNameData, out string? name) ||
!args.Data.TryGetValue(FaxConstants.FaxPaperContentData, out string? content))
return;
// Begin DeltaV - we removed the power requirement from device network but we still don't want
// unpowered faxes to happen
if (TryComp<ApcPowerReceiverComponent>(uid, out var receiver) && !receiver.Powered)
return;
// End DeltaV
args.Data.TryGetValue(FaxConstants.FaxPaperLabelData, out string? label);
args.Data.TryGetValue(FaxConstants.FaxPaperStampStateData, out string? stampState);
@@ -35,17 +35,17 @@ public sealed partial class PuddleSystem
if (!entity.Comp.SpillWhenThrown || Openable.IsClosed(entity.Owner))
return;
// DeltaV - Beer Goggles Safe Throw
if ( args.User is { } user && _safeSolutionThrower.GetSafeThrow(user))
{
_physics.SetAngularVelocity(entity, 0);
Transform(entity).LocalRotation = Angle.Zero;
return;
}
// END DeltaV
if (TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, out _, out var solution) && args.User != null)
{
// DeltaV - Beer Goggles Safe Throw
if (_safeSolutionThrower.GetSafeThrow(args.User.Value))
{
_physics.SetAngularVelocity(entity, 0);
Transform(entity).LocalRotation = Angle.Zero;
return;
}
// END DeltaV
AdminLogger.Add(LogType.Landed,
$"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing");
}
@@ -1,26 +0,0 @@
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Ghost;
[AnyCommand]
internal sealed partial class GhostFollowEntityCommand : LocalizedEntityCommands
{
public const string CommandName = "ghost_follow_entity";
[Dependency] private GhostSystem _ghost = null!;
public override string Command => CommandName;
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1 || shell.Player is not { } player)
return;
var target = args[0];
if (!NetEntity.TryParse(target, out var targetEnt))
return;
_ghost.GhostWarpRequest(player, targetEnt);
}
}
+20 -35
View File
@@ -299,22 +299,10 @@ namespace Content.Server.Ghost
#region Warp
public bool CanGhostWarp(ICommonSession session, out EntityUid entity)
{
if (session.AttachedEntity is not { Valid: true } sessionEntity
|| !_ghostQuery.HasComp(sessionEntity))
{
entity = default;
return false;
}
entity = sessionEntity;
return true;
}
private void OnGhostWarpsRequest(GhostWarpsRequestEvent msg, EntitySessionEventArgs args)
{
if (!CanGhostWarp(args.SenderSession, out var entity))
if (args.SenderSession.AttachedEntity is not {Valid: true} entity
|| !_ghostQuery.HasComp(entity))
{
Log.Warning($"User {args.SenderSession.Name} sent a {nameof(GhostWarpsRequestEvent)} without being a ghost.");
return;
@@ -324,33 +312,30 @@ namespace Content.Server.Ghost
RaiseNetworkEvent(response, args.SenderSession.Channel);
}
public void GhostWarpRequest(ICommonSession player, NetEntity target)
{
if (!CanGhostWarp(player, out var attached))
{
Log.Warning($"User {player.Name} tried to warp to {target} without being a ghost.");
return;
}
var realTarget = GetEntity(target);
if (!Exists(realTarget))
{
Log.Warning($"User {player.Name} tried to warp to an invalid entity id: {target}");
return;
}
WarpTo(attached, realTarget);
}
private void OnGhostWarpToTargetRequest(GhostWarpToTargetRequestEvent msg, EntitySessionEventArgs args)
{
GhostWarpRequest(args.SenderSession, msg.Target);
if (args.SenderSession.AttachedEntity is not {Valid: true} attached
|| !_ghostQuery.HasComp(attached))
{
Log.Warning($"User {args.SenderSession.Name} tried to warp to {msg.Target} without being a ghost.");
return;
}
var target = GetEntity(msg.Target);
if (!Exists(target))
{
Log.Warning($"User {args.SenderSession.Name} tried to warp to an invalid entity id: {msg.Target}");
return;
}
WarpTo(attached, target);
}
private void OnGhostnadoRequest(GhostnadoRequestEvent msg, EntitySessionEventArgs args)
{
if (CanGhostWarp(args.SenderSession, out var uid))
if (args.SenderSession.AttachedEntity is not {} uid
|| !_ghostQuery.HasComp(uid))
{
Log.Warning($"User {args.SenderSession.Name} tried to ghostnado without being a ghost.");
return;
+2
View File
@@ -1,3 +1,4 @@
using Content.Server._DV.AccountLinking; // DeltaV
using Content.Server.Administration;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
@@ -81,5 +82,6 @@ internal static class ServerContentIoC
deps.Register<CVarControlManager>();
deps.Register<DiscordLink>();
deps.Register<DiscordChatLink>();
deps.Register<LinkAccountManager>(); // DeltaV - Discord Account Linking
}
}
@@ -211,7 +211,7 @@ namespace Content.Server.Medical.BiomassReclaimer
_solution.ResolveSolution(toProcess, stream.BloodSolutionName, ref stream.BloodSolution, out var solution))
{
component.BloodReagents = solution.Clone();
//component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume); // Delta V - This doesn't need to be here. It just always makes the solution ~50u but also might divide by 0. Just use the current blood level so more blood = more mess.
component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume);
}
if (TryComp<ButcherableComponent>(toProcess, out var butcherableComponent))
{
+9 -9
View File
@@ -327,15 +327,15 @@ public sealed class NukeSystem : EntitySystem
// should play
if (nuke.RemainingTime <= _nukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong && !ResolvedSoundSpecifier.IsNullOrEmpty(_selectedNukeSong))
{
_sound.DispatchGlobalEventMusic(_selectedNukeSong, StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
_sound.DispatchStationEventMusic(uid, _selectedNukeSong, StationEventMusicType.Nuke);
nuke.PlayedNukeSong = true;
}
// play alert sound if time is running out
if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound)
{
_sound.PlayGlobal(_audio.ResolveSound(nuke.AlertSound), new AudioParams { Volume = -5f }); // DeltaV - Global Nuke SFX
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(nuke.AlertSound), new AudioParams{Volume = -5f});
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
nuke.PlayedAlertSound = true;
UpdateAppearance(uid, nuke);
}
@@ -504,9 +504,9 @@ public sealed class NukeSystem : EntitySystem
("time", (int) component.RemainingTime),
("location", FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString((uid, nukeXform)))));
var sender = Loc.GetString("nuke-component-announcement-sender");
_chatSystem.DispatchGlobalAnnouncement(announcement, sender, false, null, Color.Red); // DeltaV - Global Nuke Annoucement
_chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
_sound.PlayGlobal(_audio.ResolveSound(component.ArmSound)); // DeltaV - Global Nuke Music
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.ArmSound));
_nukeSongLength = (float) _audio.GetAudioLength(_selectedNukeSong).TotalSeconds;
// turn on the spinny light
@@ -544,11 +544,11 @@ public sealed class NukeSystem : EntitySystem
// warn a crew
var announcement = Loc.GetString("nuke-component-announcement-unarmed");
var sender = Loc.GetString("nuke-component-announcement-sender");
_chatSystem.DispatchGlobalAnnouncement(announcement, sender, false); // DeltaV - Global Nuke Announcement
_chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
component.PlayedNukeSong = false;
_sound.PlayGlobal(_audio.ResolveSound(component.DisarmSound)); // DeltaV - Global Nuke SFX
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.DisarmSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
// reset nuke remaining time to either itself or the minimum time, whichever is higher
component.RemainingTime = Math.Max(component.RemainingTime, component.MinimumTime);
@@ -610,7 +610,7 @@ public sealed class NukeSystem : EntitySystem
OwningStation = transform.GridUid,
});
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
Del(uid);
}
@@ -1,7 +1,5 @@
using Content.Server.Administration.Logs;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Ghost;
using Content.Server.Power.Components;
using Content.Shared._DV.Chat;
using Content.Shared.Chat;
@@ -30,8 +28,6 @@ public sealed class RadioSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
// set used to prevent radio feedback loops.
private readonly HashSet<string> _messages = new();
@@ -59,25 +55,8 @@ public sealed class RadioSystem : EntitySystem
private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args)
{
if (!TryComp(uid, out ActorComponent? actor))
return;
var msg = args.ChatMsg;
if (_ghost.CanGhostWarp(actor.PlayerSession, out _))
{
msg = new MsgChatMessage
{
Message = new ChatMessage(args.ChatMsg.Message)
{
WrappedMessage = _chatManager.PrependFollowButtonIfAppropriate(
args.ChatMsg.Message.WrappedMessage,
args.MessageSource,
actor.PlayerSession.Channel),
},
};
}
_netMan.ServerSendMessage(msg, actor.PlayerSession.Channel);
if (TryComp(uid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel);
}
// DeltaV
@@ -157,8 +136,7 @@ public sealed class RadioSystem : EntitySystem
message,
wrappedMessage,
NetEntity.Invalid,
null,
radioChannelProto: channel.ID); // DeltaV - Add RadioChannel for committing sins
null);
var chatMsg = new MsgChatMessage { Message = chat };
var ev = new RadioReceiveEvent(message, messageSource, channel, radioSource, chatMsg);
@@ -210,7 +188,7 @@ public sealed class RadioSystem : EntitySystem
}
/// <inheritdoc cref="TelecomServerComponent"/>
public bool HasActiveServer(MapId mapId, string channelId) // DeltaV - we need this
private bool HasActiveServer(MapId mapId, string channelId)
{
var servers = EntityQuery<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent, TransformComponent>();
foreach (var (_, keys, power, transform) in servers)
+2 -24
View File
@@ -1,4 +1,3 @@
using System.Linq; // DeltaV
using System.Numerics;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
@@ -9,8 +8,6 @@ using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Chat;
using Content.Shared.Dataset;
using Content.Shared.Mobs; // DeltaV
using Content.Shared.Mobs.Components; // DeltaV
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Pointing;
@@ -50,33 +47,14 @@ namespace Content.Server.RatKing
if (!TryComp<HungerComponent>(uid, out var hunger))
return;
// DeltaV - modify cost of Raise Army based on the amount of alive servants
// Check on how many servants are alive to calculate the cost of a new servant
var aliveServants = component.Servants.Count(servant
=> TryComp<MobStateComponent>(servant, out var mobState) && mobState.CurrentState == MobState.Alive);
// calculate the cost multiplier
var multiplier = aliveServants switch
{
< 5 => 1.0f, // 1-5 servants: 10 hunger
< 10 => 1.5f, // 6-10 servants: 15 hunger
< 15 => 2.5f, // 11-15 servants: 25 hunger
< 20 => 5.0f, // 16-20 servants: 50 hunger
< 25 => 10.0f, // 21-25 servants: 100 hunger
_ => 15.0f, // Above 25 servants: 150 hunger
};
var hungerPerArmyUseAdjusted = component.HungerPerArmyUse * multiplier;
//make sure the hunger doesn't go into the negatives
if (_hunger.GetHunger(hunger) < hungerPerArmyUseAdjusted)
if (_hunger.GetHunger(hunger) < component.HungerPerArmyUse)
{
_popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, uid);
return;
}
args.Handled = true;
_hunger.ModifyHunger(uid, - hungerPerArmyUseAdjusted, hunger);
// DeltaV - end to the modified cost of Raise Army
_hunger.ModifyHunger(uid, -component.HungerPerArmyUse, hunger);
var servant = Spawn(component.ArmyMobSpawnId, Transform(uid).Coordinates);
var comp = EnsureComp<RatKingServantComponent>(servant);
comp.King = uid;
@@ -1,3 +1,4 @@
using Content.Server._DV.Psionics.Systems;
using Content.Server.Administration.Managers;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
@@ -13,6 +14,7 @@ using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.StationEvents.Components;
using Content.Server.Speech.Components;
using Content.Shared._DV.Psionics.Components; // DeltaV
using Content.Shared.Body.Components;
using Content.Shared.CombatMode;
using Content.Shared.CombatMode.Pacification;
@@ -25,6 +27,7 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.NPC.Components; // DeltaV
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components;
@@ -69,6 +72,7 @@ public sealed partial class ZombieSystem
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly PsionicSystem _psionic = default!; // DeltaV
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
@@ -130,9 +134,6 @@ public sealed partial class ZombieSystem
//you're a real zombie now, son.
var zombiecomp = AddComp<ZombieComponent>(target);
// DeltaV - Save factions, psionics, etc. before modifications are made.
PreserveEntityComponentState((target, zombiecomp));
//we need to basically remove all of these because zombies shouldn't
//get diseases, breath, be thirst, be hungry, die in space, get double sentience, have offspring or be paraplegic.
RemComp<RespiratorComponent>(target);
@@ -145,6 +146,12 @@ public sealed partial class ZombieSystem
RemComp<ComplexInteractionComponent>(target);
RemComp<SentienceTargetComponent>(target);
// DeltaV Start - Prevent Psionic Zombies
RemComp<PotentialPsionicComponent>(target);
if (HasComp<PsionicComponent>(target))
_psionic.MindBreakEntity(target, false, true);
// DeltaV End - Prevent Psionic Zombies
//funny voice
var accentType = "zombie";
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
@@ -238,9 +245,6 @@ public sealed partial class ZombieSystem
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
_inventory.TryUnequip(target, "ears", true, true);
// DeltaV - Extra zombification removals, etc.
ZombifyEntityDV((target, zombiecomp), mobState);
//popup
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
@@ -257,6 +261,7 @@ public sealed partial class ZombieSystem
_faction.ClearFactions(target, dirty: false);
_faction.AddFaction(target, ZombieFaction);
EnsureComp<NoFriendlyFireComponent>(target); // DeltaV - prevent shitters biting other zombies
//gives it the funny "Zombie ___" name.
_nameMod.RefreshNameModifiers(target);
+1 -3
View File
@@ -78,8 +78,6 @@ namespace Content.Server.Zombies
SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
InitializeDV(); // DeltaV - Additional Subscriptions for ZombieComponent
}
private void OnBeforeRemoveAnomalyOnDeath(Entity<PendingZombieComponent> ent, ref BeforeRemoveAnomalyOnDeathEvent args)
@@ -327,7 +325,7 @@ namespace Content.Server.Zombies
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
{
_role.MindRemoveRole<ZombieRoleComponent>((args.Mind.Owner, args.Mind.Comp));
_role.MindRemoveRole<ZombieRoleComponent>((args.Mind.Owner, args.Mind.Comp));
}
}
}
@@ -0,0 +1,120 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared._DV.AccountLinking;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._DV.AccountLinking;
public sealed class LinkAccountManager : IPostInjectInit
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
private readonly Dictionary<NetUserId, TimeSpan> _lastRequest = new();
private readonly TimeSpan _minimumWait = TimeSpan.FromSeconds(0.5);
private readonly Dictionary<NetUserId, SharedPatronFull> _connected = new();
private readonly List<SharedPatron> _allPatrons = [];
private async Task LoadData(ICommonSession player, CancellationToken cancel)
{
var patron = await _db.GetPatron(player.UserId, cancel);
var linked = await _db.HasLinkedAccount(player.UserId, cancel);
cancel.ThrowIfCancellationRequested();
var tier = patron?.Tier;
var sharedTier = tier == null
? null
: new SharedPatronTier(
tier.ShowOnCredits,
tier.Name
);
_connected[player.UserId] = new SharedPatronFull(sharedTier, linked);
}
private void FinishLoad(ICommonSession player)
{
SendPatronStatus(player);
}
private void ClientDisconnected(ICommonSession player)
{
_connected.Remove(player.UserId);
}
private void SendPatronStatus(ICommonSession player)
{
var connected = _connected.GetValueOrDefault(player.UserId);
var msg = new LinkAccountStatusMsg { Patron = connected, };
_net.ServerSendMessage(msg, player.Channel);
SendPatrons(player);
}
private void OnRequest(LinkAccountRequestMsg message)
{
var user = message.MsgChannel.UserId;
var time = _timing.RealTime;
if (_lastRequest.TryGetValue(user, out var last) &&
last + _minimumWait > time)
{
return;
}
_lastRequest[user] = time;
var code = Guid.NewGuid();
_db.SetLinkingCode(user, code);
var response = new LinkAccountCodeMsg { Code = code };
_net.ServerSendMessage(response, message.MsgChannel);
}
public async Task RefreshAllPatrons()
{
var patrons = await _db.GetAllPatrons();
_allPatrons.Clear();
foreach (var patron in patrons)
{
_allPatrons.Add(new SharedPatron(patron.Player.LastSeenUserName, patron.Tier.Name));
}
}
public void SendPatronsToAll()
{
var msg = new PatronListMsg { Patrons = _allPatrons };
_net.ServerSendToAll(msg);
}
public void SendPatrons(ICommonSession player)
{
var msg = new PatronListMsg { Patrons = _allPatrons };
_net.ServerSendMessage(msg, player.Channel);
}
public SharedPatronFull? GetPatron(ICommonSession player)
{
return GetPatron(player.UserId);
}
public SharedPatronFull? GetPatron(NetUserId userId)
{
return _connected.GetValueOrDefault(userId);
}
void IPostInjectInit.PostInject()
{
_net.RegisterNetMessage<LinkAccountRequestMsg>(OnRequest);
_net.RegisterNetMessage<LinkAccountCodeMsg>();
_net.RegisterNetMessage<LinkAccountStatusMsg>();
_net.RegisterNetMessage<PatronListMsg>();
_userDb.AddOnLoadPlayer(LoadData);
_userDb.AddOnFinishLoad(FinishLoad);
_userDb.AddOnPlayerDisconnect(ClientDisconnected);
}
}
@@ -0,0 +1,43 @@
using Content.Server.GameTicking;
namespace Content.Server._DV.AccountLinking;
public sealed class LinkAccountSystem : EntitySystem
{
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
public override void Initialize()
{
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
ReloadPatrons();
}
private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev)
{
switch (ev.New)
{
case GameRunLevel.InRound:
break;
case GameRunLevel.PreRoundLobby:
case GameRunLevel.PostRound:
ReloadPatrons();
break;
}
}
private async void ReloadPatrons()
{
try
{
await _linkAccount.RefreshAllPatrons();
_linkAccount.SendPatronsToAll();
}
catch (Exception e)
{
Log.Error($"Error reloading Patrons list:\n{e}");
}
}
}
@@ -1,73 +0,0 @@
using Content.Shared.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Map;
namespace Content.Server.Audio;
/// <summary>
/// Extends upstream's Content.Server/Audio/ServerGlobalSoundSystem.cs.
/// </summary>
public sealed partial class ServerGlobalSoundSystem
{
/// <summary>
/// DeltaV - Plays a sound globally for all players, no matter where they are.
/// </summary>
/// <param name="specifier"></param>
/// <param name="audioParams"></param>
public void PlayGlobal(ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(specifier, audioParams);
RaiseNetworkEvent(msg);
}
public void StopGlobalEventMusic(StationEventMusicType type)
{
var msg = new StopStationEventMusic(type);
RaiseNetworkEvent(msg);
}
public void DispatchGlobalEventMusic(SoundSpecifier sound, StationEventMusicType type)
{
DispatchGlobalEventMusic(_audio.ResolveSound(sound), type);
}
public void DispatchGlobalEventMusic(ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.WithVolume(-8);
var msg = new StationEventMusicEvent(specifier, type, audio);
RaiseNetworkEvent(msg);
}
/// <summary>
/// DeltaV - Plays a sound globally for all players on a specified Map.
/// </summary>
/// <param name="specifier"></param>
/// <param name="audioParams"></param>
public void PlayGlobalOnMap(MapId map, ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(specifier, audioParams);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
public void StopMapEventMusic(MapId map, StationEventMusicType type)
{
var msg = new StopStationEventMusic(type);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
public void DispatchMapEventMusic(MapId map, SoundSpecifier sound, StationEventMusicType type)
{
DispatchMapEventMusic(map, _audio.ResolveSound(sound), type);
}
public void DispatchMapEventMusic(MapId map, ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.WithVolume(-8);
var msg = new StationEventMusicEvent(specifier, type, audio);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
}
@@ -6,6 +6,12 @@ namespace Content.Server._DV.CartridgeLoader.Cartridges;
[RegisterComponent, Access(typeof(NanoChatCartridgeSystem))]
public sealed partial class NanoChatCartridgeComponent : Component
{
/// <summary>
/// Station entity to keep track of.
/// </summary>
[DataField]
public EntityUid? Station;
/// <summary>
/// The NanoChat card to keep track of.
/// </summary>
@@ -2,8 +2,9 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.CartridgeLoader;
using Content.Server.Power.Components;
using Content.Server.Radio;
using Content.Server.Radio.EntitySystems;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.CCVar;
@@ -29,7 +30,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SharedNanoChatSystem _nanoChat = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly StationSystem _station = default!;
private EntityQuery<PdaComponent> _pdaQuery;
private EntityQuery<NanoChatCardComponent> _cardQuery;
@@ -455,7 +456,9 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{
// First verify we can send from this device
var channel = _prototype.Index(sender.Comp.RadioChannel);
if (!CanSend(sender))
var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender);
RaiseLocalEvent(ref sendAttemptEvent);
if (sendAttemptEvent.Cancelled)
return (true, new List<Entity<NanoChatCardComponent>>());
var foundRecipients = new List<Entity<NanoChatCardComponent>>();
@@ -484,19 +487,26 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (receiverCart.Card != recipient.Owner)
continue;
var receiverMapId = Transform(receiverUid).MapID;
var senderMapId = Transform(sender).MapID;
// Check if devices are on same station/map
var recipientStation = _station.GetOwningStation(receiverUid);
var senderStation = _station.GetOwningStation(sender);
// Must be on the same map unless long range is allowed.
if (!channel.LongRange && receiverMapId != senderMapId)
// Both entities must be on a station
if (recipientStation == null || senderStation == null)
continue;
// Check if telecommunications are active on both ends
if (!_radio.HasActiveServer(receiverMapId, receiverCart.RadioChannel) || !_radio.HasActiveServer(senderMapId, sender.Comp.RadioChannel))
// Must be on same map/station unless long range allowed
if (!channel.LongRange && recipientStation != senderStation)
continue;
// Needs telecomms
if (!HasActiveServer(senderStation.Value) || !HasActiveServer(recipientStation.Value))
continue;
// Check if recipient can receive
if (!CanReceive(sender, receiverUid))
var receiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, receiverUid);
RaiseLocalEvent(ref receiveAttemptEv);
if (receiveAttemptEv.Cancelled)
continue;
// Found valid cartridge that can receive
@@ -509,26 +519,21 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
}
/// <summary>
/// Tests if a NanoChat cartridge can send messages
/// Checks if there are any active telecomms servers on the given station
/// </summary>
/// <param name="sender">The NanoChat cartridge trying to send</param>
private bool CanSend(Entity<NanoChatCartridgeComponent> sender)
private bool HasActiveServer(EntityUid station)
{
var sendAttemptEvent = new RadioSendAttemptEvent(_prototype.Index(sender.Comp.RadioChannel), sender);
RaiseLocalEvent(ref sendAttemptEvent);
return !sendAttemptEvent.Cancelled;
}
// I have no idea why this isn't public in the RadioSystem
var query =
EntityQueryEnumerator<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent>();
/// <summary>
/// Tests if a receiver can receive from a given NanoChat cartridge
/// </summary>
/// <param name="sender">The NanoChat cartridge trying to send</param>
/// <param name="receiver">The receiver cartridge trying to receive</param>
private bool CanReceive(Entity<NanoChatCartridgeComponent> sender, EntityUid receiver)
{
var receiveAttemptEv = new RadioReceiveAttemptEvent(_prototype.Index(sender.Comp.RadioChannel), sender, receiver);
RaiseLocalEvent(ref receiveAttemptEv);
return !receiveAttemptEv.Cancelled;
while (query.MoveNext(out var uid, out _, out _, out var power))
{
if (_station.GetOwningStation(uid) == station && power.Powered)
return true;
}
return false;
}
/// <summary>
@@ -683,15 +688,16 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
private void UpdateUI(Entity<NanoChatCartridgeComponent> ent, EntityUid loader)
{
List<NanoChatRecipient>? contacts;
if (CanSend(ent) && _radio.HasActiveServer(Transform(ent).MapID, ent.Comp.RadioChannel))
if (_station.GetOwningStation(loader) is { } station)
{
ent.Comp.Station = station;
contacts = [];
var query = AllEntityQuery<NanoChatCardComponent, IdCardComponent>();
while (query.MoveNext(out var entityId, out var nanoChatCard, out var idCardComponent))
{
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName)
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName && _station.GetOwningStation(entityId) == station)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName));
}
@@ -700,7 +706,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var borgQuery = AllEntityQuery<NanoChatCardComponent, BorgChassisComponent>();
while (borgQuery.MoveNext(out var borgId, out var borgChatCard, out var _))
{
if (borgChatCard.ListNumber && borgChatCard.Number is uint nanoChatNumber)
if (borgChatCard.ListNumber && borgChatCard.Number is uint nanoChatNumber && _station.GetOwningStation(borgId) == station)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, MetaData(borgId).EntityName));
}
@@ -709,7 +715,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var aiQuery = AllEntityQuery<NanoChatCardComponent, StationAiHeldComponent>();
while (aiQuery.MoveNext(out var aiId, out var aiChatCard, out var _))
{
if (aiChatCard.ListNumber && aiChatCard.Number is uint nanoChatNumber)
if (aiChatCard.ListNumber && aiChatCard.Number is uint nanoChatNumber && _station.GetOwningStation(aiId) == station)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, MetaData(aiId).EntityName));
}
@@ -1,63 +0,0 @@
using Content.Server._DV.CosmicCult.Components;
using Content.Shared._DV.CosmicCult.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.CosmicCult.Abilities.Colossus;
public sealed class CosmicColossusBuffsSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MultiplyAttackRateOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleAttackRate);
SubscribeLocalEvent<FlatAttackBonusOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleDamageBonus);
SubscribeLocalEvent<MultiplyCorruptingSpeedOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleCorruptingSpeed);
SubscribeLocalEvent<HealOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleHeal);
}
private void HandleAttackRate(Entity<MultiplyAttackRateOnSupercriticalComponent> ent,
ref CosmicColossusEffigySupercriticalEvent args)
{
if (TryComp<MeleeWeaponComponent>(ent, out var weapon))
{
weapon.AttackRate *= ent.Comp.Multiplier;
}
}
private void HandleDamageBonus(Entity<FlatAttackBonusOnSupercriticalComponent> ent,
ref CosmicColossusEffigySupercriticalEvent args)
{
if (!TryComp<CosmicColossusComponent>(ent, out var colossusComp))
return;
colossusComp.BonusDamage +=
new DamageSpecifier(_proto.Index(ent.Comp.BonusDamageType), ent.Comp.BonusDamage);
}
private void HandleCorruptingSpeed(Entity<MultiplyCorruptingSpeedOnSupercriticalComponent> ent,
ref CosmicColossusEffigySupercriticalEvent args)
{
if (TryComp<CosmicCorruptingComponent>(ent, out var corrupting))
{
corrupting.CorruptionSpeed *= ent.Comp.Multiplier;
}
}
private void HandleHeal(Entity<HealOnSupercriticalComponent> ent, ref CosmicColossusEffigySupercriticalEvent args)
{
if (TryComp<DamageableComponent>(ent, out var damageable))
{
_damage.TryChangeDamage(ent.Owner,
-damageable.Damage * Math.Abs(ent.Comp.DamageFractionHealed), // just in case someone sets a negative value in YML
true);
}
}
}
@@ -1,8 +0,0 @@
using Content.Server._DV.CosmicCult.Components;
namespace Content.Server._DV.CosmicCult.Abilities.Colossus;
[ByRefEvent]
public readonly record struct CosmicColossusEffigySupercriticalEvent(
Entity<CosmicEffigyComponent> Effigy
);
@@ -1,24 +1,15 @@
using System.Numerics;
using Content.Server._DV.CosmicCult.Abilities.Colossus;
using Content.Server._DV.CosmicCult.Components;
using Content.Server.Actions;
using Content.Server.Chat.Managers;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Popups;
using Content.Shared._DV.CosmicCult;
using Content.Shared._DV.CosmicCult.Components;
using Content.Shared.Anomaly.Components;
using Content.Shared.Maps;
using Content.Shared.Mind;
using Content.Shared.Popups;
using Content.Shared.Warps;
using Robust.Server.Audio;
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._DV.CosmicCult.Abilities;
@@ -32,93 +23,12 @@ public sealed class CosmicEffigySystem : EntitySystem
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly CosmicCultObjectiveSystem _cultObjective = default!;
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CosmicColossusComponent, EventCosmicColossusEffigy>(OnColossusEffigy);
SubscribeLocalEvent<CosmicEffigyComponent, AnomalySupercriticalEvent>(OnSupercritical);
SubscribeLocalEvent<CosmicEffigyComponent, AnomalyShutdownEvent>(OnAnomShutdown);
}
private void OnAnomShutdown(Entity<CosmicEffigyComponent> ent, ref AnomalyShutdownEvent args)
{
if (args.Forced || args.Supercritical || !Exists(ent.Comp.Colossus) || !TryComp<CosmicColossusComponent>(ent.Comp.Colossus, out var colossusComp))
return;
colossusComp.DeathTimer = _time.CurTime;
colossusComp.Timed = true;
}
private void OnSupercritical(Entity<CosmicEffigyComponent> ent, ref AnomalySupercriticalEvent args)
{
if (!Exists(ent.Comp.Colossus)
|| !TryComp<CosmicColossusComponent>(ent.Comp.Colossus, out var colossusComp)
|| !_mind.TryGetMind(ent.Comp.Colossus.Value, out _, out var mind))
return;
var colossus = ent.Comp.Colossus.Value;
var ev = new CosmicColossusEffigySupercriticalEvent(ent);
RaiseLocalEvent(colossus, ref ev);
var transform = Transform(ent.Comp.Colossus.Value);
Spawn(colossusComp.BuffVfx, transform.Coordinates);
if (colossusComp.CompletedEffigies == 0)
{
_audio.PlayStatic(colossusComp.ReawakenSfx,
Filter.BroadcastMap(transform.MapID),
transform.Coordinates,
true);
}
else
{
_audio.PlayPvs(colossusComp.ReawakenSfx, ent);
}
colossusComp.CompletedEffigies += 1;
if (colossusComp.CompletedEffigies >= colossusComp.MaxEffigies)
{
colossusComp.Timed = false;
_popup.PopupEntity(Loc.GetString("colossus-buff-final-popup"), colossus, PopupType.Large);
return;
}
_popup.PopupEntity(Loc.GetString("colossus-buff-popup"), colossus, PopupType.Large);
var objIndex = mind.Objectives.FindIndex(HasComp<CosmicEffigyConditionComponent>);
if (objIndex == -1 ||
!TryComp<CosmicEffigyConditionComponent>(mind.Objectives[objIndex], out var conditionComp))
{
Log.Error($"Failed to find effigy objective on {ToPrettyString(colossus)}!");
return;
}
var objective = mind.Objectives[objIndex];
if (_cultObjective.RandomizeEffigyTarget(objective, conditionComp, setDescription: true) is not {} nextTarget)
{
Log.Error("Failed to randomize effigy objective location!");
return;
}
if (mind.UserId is { } userId)
{
_chat.DispatchServerMessage(_player.GetSessionById(userId), Loc.GetString("colossus-next-target", ("location", nextTarget)));
}
_codeCondition.SetCompleted(objective, false);
colossusComp.EffigyPlaceActionEntity = _actions.AddAction(colossus, colossusComp.EffigyPlaceAction);
colossusComp.DeathTimer = _time.CurTime + colossusComp.DeathWaitEffigy;
colossusComp.Timed = true;
}
private void OnColossusEffigy(Entity<CosmicColossusComponent> ent, ref EventCosmicColossusEffigy args)
@@ -128,16 +38,8 @@ public sealed class CosmicEffigySystem : EntitySystem
_actions.RemoveAction(ent.Owner, ent.Comp.EffigyPlaceActionEntity);
_codeCondition.SetCompleted(ent.Owner, ent.Comp.EffigyObjective);
var effigy = Spawn(ent.Comp.EffigyPrototype, pos);
Spawn(ent.Comp.EffigyPrototype, pos);
ent.Comp.Timed = false;
if (!TryComp<CosmicEffigyComponent>(effigy, out var effigyComp))
{
Log.Error("Colossus tried to place Effigy prototype missing CosmicEffigyComponent!");
return;
}
effigyComp.Colossus = ent.Owner;
}
private bool VerifyPlacement(Entity<CosmicColossusComponent> ent, out EntityCoordinates outPos)
@@ -1,56 +0,0 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.CosmicCult.Components;
/// <summary>
/// The owning colossus's attack rate will be multiplied by
/// the given multiplier once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class MultiplyAttackRateOnSupercriticalComponent : Component
{
[DataField]
public float Multiplier = 1.1f;
}
/// <summary>
/// The owning colossus's corrupting speed will be multiplied by
/// the given multiplier once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class MultiplyCorruptingSpeedOnSupercriticalComponent : Component
{
[DataField]
public float Multiplier = 0.9f;
}
/// <summary>
/// The owning colossus will receive a flat bonus to
/// its melee attack damage once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class FlatAttackBonusOnSupercriticalComponent : Component
{
[DataField]
public FixedPoint2 BonusDamage = 10;
[DataField]
public ProtoId<DamageTypePrototype> BonusDamageType = "Blunt";
}
/// <summary>
/// The owning colossus will be healed for `-CurrentDamage * Abs(DamageFractionHealed)` damage
/// once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class HealOnSupercriticalComponent : Component
{
[DataField]
public float DamageFractionHealed = 1.0f;
}
@@ -1,4 +1,3 @@
using Content.Server._DV.CosmicCult.Abilities.Colossus;
using Content.Server._DV.CosmicCult.EntitySystems;
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
@@ -6,7 +5,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server._DV.CosmicCult.Components;
[RegisterComponent, Access(typeof(CosmicCorruptingSystem), typeof(CosmicColossusBuffsSystem))]
[RegisterComponent, Access(typeof(CosmicCorruptingSystem))]
[AutoGenerateComponentPause]
public sealed partial class CosmicCorruptingComponent : Component
{
@@ -1,19 +0,0 @@
using Content.Server._DV.CosmicCult.Abilities;
using Content.Server._DV.CosmicCult.EntitySystems;
namespace Content.Server._DV.CosmicCult.Components;
/// <summary>
/// Component for cosmic effigy anomalies, spawned by Cosmic Colossi.
/// </summary>
/// <seelso cref="CosmicEffigySystem"/>.
/// <seelso cref="CosmicColossusSystem"/>.
[RegisterComponent]
public sealed partial class CosmicEffigyComponent : Component
{
/// <summary>
/// The colossus that placed this effigy.
/// </summary>
[DataField]
public EntityUid? Colossus;
}
@@ -35,12 +35,6 @@ public sealed class CosmicCultObjectiveSystem : EntitySystem
if (args.Cancelled || !_roles.MindHasRole<CosmicColossusRoleComponent>(args.MindId))
return;
if (RandomizeEffigyTarget(uid, comp) is null)
args.Cancelled = true;
}
public string? RandomizeEffigyTarget(EntityUid uid, CosmicEffigyConditionComponent comp, bool setDescription = false)
{
var warps = new List<EntityUid>();
var query = EntityQueryEnumerator<WarpPointComponent>();
var effigyBlacklist = comp.Blacklist;
@@ -57,22 +51,10 @@ public sealed class CosmicCultObjectiveSystem : EntitySystem
if (warps.Count <= 0)
{
return null;
args.Cancelled = true;
return;
}
var newWarp = _random.Pick(warps);
var warpComp = Comp<WarpPointComponent>(newWarp);
comp.EffigyTarget = newWarp;
if (setDescription)
{
_metaData.SetEntityDescription(uid,
warpComp.Location != null
? Loc.GetString("objective-condition-effigy", ("location", warpComp.Location))
: Loc.GetString("objective-condition-effigy-no-target"));
}
return warpComp.Location;
comp.EffigyTarget = _random.Pick(warps);
}
private void OnEffigyAfterAssign(EntityUid uid, CosmicEffigyConditionComponent comp, ref ObjectiveAfterAssignEvent args)
@@ -521,7 +521,7 @@ public sealed class CosmicCultRuleSystem : GameRuleSystem<CosmicCultRuleComponen
{
finComp.CurrentState = FinaleState.Unavailable;
_popup.PopupCoordinates(Loc.GetString("cosmiccult-monument-powerdown"), Transform(gameruleMonument).Coordinates, PopupType.Large);
_sound.StopGlobalEventMusic(StationEventMusicType.CosmicCult);
_sound.StopStationEventMusic(gameruleMonument, StationEventMusicType.CosmicCult);
_monument.UpdateMonumentAppearance(gameruleMonument, false);
}
@@ -82,9 +82,11 @@ public sealed partial class CosmicCultSystem : SharedCosmicCultSystem
_corrupting.SetCorruptionTime((uid, corruptingComp), TimeSpan.FromSeconds(1));
_appearance.SetData(uid, MonumentVisuals.FinaleReached, 2);
_sound.DispatchGlobalEventMusic(comp.SelectedSong, StationEventMusicType.CosmicCult);
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-finale-location", ("location", indicatedLocation)),
null, false, null, Color.FromHex("#cae8e8"));
_sound.DispatchStationEventMusic(uid, comp.SelectedSong, StationEventMusicType.CosmicCult);
_chatSystem.DispatchStationAnnouncement(uid,
Loc.GetString("cosmiccult-finale-location", ("location", indicatedLocation)),
null, false, null,
Color.FromHex("#cae8e8"));
var stationUid = _station.GetStationInMap(Transform(uid).MapID);
if (stationUid != null)
@@ -126,8 +128,8 @@ public sealed partial class CosmicCultSystem : SharedCosmicCultSystem
if (stationUid != null)
_alert.SetLevel(stationUid.Value, "green", true, true, true);
_sound.PlayGlobal(_audio.ResolveSound(comp.CancelEventSound));
_sound.StopGlobalEventMusic(StationEventMusicType.CosmicCult);
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(comp.CancelEventSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.CosmicCult);
uid.Comp.CurrentState = FinaleState.ReadyFinale;
if (TryComp<CosmicCorruptingComponent>(uid, out var corruptingComp))
@@ -8,7 +8,6 @@ using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -26,7 +25,6 @@ public sealed class CosmicChantrySystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly NavMapSystem _navMap = default!;
/// <summary>
@@ -81,11 +79,11 @@ public sealed class CosmicChantrySystem : EntitySystem
comp.SpawnTimer = _timing.CurTime + comp.SpawningTime;
comp.CountdownTimer = _timing.CurTime + comp.EventTime;
var targetMap = _transform.GetMapId(ent.Owner);
_sound.PlayGlobalOnMap(targetMap, _audio.ResolveSound(comp.ChantryAlarm));
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-chantry-location",
("location", indicatedLocation)), null, false, null, Color.FromHex("#cae8e8"));
_sound.PlayGlobalOnStation(ent, _audio.ResolveSound(comp.ChantryAlarm));
_chatSystem.DispatchStationAnnouncement(ent,
Loc.GetString("cosmiccult-chantry-location", ("location", indicatedLocation)),
null, false, null,
Color.FromHex("#cae8e8"));
if (_mind.TryGetMind(comp.InternalVictim, out _, out var mind))
mind.PreventGhosting = true;
@@ -13,7 +13,6 @@ using Content.Shared.Popups;
using Content.Shared.Station.Components;
using Content.Shared.Throwing;
using Content.Shared.Warps;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Components;
@@ -42,7 +41,6 @@ public sealed class CosmicColossusSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<CosmicColossusComponent, ComponentInit>(OnSpawn);
SubscribeLocalEvent<CosmicColossusComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<CosmicColossusComponent, MeleeHitEvent>(OnMeleeHit);
}
public override void Update(float frameTime)
@@ -84,7 +82,7 @@ public sealed class CosmicColossusSystem : EntitySystem
private void OnSpawn(Entity<CosmicColossusComponent> ent, ref ComponentInit args) // I WANT THIS BIG GUY HURLED TOWARDS THE STATION
{
ent.Comp.DeathTimer = _timing.CurTime + ent.Comp.DeathWaitSpawn;
ent.Comp.DeathTimer = _timing.CurTime + ent.Comp.DeathWait;
var station = _station.GetStationInMap(Transform(ent).MapID);
if (TryComp<StationDataComponent>(station, out var stationData))
{
@@ -117,9 +115,4 @@ public sealed class CosmicColossusSystem : EntitySystem
RemComp<WarpPointComponent>(ent);
RemComp<CosmicCorruptingComponent>(ent);
}
private void OnMeleeHit(Entity<CosmicColossusComponent> colossus, ref MeleeHitEvent args)
{
args.BonusDamage += colossus.Comp.BonusDamage;
}
}
@@ -79,13 +79,13 @@ public sealed class MonumentSystem : SharedMonumentSystem
{
comp.SongTimer = null;
if (comp.SelectedSong is { } song)
_sound.DispatchGlobalEventMusic(song, StationEventMusicType.CosmicCult);
_sound.DispatchStationEventMusic(uid, song, StationEventMusicType.CosmicCult);
}
if (comp.CurrentState == FinaleState.ActiveFinale && comp.FinaleAnnounceCheck && comp.FinaleTimer - _timing.CurTime < comp.VisualsThreshold)
{
_appearance.SetData(uid, MonumentVisuals.FinaleReached, 3);
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-announce-finale-warning"), null, false, null, Color.FromHex("#cae8e8"));
_chatSystem.DispatchStationAnnouncement(uid, Loc.GetString("cosmiccult-announce-finale-warning"), null, false, null, Color.FromHex("#cae8e8"));
comp.FinaleAnnounceCheck = false;
}
@@ -90,7 +90,6 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
private void OnPowerChanged(Entity<DeepFryerComponent> ent, ref PowerChangedEvent args)
{
UpdateAppearance(ent);
ResetCookingItemsStartTime(ent);
}
private void OnThrowHitBy(Entity<DeepFryerComponent> ent, ref ThrowHitByEvent args)
@@ -675,26 +674,4 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
// Add the split solution to the food
Solution.AddSolution(foodSolutionEnt.Value, transferredSolution);
}
/// <summary>
/// Resets the starttime for cooking items (on power change)
/// </summary>
private void ResetCookingItemsStartTime(Entity<DeepFryerComponent> ent)
{
if (_power.IsPowered(ent.Owner) && !ent.Comp.WasPreviouslyPowered)
{
// Power just came back on - reset all cooking timers
foreach (var itemUid in ent.Comp.CookingItems.Keys.ToList())
{
var cookingItem = ent.Comp.CookingItems[itemUid];
cookingItem.TimeStarted = _timing.CurTime;
ent.Comp.CookingItems[itemUid] = cookingItem;
}
ent.Comp.WasPreviouslyPowered = true;
}
else if (!_power.IsPowered(ent.Owner) && ent.Comp.WasPreviouslyPowered)
{
ent.Comp.WasPreviouslyPowered = false;
}
}
}
@@ -0,0 +1,212 @@
using Content.Server.Cloning;
using Content.Server.Mind;
using Content.Server.Station.Systems;
using Content.Shared._DV.Psionics.Components;
using Content.Shared._DV.Psionics.Components.PsionicPowers;
using Content.Shared._DV.Psionics.Systems.PsionicPowers;
using Content.Shared._DV.Species;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.Preferences;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server._DV.Psionics.Systems.PsionicPowers;
public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly PvsOverrideSystem _pvsOverride = default!;
// holy initialize performance? but better for it to happen once than the double dict lookup every tick!!
private EntityQuery<FracturedFormBodyComponent> _bodyQuery;
private EntityQuery<SleepingComponent> _sleepingQuery;
public override void Initialize()
{
base.Initialize();
_bodyQuery = GetEntityQuery<FracturedFormBodyComponent>();
_sleepingQuery = GetEntityQuery<SleepingComponent>();
}
protected override void OnPowerInit(Entity<FracturedFormPowerComponent> power, ref MapInitEvent args)
{
base.OnPowerInit(power, ref args);
// The next random swap is between 5 and 20 minutes.
var randomTime = Random.Next(power.Comp.NextSwapMinTime, power.Comp.NextSwapMaxTime);
power.Comp.NextSwap = Timing.CurTime + randomTime;
power.Comp.NextVoluntarySwap = Timing.CurTime + power.Comp.VoluntarySwapCooldown;
// Don't generate a new body if we're already part of a network.
if (HasComp<FracturedFormBodyComponent>(power))
return;
// Don't make bodies if there is no body. This is solely for test fails.
if (!HasComp<BodyComponent>(power))
return;
var bodyComp = AddComp<FracturedFormBodyComponent>(power);
bodyComp.ControllingForm = power.Owner;
power.Comp.Bodies.Add(power);
var body = GenerateForm(power);
// hide the SSD indicator.
if (SsdQuery.TryComp(body, out var ssdComp))
ssdComp.IsSSD = false;
}
private EntityUid GenerateForm(Entity<FracturedFormPowerComponent> original)
{
// Form:
// - Same appearance as original
// - Different apperance, still humanoid
// Equipment:
// - Same as original body
// - Nude and helpless
var xform = Transform(original);
var hasGear = Random.Prob(original.Comp.HasGearChance);
if (Random.Prob(original.Comp.DifferentSpeciesChance) || !_cloning.TryCloning(original, _transform.GetMapCoordinates(original), hasGear ? original.Comp.CopyClothed : original.Comp.CopyNaked, out var newBody)) // Slightly lower chance to copy the original body
{
// Either the dice rolled poorly, or the cloning failed. Either way, make a new body instead. (Or try to)
var validSpecies = new List<ProtoId<SpeciesPrototype>>();
var speciesPrototypes = _prototype.EnumeratePrototypes<SpeciesPrototype>();
foreach (var proto in speciesPrototypes)
{
var speciesEntityPrototype = _prototype.Index<EntityPrototype>(proto.Prototype);
// If they have the PotentialPsionicComponent, they can be psionic.
if (proto.RoundStart && speciesEntityPrototype.TryGetComponent<PotentialPsionicComponent>(out _, Factory) && !SpeciesHiderSystem.IsHidden(proto.ID))
validSpecies.Add(proto.ID);
}
var species = Random.Pick(validSpecies);
var character = HumanoidCharacterProfile.RandomWithSpecies(species);
newBody = _stationSpawning.SpawnPlayerMob(xform.Coordinates, hasGear ? original.Comp.VisitorJob : original.Comp.NakedJob, character, _station.GetOwningStation(original.Owner));
if (newBody is not { } bodyV || Deleted(bodyV))
{
Log.Error($"Failed to create a new body for {ToPrettyString(original)}. This is a bug.");
return EntityUid.Invalid;
}
}
if (newBody is not { } body || Deleted(body))
return default!;
var bodyComp = AddComp<FracturedFormBodyComponent>(body);
original.Comp.Bodies.Add(body);
bodyComp.ControllingForm = original.Owner;
if (_player.TryGetSessionByEntity(original, out var session))
_pvsOverride.AddSessionOverride(body, session);
Dirty(original);
return body;
}
private bool TryGetValidBody(Entity<FracturedFormPowerComponent> psionic, out EntityUid validBody)
{
foreach (var body in psionic.Comp.Bodies)
{
if (!IsValidBody(psionic, body))
continue;
validBody = body;
return true;
}
validBody = default;
return false;
}
private void Swap(Entity<FracturedFormPowerComponent> psionic)
{
if (!TryGetValidBody(psionic, out var targetBody))
return;
_audio.PlayPvs(psionic.Comp.SwapSound, psionic);
// Transfer mind if present
if (MindContainerQuery.TryComp(psionic, out var mindContainer) && mindContainer.Mind.HasValue)
_mind.TransferTo(mindContainer.Mind.Value, targetBody);
// Wake up the new body
Sleeping.TryWaking(targetBody);
// Remove the action.
Action.RemoveAction(psionic.Comp.ActionEntity);
// Create new component on target and copy data
var duplicate = EnsureComp<FracturedFormPowerComponent>(targetBody);
duplicate.Bodies = psionic.Comp.Bodies;
// Update all body references
foreach (var body in duplicate.Bodies)
{
if (_bodyQuery.TryComp(body, out var bodyComp))
bodyComp.ControllingForm = targetBody;
}
if (_player.TryGetSessionByEntity(targetBody, out var session))
{
_pvsOverride.AddSessionOverride(psionic, session);
_pvsOverride.RemoveSessionOverride(targetBody, session);
}
RemCompDeferred<FracturedFormPowerComponent>(psionic);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
List<Entity<FracturedFormPowerComponent>> swapTargets = [];
var entities = EntityQueryEnumerator<FracturedFormPowerComponent, MobStateComponent>();
while (entities.MoveNext(out var uid, out var comp, out var mobState))
{
// Check sleep warning
if (!comp.SleepWarned && Timing.CurTime > comp.NextSwap - comp.WarningTimeBeforeSleep)
{
comp.SleepWarned = true;
Popup.PopupEntity(Loc.GetString("psionic-power-fractured-form-sleepy"), uid, uid, PopupType.LargeCaution);
Chat.TryEmoteWithChat(uid, "Yawn");
}
// Swap check
if ((_sleepingQuery.HasComp(uid) || MobState.IsIncapacitated(uid, mobState)) && Timing.CurTime > comp.NextVoluntarySwap
|| Timing.CurTime > comp.NextSwap)
swapTargets.Add((uid, comp));
}
foreach (var target in swapTargets)
{
Swap(target);
}
// Process bodies
var bodies = EntityQueryEnumerator<FracturedFormBodyComponent, MobStateComponent>();
while (bodies.MoveNext(out var uid, out var comp, out var mobState))
{
// Put to sleep if no sleeping component and no mind
if (!_sleepingQuery.HasComp(uid) && !_mind.GetMind(uid).HasValue && !FracturedQuery.HasComp(uid))
Sleeping.TrySleeping((uid, mobState));
// Cleanup invalid bodies
if (!comp.ControllingForm.IsValid()
|| Deleted(comp.ControllingForm)
|| !FracturedQuery.HasComp(comp.ControllingForm))
{
RemCompDeferred<FracturedFormBodyComponent>(uid);
}
}
}
}
@@ -1,9 +1,9 @@
using Content.Server.Power.Components;
using Content.Server._DV.StationEvents.Events;
using Content.Server.StationEvents.Events;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.StationEvents.Components;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// When fired, turns off power on the station for a few seconds, playing <see cref="EpsilonEventRuleComponent.PowerOffSound"/>
@@ -1,15 +1,14 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.StationEvents.Events;
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Server.AlertLevel;
using Content.Server.Audio;
using Content.Server._DV.StationEvents.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server._DV.StationEvents.Events;
namespace Content.Server.StationEvents.Events
{
public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleComponent>
{
[Dependency] private readonly ApcSystem _apcSystem = default!;
@@ -26,7 +25,7 @@ public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleCompon
return;
component.AffectedStation = chosenStation.Value;
// Plays the power off sound for those who are on the station.
// Plays the power off sound to the station.
_sound.PlayGlobalOnStation(component.AffectedStation, _audio.ResolveSound(component.PowerOffSound));
var query = AllEntityQuery<ApcComponent, TransformComponent>();
@@ -71,3 +70,4 @@ public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleCompon
_alertLevelSystem.SetLevel(component.AffectedStation, "epsilon", true, true, true);
}
}
}
+1 -2
View File
@@ -109,8 +109,7 @@ public sealed class TraitSystem : EntitySystem
JobId = jobId,
SpeciesId = speciesId,
Profile = profile,
StatusEffects = _statusEffects,
SelectedTraits = selectedTraits
StatusEffects = _statusEffects
};
foreach (var traitId in selectedTraits)
-124
View File
@@ -1,124 +0,0 @@
using Content.Server._DV.Psionics.Systems;
using Content.Server.Antag;
using Content.Shared._DV.Movement.Components;
using Content.Shared._DV.Psionics.Components;
using Content.Shared.CharacterInfo;
using Content.Shared.Cloning.Events;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NPC.Components;
using Content.Shared.Radio.Components;
using Content.Shared.Roles.Components;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
namespace Content.Server.Zombies;
public sealed partial class ZombieSystem
{
//private static readonly string MindRoleInitialInfected = "MindRoleInitialInfected";
private static readonly EntProtoId InitialInfectedFailureSurviveObjective = "InitialInfectedFailureSurviveObjective";
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly PsionicSystem _psionic = default!; // DeltaV
[Dependency] private readonly SharedJetpackSystem _jetpack = default!; // DeltaV - Prevent Jetpacks on Zombies
private void InitializeDV()
{
// This needs to be done before CloneEvent or else the proper attributes won't be restored or modified on
// the new entity.
SubscribeLocalEvent<ZombieComponent, CloningAttemptEvent>(OnBeforeUnzombify);
SubscribeLocalEvent<InitialInfectedComponent, CloningEvent>(OnInitialInfectedCloning);
}
/// <summary>
/// DeltaV - Extra things we want to ensure, remove, or take care of when someone is zombified.
/// </summary>
private void ZombifyEntityDV(Entity<ZombieComponent> ent, MobStateComponent? mobState = null)
{
// Remove innate radio
RemComp<ActiveRadioComponent>(ent); // If the zombie has an innate radio, get rid of it.
// Remove headsets in pockets
for (var i = 1; i <= 4; i++) // Arachnids have 4 pockets
{
if (_inventory.TryGetSlotEntity(ent, $"pocket{i}", out var headset) && HasComp<HeadsetComponent>(headset))
_inventory.TryUnequip(ent, $"pocket{i}", true, true);
}
// Prevent Psionic Zombies
RemComp<PotentialPsionicComponent>(ent);
_psionic.MindBreakEntity(ent.Owner, false, true);
// Prevent Jetpacks on Zombies
if (TryComp<JetpackUserComponent>(ent, out var jetpackUser))
{
if (TryComp<JetpackComponent>(jetpackUser.Jetpack, out var jetpack))
_jetpack.SetEnabled(jetpackUser.Jetpack, jetpack, false, ent);
}
RemComp<AutomaticJetpackUserComponent>(ent);
// Prevent shitters biting other zombies
EnsureComp<NoFriendlyFireComponent>(ent);
}
/// <summary>
/// DeltaV - We need to save some extra things to the ZombieComponent so we can restore them when
/// they are about to be cloned.
/// </summary>
/// <param name="ent"></param>
private void PreserveEntityComponentState(Entity<ZombieComponent> ent)
{
SaveFactionsBeforeZombification(ent);
// Zombification removes pacifism but lets track if they have it so the pacifist players will be... pacified.
ent.Comp.WasPacisfistBeforeZombification = HasComp<PacifiedComponent>(ent);
}
private void SaveFactionsBeforeZombification(Entity<ZombieComponent> ent)
{
if (!TryComp<NpcFactionMemberComponent>(ent, out var faction))
return;
ent.Comp.FactionsBeforeZombification.AddRange(faction.Factions);
}
/// <summary>
/// DeltaV - Restores certain components to their previous state that the upstream unzombify doesn't handle.
/// We need to do this before CloningEvent so the cloned body will have the updated components BEFORE cloning happens.
/// </summary>
/// <param name="ent"></param>
/// <param name="args"></param>
private void OnBeforeUnzombify(Entity<ZombieComponent> ent, ref CloningAttemptEvent args)
{
// Restore factions
_faction.ClearFactions(ent.Owner); // Should only be zombie, but might as well clear the whole list in case.
ent.Comp.FactionsBeforeZombification.ForEach(previousFaction => _faction.AddFaction(ent.Owner, previousFaction));
// Restore pacifism
if (ent.Comp.WasPacisfistBeforeZombification)
EnsureComp<PacifiedComponent>(ent);
}
/// <summary>
/// DeltaV - Adds a survival objective to Initial Infected who are cloned. Chances are that the crew has recovered, so
/// bioterrorism time is probably over.
/// </summary>
/// <param name="ent"></param>
/// <param name="args"></param>
private void OnInitialInfectedCloning(Entity<InitialInfectedComponent> ent, ref CloningEvent args)
{
// Change II objectives so they no longer have infect objectives. Now, they'll have survival objectives. Live to infect another day. :)
_mind.TryGetMind(ent.Owner, out var mindId, out var mindContainer);
// If we can't find the mind, oh well. We tried.
if (mindContainer is not { } mind)
return;
if (_role.MindHasRole<InitialInfectedRoleComponent>(mindId))
_mind.TryAddObjective(mindId, mind, InitialInfectedFailureSurviveObjective);
}
}
@@ -285,11 +285,6 @@ public sealed partial class AnomalyComponent : Component
[DataField]
public bool DeleteEntity = true;
// DeltaV - Colossus additions START
[DataField]
public bool AlwaysGrow;
// DeltaV - Colossus additions END
}
/// <summary>
@@ -313,7 +308,7 @@ public readonly record struct AnomalySupercriticalEvent(EntityUid Anomaly, float
/// <param name="Anomaly">The anomaly being shut down.</param>
/// <param name="Supercritical">Whether or not the anomaly shut down passively or via a supercritical event.</param>
[ByRefEvent]
public readonly record struct AnomalyShutdownEvent(EntityUid Anomaly, bool Supercritical, bool Forced); // DeltaV - Add Forced
public readonly record struct AnomalyShutdownEvent(EntityUid Anomaly, bool Supercritical);
/// <summary>
/// Event broadcast when an anomaly's severity is changed.
+4 -13
View File
@@ -84,7 +84,7 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
Log.Info($"Performing anomaly pulse. Entity: {ToPrettyString(uid)}");
// if we are above the growth threshold, then grow before the pulse
if (component.AlwaysGrow || component.Stability > component.GrowthThreshold) // DeltaV - Add AlwaysGrow
if (component.Stability > component.GrowthThreshold)
{
ChangeAnomalySeverity(uid, GetSeverityIncreaseFromGrowth(component), component);
}
@@ -192,8 +192,7 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
/// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
/// <param name="spawnCore">Create anomaly cores based on the result of completing an anomaly?</param>
/// <param name="logged">Whether or not the anomaly decaying/going supercritical is logged</param>
/// <param name="forced">Whether or not the anomaly shutdown was caused by component shutdown</param> // DeltaV - Add forced
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false, bool forced = false) // DeltaV - Add forced
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false)
{
if (logged)
{
@@ -208,7 +207,7 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
if (!Resolve(uid, ref component))
return;
var ev = new AnomalyShutdownEvent(uid, supercritical, forced); // DeltaV - Add forced
var ev = new AnomalyShutdownEvent(uid, supercritical);
RaiseLocalEvent(uid, ref ev, true);
if (Terminating(uid) || _net.IsClient)
@@ -346,7 +345,7 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
// if the stability is under the death threshold,
// update it every second to start killing it slowly.
if (!anomaly.AlwaysGrow && anomaly.Stability < anomaly.DecayThreshold) // DeltaV - Add AlwaysGrow
if (anomaly.Stability < anomaly.DecayThreshold)
{
ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly);
}
@@ -480,14 +479,6 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
if (!Resolve(ent, ref ent.Comp, logMissing: false))
return false;
// DeltaV - Colossus Additions START
if (ent.Comp.AlwaysGrow)
{
visual = AnomalyStabilityVisuals.Growing;
return true;
}
// DeltaV - Colossus Additions END
visual = AnomalyStabilityVisuals.Stable;
if (ent.Comp.Stability <= ent.Comp.DecayThreshold)
{
-6
View File
@@ -135,10 +135,4 @@ public sealed partial class CCVars
/// </summary>
public static readonly CVarDef<int> AdminOverlayStackMax =
CVarDef.Create("ui.admin_overlay_stack_max", 3, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// If true, ghosts will see an "(F)" button next to chat messages, which can be used to follow the sender.
/// </summary>
public static readonly CVarDef<bool> InterfaceChatFollowButton =
CVarDef.Create("ui.chat_follow_button", true, CVar.CLIENT | CVar.REPLICATED | CVar.ARCHIVE);
}
+1 -23
View File
@@ -41,13 +41,7 @@ namespace Content.Shared.Chat
[NonSerialized]
public bool Read;
/// <summary>
/// DeltaV - If not null, contains the radio channel proto ID that the message was sent on.
/// Used to replace the channel color client-side.
/// </summary>
public string? RadioChannelProtoId = null;
public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, string? radioChannelProto = null) // DeltaV - added radioChannelProto
public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
{
Channel = channel;
Message = message;
@@ -58,22 +52,6 @@ namespace Content.Shared.Chat
MessageColorOverride = colorOverride;
AudioPath = audioPath;
AudioVolume = audioVolume;
RadioChannelProtoId = radioChannelProto; // DeltaV
}
public ChatMessage(ChatMessage copyFrom)
{
Channel = copyFrom.Channel;
Message = copyFrom.Message;
WrappedMessage = copyFrom.WrappedMessage;
SenderEntity = copyFrom.SenderEntity;
SenderKey = copyFrom.SenderKey;
HideChat = copyFrom.HideChat;
MessageColorOverride = copyFrom.MessageColorOverride;
AudioPath = copyFrom.AudioPath;
AudioVolume = copyFrom.AudioVolume;
Read = copyFrom.Read;
RadioChannelProtoId = copyFrom.RadioChannelProtoId; // DeltaV
}
}
@@ -2,7 +2,6 @@ using System.Linq;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Ghost;
using Content.Shared.Interaction;
using Content.Shared.Inventory; // Harmony - for lanyards
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
@@ -294,7 +293,7 @@ namespace Content.Shared.Examine
/// If you're pushing multiple messages that should be grouped together (or ordered in some way),
/// call <see cref="PushGroup"/> before pushing and <see cref="PopGroup"/> when finished.
/// </summary>
public sealed class ExaminedEvent : EntityEventArgs, IInventoryRelayEvent //Harmony InventoryRelayEvent for Lanyards
public sealed class ExaminedEvent : EntityEventArgs
{
/// <summary>
/// The message that will be displayed as the examine text.
@@ -530,7 +529,6 @@ namespace Content.Shared.Examine
}
private record ExamineMessagePart(FormattedMessage Message, int Priority, bool DoNewLine, string? Group);
public SlotFlags TargetSlots { get; } = SlotFlags.NECK; // Harmony - For targeting lanyards.
}
@@ -30,11 +30,6 @@ public sealed class InteractUsingEvent : HandledEntityEventArgs
/// </summary>
public EntityCoordinates ClickLocation { get; }
/// <summary>
/// Stellar - set to false if this interaction shouldn't have an interaction particle
/// </summary>
public bool InteractionParticle = true;
public InteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation)
{
// Interact using should not have the same used and target.
@@ -31,7 +31,6 @@ using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Content.Shared.Wall;
using Content.Shared._Goobstation.DoAfter; // Goobstation
using Content.Shared.Inventory.VirtualItem; // Stellar - interaction particles
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Input;
@@ -1074,8 +1073,8 @@ namespace Content.Shared.Interaction
var userInteractUsingEvent = new UserInteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(user, userInteractUsingEvent, true);
DoContactInteraction(user, used, null, true, interactUsingEvent, interactionParticles: interactUsingEvent.InteractionParticle); // Stellar - interaction particles
DoContactInteraction(user, target, used, true, interactUsingEvent, interactionParticles: interactUsingEvent.InteractionParticle); // Stellar - interaction particles
DoContactInteraction(user, used, null, true, interactUsingEvent); // Stellar - interaction particles
DoContactInteraction(user, target, used, true, interactUsingEvent); // Stellar - interaction particles
// Contact interactions are currently only used for forensics, so we don't raise used -> target
if (interactUsingEvent.Handled || userInteractUsingEvent.Handled)
return true;
@@ -1438,8 +1437,7 @@ namespace Content.Shared.Interaction
/// <param name="predicted">Whether this interaction is predicted. <see cref="uidA"/> is assumed to be the client entity.</param>
/// <param name="args">Optional handleable entity event to check.</param>
/// <param name="interactionParticles">Whether to spawn interaction particles on this contact.</param>
/// <param name="interactionParticleType">The type of interaction particle to spawn for this event.</param>
public void DoContactInteraction(EntityUid uidA, EntityUid? uidB, EntityUid? used, bool predicted, HandledEntityEventArgs? args = null, bool interactionParticles = true, StellarInteractionParticleType interactionParticleType = StellarInteractionParticleType.Use) // Stellar/ES - interaction particles
public void DoContactInteraction(EntityUid uidA, EntityUid? uidB, EntityUid? used, bool predicted, HandledEntityEventArgs? args = null, bool interactionParticles = true) // Stellar/ES - interaction particles
{
if (uidB == null || args?.Handled == false)
return;
@@ -1461,7 +1459,7 @@ namespace Content.Shared.Interaction
RaiseLocalEvent(uidB.Value, ev);
// Begin Stellar/ES Additions - Interaction particles
if (!interactionParticles || HasComp<VirtualItemComponent>(uidB))
if (!interactionParticles)
return;
if (_net.IsServer)
@@ -1470,11 +1468,11 @@ namespace Content.Shared.Interaction
? Filter.PvsExcept(uidA, entityManager: EntityManager)
: Filter.Pvs(uidA, entityManager: EntityManager);
RaiseNetworkEvent(new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), false, interactionParticleType), filter);
RaiseNetworkEvent(new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), false), filter);
}
else if (_gameTiming.IsFirstTimePredicted)
{
var evt = new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), true, interactionParticleType);
var evt = new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), true);
RaiseLocalEvent(evt);
}
// End Stellar/ES Additions - Interaction particles
@@ -33,7 +33,6 @@ using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Wieldable;
using Content.Shared.Zombies;
using Content.Shared.Examine; //Harmony
namespace Content.Shared.Inventory;
@@ -65,7 +64,6 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, StoodEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, DownedEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ExaminedEvent>(RelayInventoryEvent); // Harmony - added for lanyards.
// by-ref events
SubscribeLocalEvent<InventoryComponent, RefreshFrictionModifiersEvent>(RefRelayInventoryEvent);
@@ -1,8 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement; //Harmony Lanyards
using Content.Shared.Inventory; //Harmony Lanyards
using Content.Shared.Labels.Components;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Paper;
@@ -10,7 +8,6 @@ using Content.Shared.Tag; // DeltaV
using Robust.Shared.Containers;
using Robust.Shared.Prototypes; // DeltaV
using Robust.Shared.Utility;
using System.Linq; //Harmony Lanyards
namespace Content.Shared.Labels.EntitySystems;
@@ -37,11 +34,6 @@ public sealed partial class LabelSystem : EntitySystem
SubscribeLocalEvent<PaperLabelComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, ExaminedEvent>(OnExamined);
// Harmony - OnExamined can now be inventory relayed from the neck slot for lanyard implementation,
// getting the entity's pronouns and changing the label inspection text to reflect it being from a lanyard.
// This would cause any other neck slot item with the label component to be described as a lanyard on inspection,
// but currently no others exist.
SubscribeLocalEvent<PaperLabelComponent, InventoryRelayedEvent<ExaminedEvent>>((e, c, ev) => OnExaminedInInventory(e, c, ev.Args));
}
private void OnLabelCompMapInit(Entity<LabelComponent> ent, ref MapInitEvent args)
@@ -131,65 +123,9 @@ public sealed partial class LabelSystem : EntitySystem
args.PushMarkup(Loc.GetString("comp-paper-label-has-label"));
var text = paper.Content;
args.PushMarkup(text.TrimEnd());
// Harmony addition begins - shows which stamps have been applied to a label when inspected. Copied from PaperSystem.
if (paper.StampedBy.Count > 0)
{
var commaSeparated =
string.Join(", ", paper.StampedBy.Select(s => Loc.GetString(s.StampedName)));
args.PushMarkup(
Loc.GetString(
"comp-label-examine-detail-stamped-by",
("stamps", commaSeparated))
);
}
// Harmony addition ends
}
}
// Harmony addition begins - version of OnExamined for when the event is inventory relayed. Used when reading from a worn lanyard.
private void OnExaminedInInventory(EntityUid uid, PaperLabelComponent comp, ExaminedEvent args)
{
if (comp.LabelSlot.Item is not { Valid: true } item)
return;
using (args.PushGroup(nameof(PaperLabelComponent)))
{
// UID's parent is saved to be used for localisation grammar when the label is from a lanyard, since the text is changed.
var user = Comp<TransformComponent>(uid).ParentUid;
if (!args.IsInDetailsRange)
{
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard-cant-read", ("user", Identity.Entity(user, EntityManager))));
return;
}
if (!TryComp(item, out PaperComponent? paper))
// Assuming yaml has the correct entity whitelist, this should not happen.
return;
if (string.IsNullOrWhiteSpace(paper.Content))
{
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard-blank", ("user", Identity.Entity(user, EntityManager))));
return;
}
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard", ("user", Identity.Entity(user, EntityManager))));
var text = paper.Content;
args.PushMarkup(text.TrimEnd());
// Harmony - shows which stamps have been applied to a lanyard's label when inspected. Copied from PaperSystem.
if (paper.StampedBy.Count > 0)
{
var commaSeparated =
string.Join(", ", paper.StampedBy.Select(s => Loc.GetString(s.StampedName)));
args.PushMarkup(
Loc.GetString(
"comp-lanyard-examine-detail-stamped-by",
("stamps", commaSeparated))
);
}
}
}
// Harmony Addition Ends
// Not ref-sub due to being used for multiple subscriptions.
private void OnContainerModified(EntityUid uid, PaperLabelComponent label, ContainerModifiedMessage args)
{
-5
View File
@@ -122,11 +122,6 @@ public sealed class MindExamineSystem : EntitySystem
else
ent.Comp.State = MindState.None;
// DeltaV - SSD Recency START
var ev = new MindStateUpdatedEvent(ent.Comp.State);
RaiseLocalEvent(ent, ref ev);
// DeltaV END
Dirty(ent);
}
}
@@ -1,4 +1,3 @@
using Content.Shared._ST.Interaction; // Stellar - interaction particles
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
@@ -546,7 +545,7 @@ public sealed class PullingSystem : EntitySystem
// Pulling confirmed
_interaction.DoContactInteraction(pullerUid, pullableUid,null, true, interactionParticleType: StellarInteractionParticleType.Pull); // Stellar - Interaction particles
_interaction.DoContactInteraction(pullableUid, pullerUid, null, true); // Stellar - Interaction particles
// Use net entity so it's consistent across client and server.
pullableComp.PullJointId = $"pull-joint-{GetNetEntity(pullableUid)}";

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