Compare 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
681 changed files with 72514 additions and 131173 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]]
@@ -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;
}
+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()
@@ -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();
}
@@ -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);
}
}
@@ -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;
@@ -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")]
+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"));
}
+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 -16
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);
@@ -588,15 +582,6 @@ public sealed class FaxSystem : EntitySystem
return;
var faxName = Loc.GetString("fax-machine-popup-source-unknown");
// BEGIN DeltaV - Refresh Fax IDs before recieving
// This will seem odd that we're doing the same if statement twice BUT if its not in the known fax machine list, the
// sending fax might not have been online when the current fax machine was refreshed/init'd. So, if its already known,
// we don't need to refresh. But if its not known, let's attempt to refresh first, then check the fromAddress in the KnownFaxes.
if (fromAddress != null && !component.KnownFaxes.TryGetValue(fromAddress, out var _))
Refresh(uid, component);
// END DeltaV
if (fromAddress != null && component.KnownFaxes.TryGetValue(fromAddress, out var fax)) // If message received from unknown fax address
faxName = fax;
@@ -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))
{
@@ -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
@@ -209,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}");
}
}
}
@@ -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)
@@ -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;
}
}
@@ -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);
}
}
}
}
-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);
}
-14
View File
@@ -53,20 +53,6 @@ namespace Content.Shared.Chat
AudioPath = audioPath;
AudioVolume = audioVolume;
}
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;
}
}
/// <summary>
@@ -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
-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)}";
@@ -6,7 +6,7 @@ using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Traits;
//using Content.Shared.Traits; // DeltaV - Traits Rework
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
+1 -3
View File
@@ -18,13 +18,11 @@ public sealed partial class RatKingComponent : Component
[DataField("actionRaiseArmyEntity")]
public EntityUid? ActionRaiseArmyEntity;
// Delta-V - switched to a base cost modified by the amount of living servants
/// <summary>
/// The amount of hunger one use of Raise Army consumes
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("hungerPerArmyUse", required: true)]
public float HungerPerArmyUse = 10f;
// end DeltaV
public float HungerPerArmyUse = 25f;
/// <summary>
/// The entity prototype of the mob that Raise Army summons
@@ -50,12 +50,11 @@ public sealed partial class SSDIndicatorComponent : Component
// DeltaV - SSD Recency Additions START
/// <summary>
/// The time at which the player became SSD.
/// This will remain unset on SSD entities that never had minds attached, such as newly spawn ghost roles.
/// The time at which the Entity became SSD.
/// </summary>
[AutoNetworkedField, AutoPausedField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? SsdSince;
public TimeSpan SsdSince = TimeSpan.Zero;
/// <summary>
/// The icon displayed next to the associated entity when it is recently SSD (stage 2).
@@ -1,9 +1,6 @@
using Content.Shared._DV.CCVars; // DeltaV - SSD Recency
using Content.Shared._DV.Mind; // DeltaV - SSD Recency
using Content.Shared.CCVar;
using Content.Shared.Examine; // DeltaV - SSD Recency
using Content.Shared.Mobs.Systems; // DeltaV - SSD Recency
using Content.Shared.Mind.Components; // DeltaV - SSD Recency
using Content.Shared.StatusEffectNew;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
@@ -22,20 +19,18 @@ public sealed class SSDIndicatorSystem : EntitySystem
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; // DeltaV - SSD Recency
private bool _icSsdSleep;
private float _icSsdSleepTime;
private TimeSpan _cryoableSsdSeconds; // DeltaV - SSD Recency
private TimeSpan _recentSsdSeconds; // DeltaV - SSD Recency
private TimeSpan _cryoableSsdSeconds; // DeltaV
private TimeSpan _recentSsdSeconds; // DeltaV
public override void Initialize()
{
SubscribeLocalEvent<SSDIndicatorComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<SSDIndicatorComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<SSDIndicatorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SSDIndicatorComponent, MindStateUpdatedEvent>(OnMindStateUpdated); // DeltaV - SSD Recency
_cfg.OnValueChanged(CCVars.ICSSDSleep, obj => _icSsdSleep = obj, true);
_cfg.OnValueChanged(CCVars.ICSSDSleepTime, obj => _icSsdSleepTime = obj, true);
@@ -48,15 +43,12 @@ public sealed class SSDIndicatorSystem : EntitySystem
// DeltaV - SSD Recency START
private void OnExamine(Entity<SSDIndicatorComponent> ent, ref ExaminedEvent args)
{
if (ent.Comp.SsdSince is not { } ssdSince)
return;
if (_mobState.IsDead(ent))
if (!ent.Comp.IsSSD)
return;
using (args.PushGroup(nameof(SSDIndicatorComponent)))
{
var timestamp = (_timing.CurTime - ssdSince).ToString("%hh':'mm':'ss");
var timestamp = (_timing.CurTime - ent.Comp.SsdSince).ToString("%hh':'mm':'ss");
args.PushMarkup(Loc.GetString("ssd-examine-duration", ("time", timestamp)));
args.PushMarkup(Loc.GetString($"ssd-examine-{GetStage(ent).ToString().ToLower()}"));
}
@@ -66,28 +58,17 @@ public sealed class SSDIndicatorSystem : EntitySystem
{
var curTime = _timing.CurTime;
if (ent.Comp.SsdSince + _recentSsdSeconds >= curTime)
if (ent.Comp.SsdSince + _cryoableSsdSeconds < curTime)
{
return SsdStage.VeryRecent;
return SsdStage.Cryoable;
}
if (ent.Comp.SsdSince + _cryoableSsdSeconds >= curTime)
if (ent.Comp.SsdSince + _recentSsdSeconds < curTime)
{
return SsdStage.Recent;
}
return SsdStage.Cryoable;
}
public void OnMindStateUpdated(Entity<SSDIndicatorComponent> ent, ref MindStateUpdatedEvent args)
{
if (args.State is MindState.SSD or MindState.DeadSSD) {
if (ent.Comp.SsdSince is null)
ent.Comp.SsdSince = _timing.CurTime;
}
else
ent.Comp.SsdSince = null;
Dirty(ent, ent.Comp);
return SsdStage.VeryRecent;
}
// DeltaV END
@@ -115,6 +96,8 @@ public sealed class SSDIndicatorSystem : EntitySystem
component.FallAsleepTime = _timing.CurTime + TimeSpan.FromSeconds(_icSsdSleepTime);
}
component.SsdSince = _timing.CurTime; // DeltaV - SSD Recency
Dirty(uid, component);
}
@@ -62,7 +62,6 @@ public abstract class SharedEmitSoundSystem : EntitySystem
if (_whitelistSystem.IsWhitelistFail(component.Blacklist, args.User))
{
TryEmitSound(uid, component, args.User);
args.InteractionParticle = true; // Stellar - interaction particles
}
}
@@ -106,7 +106,6 @@ public abstract partial class SharedStackSystem : EntitySystem
var localRotation = Transform(args.Used).LocalRotation;
_storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User);
args.InteractionParticle = false; // Stellar
}
/// <summary>
@@ -52,11 +52,6 @@ public sealed class AfterActivatableUIOpenEvent(EntityUid user) : EntityEventArg
/// The player that opened the UI.
/// </summary>
public readonly EntityUid User = user;
/// <summary>
/// Stellar - if an interaction particle should be played for this event.
/// </summary>
public bool InteractionParticle;
}
/// <summary>
@@ -21,7 +21,6 @@ public sealed partial class ActivatableUISystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
public override void Initialize()
{
@@ -73,7 +72,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
args.Verbs.Add(new ActivationVerb
{
Act = () => InteractUI(args.User, (uid, component)), // Stellar - interaction particles
Act = () => InteractUI(args.User, uid, component),
Text = Loc.GetString(component.VerbText),
// TODO VERB ICON find a better icon
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
@@ -87,7 +86,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
args.Verbs.Add(new Verb
{
Act = () => InteractUI(args.User, (uid, component)), // Stellar - interaction particles
Act = () => InteractUI(args.User, uid, component),
Text = Loc.GetString(component.VerbText),
// TODO VERB ICON find a better icon
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
@@ -134,8 +133,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (component.RequiredItems != null)
return;
var interactionParticle = false; // Stellar - interaction particles
args.Handled = InteractUI(args.User, uid, component, ref interactionParticle); // Stellar - interaction particles
args.Handled = InteractUI(args.User, uid, component);
}
private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args)
@@ -149,7 +147,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (component.RequiredItems != null)
return;
args.Handled = InteractUI(args.User, uid, component, ref args.InteractionParticle); // Stellar - interaction particles
args.Handled = InteractUI(args.User, uid, component);
}
private void OnInteractUsing(EntityUid uid, ActivatableUIComponent component, InteractUsingEvent args)
@@ -166,7 +164,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (_whitelistSystem.IsWhitelistFail(component.RequiredItems, args.Used))
return;
args.Handled = InteractUI(args.User, uid, component, ref args.InteractionParticle); // Stellar - interaction particles
args.Handled = InteractUI(args.User, uid, component);
}
private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args)
@@ -182,23 +180,13 @@ public sealed partial class ActivatableUISystem : EntitySystem
SetCurrentSingleUser(uid, null, component);
}
// Begin Stellar - interaction particles
private void InteractUI(EntityUid user, Entity<ActivatableUIComponent> ui)
{
var interactionParticle = false;
InteractUI(user, ui, ui, ref interactionParticle);
_interaction.DoContactInteraction(user, ui, null, true, interactionParticles: interactionParticle);
}
// End Stellar - interaction particles
private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui, ref bool interactionParticle) // Stellar - interaction particles
private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui)
{
if (aui.Key == null || !_uiSystem.HasUi(uiEntity, aui.Key))
return false;
if (_uiSystem.IsUiOpen(uiEntity, aui.Key, user))
{
interactionParticle = false; // Stellar - interaction particles
_uiSystem.CloseUi(uiEntity, aui.Key, user);
return true;
}
@@ -255,8 +243,6 @@ public sealed partial class ActivatableUISystem : EntitySystem
var aae = new AfterActivatableUIOpenEvent(user);
RaiseLocalEvent(uiEntity, aae);
interactionParticle = aae.InteractionParticle; // Stellar
return true;
}
@@ -31,7 +31,6 @@ using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Wieldable.Components; // Starlight | ES Screenshake
using Content.Shared.Zombies; // DeltaV - Buff Zombies
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
@@ -598,7 +597,18 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
if (damageResult.GetTotal() > FixedPoint2.Zero)
{
DoDamageEffect(targets, user, targetXform);
DoScreenshake(meleeUid, damageResult, user, targets); // Starlight | ES Screenshake
// ES START
// dog shit copy plaste but thats melee for you
var userShakeRotation = new ESScreenshakeParameters()
{ Trauma = 0.08f, DecayRate = 1.0f, Frequency = 0.009f };
var otherShakeTranslation = new ESScreenshakeParameters() { Trauma = 0.45f, DecayRate = 1.1f, Frequency = 0.04f };
_shake.Screenshake(user, null, userShakeRotation);
foreach (var shakeTarget in targets)
{
_shake.Screenshake(shakeTarget, otherShakeTranslation, null);
}
// ES END
}
}
@@ -764,10 +774,21 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
_meleeSound.PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component);
}
// ES START
// dog shit copy plaste but thats melee for you
var userShakeRotation = new ESScreenshakeParameters()
{ Trauma = 0.08f, DecayRate = 1.0f, Frequency = 0.009f };
var otherShakeTranslation = new ESScreenshakeParameters() { Trauma = 0.45f, DecayRate = 1.1f, Frequency = 0.04f };
_shake.Screenshake(user, null, userShakeRotation);
foreach (var shakeTarget in targets)
{
_shake.Screenshake(shakeTarget, otherShakeTranslation, null);
}
// ES END
if (appliedDamage.GetTotal() > FixedPoint2.Zero)
{
DoDamageEffect(targets, user, Transform(targets[0]));
DoScreenshake(meleeUid, damage, user, targets); // Starlight | ES Screenshake
}
return true;
@@ -1083,55 +1104,4 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
}
}
}
//Starlight begin | ES Screenshake
private void DoScreenshake(EntityUid weapon, DamageSpecifier damage, EntityUid attacker, List<EntityUid> targets)
{
if(damage.GetTotal()>4) // only show to others if it hurts real bad // DeltaV - reduce from 8 to 4
{
var otherTranslation = new ESScreenshakeParameters
{
Trauma = 0.45f,
DecayRate = 1.1f,
Frequency = 0.04f,
};
foreach(var target in targets)
_shake.Screenshake(target, otherTranslation, null);
}
// only show to attacker if they put real oompf into it, or the weapon is just THAT strong
// var bluntRequirement = damage.DamageDict.TryGetValue(BluntDamageName, out var blunt) && blunt >= 20; // DeltaV - unused
var isWielding = TryComp<WieldableComponent>(weapon, out var wieldable) && wieldable.Wielded;
// DeltaV - unused
// if (!bluntRequirement && !wieldRequirement)
// return;
// DeltaV - heavy/light screenshake variants START
ESScreenshakeParameters userRotation;
if (damage.GetTotal() >= 15 || isWielding)
{
// heavy damage or two-handed
userRotation = new ESScreenshakeParameters
{
Trauma = 0.08f,
DecayRate = 1,
Frequency = 0.009f,
};
}
else
{
// light damage
userRotation = new ESScreenshakeParameters
{
Trauma = 0.06f,
DecayRate = 1,
Frequency = 0.0045f,
};
}
// DeltaV END
_shake.Screenshake(attacker, null, userRotation);
}
//Starlight end
}
@@ -0,0 +1,22 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.AccountLinking;
public sealed class LinkAccountCodeMsg : NetMessage
{
public Guid Code;
public override MsgGroups MsgGroup => MsgGroups.Core;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
Code = buffer.ReadGuid();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(Code);
}
}
@@ -0,0 +1,18 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.AccountLinking;
public sealed class LinkAccountRequestMsg : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Core;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
}
}
@@ -0,0 +1,45 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.AccountLinking;
public sealed class LinkAccountStatusMsg : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Core;
public SharedPatronFull? Patron;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
var isPatron = buffer.ReadBoolean();
if (!isPatron)
return;
buffer.ReadPadBits();
var length = buffer.ReadVariableInt32();
using var stream = new MemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
Patron = serializer.Deserialize<SharedPatronFull>(stream);
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
if (Patron == null)
{
buffer.Write(false);
return;
}
buffer.Write(true);
buffer.WritePadBits();
using (var stream = new MemoryStream())
{
serializer.Serialize(stream, Patron);
buffer.WriteVariableInt32((int) stream.Length);
stream.TryGetBuffer(out var segment);
buffer.Write(segment);
}
}
}
@@ -0,0 +1,34 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.AccountLinking;
public sealed class PatronListMsg : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Core;
public List<SharedPatron> Patrons = default!;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
var count = buffer.ReadVariableInt32();
Patrons = new List<SharedPatron>(count);
for (var i = 0; i < count; i++)
{
var name = buffer.ReadString();
var tier = buffer.ReadString();
Patrons.Add(new SharedPatron(name, tier));
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.WriteVariableInt32(Patrons.Count);
foreach (var patron in Patrons)
{
buffer.Write(patron.Name);
buffer.Write(patron.Tier);
}
}
}
@@ -0,0 +1,10 @@
using Robust.Shared.Serialization;
namespace Content.Shared._DV.AccountLinking;
[Serializable, NetSerializable]
public sealed class SharedPatron(string name, string tier)
{
public readonly string Name = name;
public readonly string Tier = tier;
}
@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared._DV.AccountLinking;
[Serializable, NetSerializable]
public sealed record SharedPatronFull(
SharedPatronTier? Tier,
bool Linked
);
@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared._DV.AccountLinking;
[Serializable, NetSerializable]
public sealed record SharedPatronTier(
bool ShowOnCredits,
string Tier
);
@@ -0,0 +1,13 @@
using Robust.Shared.Configuration;
namespace Content.Shared._DV.CCVars;
public sealed partial class DCCVars
{
/// <summary>
/// Redirects you to the channel in discord to link your account.
/// </summary>
public static readonly CVarDef<string> DiscordAccountLinkingMessageLink =
CVarDef.Create("discord.discord_account_linking_message_link", "", CVar.REPLICATED | CVar.SERVER);
}
@@ -1,5 +1,3 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -38,8 +36,6 @@ public sealed partial class CosmicColossusComponent : Component
[DataField] public EntProtoId Attack1Vfx = "CosmicColossusAttack1Vfx";
[DataField] public EntProtoId BuffVfx = "ColossusBuffVfx";
[DataField] public EntProtoId TileDetonations = "MobTileDamageZone";
[DataField] public EntProtoId EffigyPrototype = "CosmicEffigy";
@@ -56,23 +52,13 @@ public sealed partial class CosmicColossusComponent : Component
[DataField] public TimeSpan HibernationWait = TimeSpan.FromSeconds(30);
[DataField] public TimeSpan DeathWaitSpawn = TimeSpan.FromMinutes(15);
[DataField] public TimeSpan DeathWaitEffigy = TimeSpan.FromMinutes(10);
[DataField] public TimeSpan DeathWait = TimeSpan.FromMinutes(15);
[DataField] public bool Attacking;
[DataField] public bool Hibernating;
[DataField] public bool Timed;
[DataField] public short CompletedEffigies;
[DataField] public short MaxEffigies = 3;
[DataField] public DamageSpecifier BonusDamage = new();
}
[Serializable, NetSerializable]
@@ -1,6 +0,0 @@
namespace Content.Shared._DV.Mind;
using Content.Shared.Mind.Components;
[ByRefEvent]
public record struct MindStateUpdatedEvent(MindState State);
@@ -1,4 +1,3 @@
using Content.Shared.ActionBlocker;
using Content.Shared._DV.Movement.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
@@ -8,19 +7,12 @@ namespace Content.Shared.Movement.Systems;
public abstract partial class SharedJetpackSystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
private void OnJetpackToggle(Entity<JetpackComponent> jetpack, ref ToggleJetpackEvent args)
{
if (args.Handled)
return;
if (!_actionBlocker.CanComplexInteract(args.Performer))
{
_popup.PopupClient(Loc.GetString("jetpack-too-complex"), jetpack, args.Performer);
return;
}
jetpack.Comp.AutomaticMode = !jetpack.Comp.AutomaticMode;
jetpack.Comp.AutomaticUser = args.Performer;
Dirty(jetpack);
-3
View File
@@ -42,9 +42,6 @@ public sealed class PagerSystem : EntitySystem
private void OnAfterInteract(Entity<PagerComponent> ent, ref AfterInteractEvent args)
{
if (!args.CanReach)
return;
if (args.Handled || !TryComp<DeviceNetworkComponent>(args.Target, out var targetNetwork) || !HasComp<PageSenderComponent>(args.Target))
return;
@@ -0,0 +1,13 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Psionics.Components.PsionicPowers;
[RegisterComponent, NetworkedComponent]
public sealed partial class FracturedFormBodyComponent : Component
{
/// <summary>
/// The entity uid of the form that is currently being controlled.
/// </summary>
[DataField]
public EntityUid ControllingForm { get; set; }
}
@@ -0,0 +1,124 @@
using Content.Shared.Cloning;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Psionics.Components.PsionicPowers;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class FracturedFormPowerComponent : BasePsionicPowerComponent
{
public override EntProtoId ActionProtoId { get; set; } = "ActionFracturedForm";
public override string PowerName { get; set; } = "psionic-power-name-fractured-form";
public override int MinGlimmerChanged { get; set; } = 5;
public override int MaxGlimmerChanged { get; set; } = 15;
/// <summary>
/// The chance for the clone to have the starting gear of the psionic.
/// </summary>
[DataField]
public float HasGearChance = 0.4f;
/// <summary>
/// The chance for the clone to have a different species than the psionic.
/// </summary>
[DataField]
public float DifferentSpeciesChance = 0.6f;
/// <summary>
/// These are settings for creating the second body.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> CopyNaked = "CloningPod";
/// <summary>
/// These are settings for creating the second body.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> CopyClothed = "Antag";
/// <summary>
/// These are settings for creating the second body.
/// </summary>
[DataField]
public ProtoId<JobPrototype> VisitorJob = "Passenger";
/// <summary>
/// These are settings for creating the second body.
/// </summary>
[DataField]
public ProtoId<JobPrototype>? NakedJob; // Scary null, but we explicitly want no job for naked spawns.
/// <summary>
/// The minimum time between forced swaps.
/// </summary>
[DataField]
public TimeSpan NextSwapMinTime = TimeSpan.FromMinutes(5);
/// <summary>
/// The maximum time between swaps.
/// </summary>
[DataField]
public TimeSpan NextSwapMaxTime = TimeSpan.FromMinutes(20);
/// <summary>
/// The current timer for when the next swap is forced.
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextSwap = TimeSpan.FromMinutes(5);
/// <summary>
/// The minimum time between voluntary swaps by sleeping.
/// </summary>
[DataField]
public TimeSpan VoluntarySwapCooldown = TimeSpan.FromSeconds(10);
/// <summary>
/// The minimum time between voluntary swaps by sleeping.
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextVoluntarySwap = TimeSpan.FromSeconds(10);
/// <summary>
/// The length of the DoAfter for swapping bodies.
/// </summary>
[DataField]
public float ManualSwapTime = 5f;
/// <summary>
/// The time the psionic needs to be asleep to swap bodies.
/// </summary>
[DataField]
public TimeSpan SwapTimeAsleep = TimeSpan.FromSeconds(3);
/// <summary>
/// The time length where the warning is sent before the forced swap.
/// </summary>
[DataField]
public TimeSpan WarningTimeBeforeSleep = TimeSpan.FromSeconds(5);
/// <summary>
/// A boolean check for when the person has been warned.
/// </summary>
[DataField]
public bool SleepWarned;
/// <summary>
/// The bodies that the psionic user can swap into.
/// </summary>
[DataField, AutoNetworkedField]
public List<EntityUid> Bodies { get; set; } = [];
/// <summary>
/// The sound that plays when the swap happens.
/// </summary>
[DataField]
public SoundSpecifier SwapSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg")
{
Params = AudioParams.Default.WithVariation(0.05f),
};
}
@@ -0,0 +1,5 @@
using Content.Shared.Actions;
namespace Content.Shared._DV.Psionics.Events.PowerActionEvents;
public sealed partial class FracturedFormPowerActionEvent : InstantActionEvent;
@@ -0,0 +1,7 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Psionics.Events.PowerDoAfterEvents;
[Serializable, NetSerializable]
public sealed partial class FracturedFormDoAfterEvent : SimpleDoAfterEvent;
@@ -0,0 +1,150 @@
using Content.Shared._DV.Psionics.Components.PsionicPowers;
using Content.Shared._DV.Psionics.Events;
using Content.Shared._DV.Psionics.Events.PowerActionEvents;
using Content.Shared._DV.Psionics.Events.PowerDoAfterEvents;
using Content.Shared.Bed.Sleep;
using Content.Shared.Chat;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.SSDIndicator;
using Robust.Shared.Player;
namespace Content.Shared._DV.Psionics.Systems.PsionicPowers;
public abstract class SharedFracturedFormPowerSystem : BasePsionicPowerSystem<FracturedFormPowerComponent, FracturedFormPowerActionEvent>
{
[Dependency] protected readonly SharedChatSystem Chat = default!;
[Dependency] protected readonly MobStateSystem MobState = default!;
[Dependency] protected readonly SleepingSystem Sleeping = default!;
protected EntityQuery<FracturedFormPowerComponent> FracturedQuery;
private EntityQuery<ForcedSleepingStatusEffectComponent> _forcedSleepQuery;
protected EntityQuery<MindContainerComponent> MindContainerQuery;
private EntityQuery<MobStateComponent> _mobStateQuery;
protected EntityQuery<SSDIndicatorComponent> SsdQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FracturedFormPowerComponent, FracturedFormDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<FracturedFormBodyComponent, PlayerDetachedEvent>(OnPlayerDetached, after: [typeof(SSDIndicatorSystem)]);
SubscribeLocalEvent<FracturedFormBodyComponent, ExaminedEvent>(OnExamine);
FracturedQuery = GetEntityQuery<FracturedFormPowerComponent>();
_forcedSleepQuery = GetEntityQuery<ForcedSleepingStatusEffectComponent>();
MindContainerQuery = GetEntityQuery<MindContainerComponent>();
_mobStateQuery = GetEntityQuery<MobStateComponent>();
SsdQuery = GetEntityQuery<SSDIndicatorComponent>();
}
protected override void OnPowerUsed(Entity<FracturedFormPowerComponent> psionic, ref FracturedFormPowerActionEvent args)
{
if (!CanSwap(psionic))
{
Popup.PopupClient(Loc.GetString("psionic-power-fractured-form-nobodies"), psionic, args.Performer, PopupType.Large);
return;
}
Chat.TryEmoteWithChat(psionic.Owner, "Yawn");
Popup.PopupClient(Loc.GetString("psionic-power-fractured-form-sleepy"), psionic, args.Performer, PopupType.LargeCaution);
var doAfterArgs = new DoAfterArgs(EntityManager, args.Performer, psionic.Comp.ManualSwapTime, new FracturedFormDoAfterEvent(), psionic);
if (!DoAfter.TryStartDoAfter(doAfterArgs, out var doAfterId))
return;
psionic.Comp.SaveDoAfterId(doAfterId.Value);
Dirty(psionic);
AfterPowerUsed(psionic, args.Performer);
}
private void OnDoAfter(Entity<FracturedFormPowerComponent> psionic, ref FracturedFormDoAfterEvent args)
{
if (args.Cancelled || args.Handled)
return;
args.Handled = true;
psionic.Comp.RemoveSavedDoAfterId();
Dirty(psionic);
Sleeping.TrySleeping(psionic.Owner);
}
private void OnPlayerDetached(Entity<FracturedFormBodyComponent> body, ref PlayerDetachedEvent args)
{
if (!SsdQuery.TryComp(body, out var comp)
|| FracturedQuery.HasComp(body))
return;
comp.IsSSD = false;
}
private void OnExamine(Entity<FracturedFormBodyComponent> psionic, ref ExaminedEvent args)
{
if (HasComp<FracturedFormPowerComponent>(psionic))
return;
if (TryComp<FracturedFormPowerComponent>(args.Examiner, out var fracturedHost) && fracturedHost.Bodies.Contains(psionic.Owner))
args.PushMarkup($"[color=yellow]{Loc.GetString("psionic-power-fractured-form-examine-self", ("ent", psionic))}[/color]");
else
args.PushMarkup($"[color=yellow]{Loc.GetString("psionic-power-fractured-form-ssd", ("ent", psionic))}[/color]");
}
/// <summary>
/// We're overriding the dispelled method to introduce some special behavior.
/// </summary>
protected override void OnDispelled(Entity<FracturedFormPowerComponent> psionic, ref DispelledEvent args)
{
if (args.Handled)
return;
args.Handled = true;
// This resets the swap timer.
var randomTime = Random.Next(psionic.Comp.NextSwapMinTime, psionic.Comp.NextSwapMaxTime);
psionic.Comp.NextSwap = Timing.CurTime + randomTime;
if (psionic.Comp.GetDoAfterId() is not { } doAfterId)
{
Popup.PopupClient(Loc.GetString("psionic-power-fractured-form-dispelled"), psionic, PopupType.MediumCaution);
return;
}
DoAfter.Cancel(doAfterId);
Popup.PopupClient(Loc.GetString("psionic-dispelled"), psionic, PopupType.MediumCaution);
psionic.Comp.RemoveSavedDoAfterId();
}
protected bool IsValidBody(Entity<FracturedFormPowerComponent> psionic, EntityUid otherBody)
{
if (otherBody == psionic.Owner)
return false;
if (!psionic.Comp.Bodies.Contains(otherBody))
return false;
if (!MindContainerQuery.TryComp(otherBody, out var cmind))
return false;
if (cmind.HasMind)
return false;
if (_forcedSleepQuery.HasComp(otherBody))
return false;
if (!_mobStateQuery.TryComp(otherBody, out var mobState))
return false;
if (MobState.IsIncapacitated(otherBody, mobState))
return false;
return true;
}
public bool CanSwap(Entity<FracturedFormPowerComponent> psionic)
{
foreach (var body in psionic.Comp.Bodies)
{
if (IsValidBody(psionic, body))
return true;
}
return false;
}
}
@@ -1,10 +0,0 @@
using Content.Shared.Trigger.Components.Effects;
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Trigger.Components.Effects;
/// <summary>
/// is used for implants and such so the right paramters can be passed
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class CreateHitmanCardOnTriggerComponent : BaseXOnTriggerComponent;
@@ -1,30 +0,0 @@
using Content.Shared._DV.Trigger.Components.Effects;
using Content.Shared.Trigger;
using Content.Shared.Hands.EntitySystems;
namespace Content.Shared._DV.Trigger.Systems;
public sealed class CreateHitmanCardOnTriggerSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CreateHitmanCardOnTriggerComponent, TriggerEvent>(OnTrigger);
}
//just spawns a card on the given target. uses predicated or spawn next to based on if. as trigger effect doesnt seem to have predicted on it ill just check if this is sever first and if not assume its predicted.
private void OnTrigger(Entity<CreateHitmanCardOnTriggerComponent> ent, ref TriggerEvent args)
{
if (args.Handled || args.User is not { } user)
return;
var mapCoords = _transform.GetMapCoordinates(ent);
var card = EntityManager.PredictedSpawn("HitmanBusinessCard", mapCoords);
_hands.TryPickupAnyHand(user, card);
args.Handled = true;
}
}
@@ -1,23 +0,0 @@
using Content.Shared.NPC.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Zombies;
public sealed partial class ZombieComponent
{
/// <summary>
/// DeltaV - The list of factions that an entity is part of from before they were zombified.
/// The zombification process removes them, but cloning a zombie copies the NPCFactionMemberComponent
/// so they are still considered a zombie faction-wise, so we need to keep track of what they had right
/// before the factions are removed.
/// </summary>
[DataField]
public List<ProtoId<NpcFactionPrototype>> FactionsBeforeZombification = new();
/// <summary>
/// DeltaV - Tracks if the entity was a pacifist before turning into a zombie. Although, there are other
/// scenarios where the person might be pacified but those are usually temporary.
/// </summary>
public bool WasPacisfistBeforeZombification = false;
}

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