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
github-actions[bot]andDeltaV-Bot e2e5c40f83 Update Credits (#5974)
Co-authored-by: DeltaV-Bot <github@deltav.gay>
2026-06-07 04:55:51 +02:00
Delta-V bot d0c3cff669 Automatic changelog update 2026-06-06 21:14:23 +02:00
ed1fa64a6e feat: Recent SSD indicator, SSD time in admin overlay (#5699)
* feat: Recent SSD indicator, SSD time in admin overlay

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* tweak: remove colon from ssd time loc string

* feat: add another stage

* feat: ssd examine text

* apply suggestion: make ssdstage serializable, use proper doc comments

Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Signed-off-by: DisposableCrewmember42 <disposablecrewmember42@proton.me>

* apply suggestion: more compact switch, throw instead of logging if invalid stage

Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Signed-off-by: DisposableCrewmember42 <disposablecrewmember42@proton.me>

* apply suggestion: missing DeltaV comment

Co-authored-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>
Signed-off-by: DisposableCrewmember42 <disposablecrewmember42@proton.me>

* fix: add missing import

* refactor: remove unnecessary state-keeping

i don't remember why i did it like that

* chore: remove unused import

* refactor: don't separate comp from ent

---------

Signed-off-by: DisposableCrewmember42 <disposablecrewmember42@proton.me>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>
2026-06-06 19:14:03 +00:00
Delta-V bot cb245477b8 Automatic changelog update 2026-06-06 14:08:17 +02:00
0fdfef7175 New Lobby/Jukebox Track Made By Me (#5620)
* woah

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* attributions updates

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update Resources/Audio/_DV/Lobby/attributions.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: This_Is_Gray <167829864+IAmNotGray@users.noreply.github.com>

* Update Resources/Audio/_DV/Jukebox/attributions.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: This_Is_Gray <167829864+IAmNotGray@users.noreply.github.com>

* Update RobustToolbox

---------

Signed-off-by: This_Is_Gray <167829864+IAmNotGray@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-06-06 12:07:55 +00:00
Delta-V bot 646d538c4a Automatic changelog update 2026-06-06 11:53:38 +02:00
Stxcking 6ebcf78d08 Fix: Radiation Barrier Projector Works as Intended (#5787)
fix
2026-06-06 11:53:19 +02:00
Delta-V bot 2b7e3c8058 Automatic changelog update 2026-06-05 23:14:09 +02:00
pathetic meowmeow 4736a8db6f Add cyborg wire panel (#5948)
* Add cyborg wire panel

* MORE WIRES RAAAGH

* file capitalization

* renamed

* more comments
2026-06-05 23:13:49 +02:00
Delta-V bot 6057388bae Automatic changelog update 2026-06-05 19:34:29 +02:00
50a651beea Added Chameleon Armor (#5921)
* Added Chameleon Armor

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* nerfvest

* Lowered Price

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Changed Carapace Parent

* Remove irrelevant Braces

Signed-off-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>

---------

Signed-off-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>
2026-06-05 17:34:09 +00:00
VelcroboyandVelcroboy 7e1308dd12 Map Hotfix: Elegance (#5966)
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-06-05 18:06:03 +02:00
Delta-V bot 338ff271c7 Automatic changelog update 2026-06-05 17:33:16 +02:00
Stxcking fddd9079d3 Feat: Small Nuclear Reactor and Small Gas Turbine for Purchase (#5945)
mhmm
2026-06-05 11:32:56 -04:00
Delta-V bot bedc17ae73 Automatic changelog update 2026-06-05 17:29:04 +02:00
Stxcking ec80e99e26 Tweak: Added More Pride Theme Items in the Pride-o-Mat (#5946)
* gay

* more gay
2026-06-05 11:28:44 -04:00
Delta-V bot 4ceb567439 Automatic changelog update 2026-06-05 17:26:45 +02:00
Theo 6a9984475c Random nukie and ninja appearance (#5922)
* ninja and nukie random appearance component added to gamerules

* ninja and nukie random appearance component added to gamerules

* add keepgender
2026-06-05 15:26:24 +00:00
Delta-V bot 030eb95c63 Automatic changelog update 2026-06-05 17:18:10 +02:00
Stxcking 00abed028d Feat: No Paradox Clone Trait (#5937)
* yip

* whoops
2026-06-05 11:17:50 -04:00
Delta-V bot 0f3d319a6d Automatic changelog update 2026-06-05 17:08:02 +02:00
biddygelson a9ddc102e7 Academy 1.8.1 (#5880)
* added secure windoors to sec disposal chutes
* added secure windoors to epi-brig disposal chute, sec and epi locked.
* tweak medical escape pods for DV 5x7 variant
* tweak justice escape pods for DV 5x7 variant
* replaced all solar grill with random grill spawner
* tweak moved disposal light so it doesn't break from trash
2026-06-05 17:07:41 +02:00
Delta-V bot a51896b08d Automatic changelog update 2026-06-05 17:06:12 +02:00
Stxcking 356106342f Tweak: Revert Upstream Medical Borg Rework + More (#5656)
* Medical Borg Good Again :D

* Indents buh

* im blind

or dumb, theres errors that seem unrelated to the bodybags after this

* IM SORRY I FORGOT ABOUT THIS

* Anchoring it
2026-06-05 11:05:52 -04:00
Delta-V bot a3b450ac07 Automatic changelog update 2026-06-05 15:34:19 +02:00
pathetic meowmeow 4ddb5901b0 Better douner name generation (#5836)
* better douner name generation

* more names

* yet more names

* ugly sins that i hate

* screaming

* e
2026-06-05 15:33:56 +02:00
Delta-V bot 972d852bf4 Automatic changelog update 2026-06-05 14:46:20 +02:00
pathetic meowmeow ef0d317d12 Interaction particle finetuning (#5933) 2026-06-05 08:46:01 -04:00
003e08b583 Flavourful emotes (#5595)
* Changed up how emoting works

This was a long 10 hours just to get the chat to behave slightly different.

At least we can now scream over radio!

Will most definitely need some more work to get bugs out since this is the first implementation.

* Not ok

This was unmarked for some reason... I don't remember unmarking this.

* DeltaV comments

Added comments to explain changes.

Moved localization files to the _DV folder.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Moved code

Moved code to more suitable files and added some comments.

* Rights for robots

Now those with intrinsic radios can also emote!

* Fixes up some small stuff

Changes to fix some small issues with the code to align it more with modern standards.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix by-ref event

Never worked with this event type before so gonna hope this works.

* Update Content.Shared/Chat/SharedChatSystem.Emote.cs

Signed-off-by: pathetic meowmeow <uhhadd@gmail.com>

* Update Content.Shared/Chat/SharedChatSystem.cs

Signed-off-by: pathetic meowmeow <uhhadd@gmail.com>

* Update Content.Server/Chat/Systems/ChatSystem.cs

Signed-off-by: pathetic meowmeow <uhhadd@gmail.com>

* fix style nits

* remove conflicting chatsan

---------

Signed-off-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
2026-06-04 17:25:58 +00:00
Delta-V bot 74739709c4 Automatic changelog update 2026-06-04 12:59:17 +02:00
AeraAulinandSir Warock 61840fd5ab Rename Holy to Radiant and Shadow to Soul (#5952)
* rename shadow to soul and change it all since it's ours, hopefully change holy to radiant while touching as little as possible

* obliterate a mention of shadow

* Update Resources/Prototypes/_Impstation/Entities/Mobs/Anomalites/anomalite_damage.yml

Co-authored-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>
Signed-off-by: AeraAulin <133451603+AeraAuling@users.noreply.github.com>

---------

Signed-off-by: AeraAulin <133451603+AeraAuling@users.noreply.github.com>
Co-authored-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>
2026-06-04 07:58:56 -03:00
Delta-V bot 90f7aa7999 Automatic changelog update 2026-06-03 20:44:15 +02:00
turtlemutt 2260af7ce1 Bluehouse (#5898)
Update lighthouse.yml
2026-06-03 20:43:55 +02:00
205 changed files with 11098 additions and 2615 deletions
+3
View File
@@ -312,3 +312,6 @@ Resources/MapImages
# Changelog package lock
package-lock.json
# Discord Bot Config
link-config.json
@@ -8,12 +8,14 @@ using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Content.Shared.SSDIndicator; // DeltaV - SSD time indicator
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; // DeltaV - SSD time indicator
namespace Content.Client.Administration;
@@ -26,6 +28,7 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly SharedRoleSystem _roles;
private readonly IPrototypeManager _prototypeManager;
private readonly IGameTiming _timing; // DeltaV - Add timing
private readonly Font _font;
private readonly Font _fontBold;
private AdminOverlayAntagFormat _overlayFormat;
@@ -53,7 +56,8 @@ internal sealed class AdminNameOverlay : Overlay
IUserInterfaceManager userInterfaceManager,
IConfigurationManager config,
SharedRoleSystem roles,
IPrototypeManager prototypeManager)
IPrototypeManager prototypeManager,
IGameTiming timing) // DeltaV - Add timing
{
_system = system;
_entityManager = entityManager;
@@ -62,6 +66,7 @@ internal sealed class AdminNameOverlay : Overlay
_userInterfaceManager = userInterfaceManager;
_roles = roles;
_prototypeManager = prototypeManager;
_timing = timing; // DeltaV - Add timing
ZIndex = 200;
// Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack();
@@ -231,6 +236,17 @@ internal sealed class AdminNameOverlay : Overlay
currentOffset += lineoffset;
}
// DeltaV - SSD Time START
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 - ssdIndicator.SsdSince).ToString("%hh':'mm':'ss")));
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, ssdText, uiScale, color);
currentOffset += lineoffset;
}
// DeltaV END
// Determine antag symbol
string? symbol;
switch (_overlaySymbolStyle)
@@ -5,6 +5,7 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; // DeltaV - SSD time indicator
namespace Content.Client.Administration.Systems
{
@@ -19,6 +20,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IGameTiming _timing = default!; // DeltaV - added for SSD time indicator
private AdminNameOverlay _adminNameOverlay = default!;
@@ -36,7 +38,8 @@ namespace Content.Client.Administration.Systems
_userInterfaceManager,
_configurationManager,
_roles,
_proto);
_proto,
_timing); // DeltaV - Add timing
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}
+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()
@@ -21,11 +21,8 @@ public sealed class SignalTimerBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<SignalTimerWindow>();
_window.OnStartTimer += StartTimer;
_window.OnCurrentTextChanged += OnTextChanged;
// Begin Monolith
// _window.OnCurrentDelayMinutesChanged += OnDelayChanged;
// _window.OnCurrentDelaySecondsChanged += OnDelayChanged;
_window.OnCurrentDelayChanged += OnDelayChanged;
// End Monolith
_window.OnCurrentDelayMinutesChanged += OnDelayChanged;
_window.OnCurrentDelaySecondsChanged += OnDelayChanged;
}
public void StartTimer()
@@ -38,11 +35,11 @@ public sealed class SignalTimerBoundUserInterface : BoundUserInterface
SendMessage(new SignalTimerTextChangedMessage(newText));
}
private void OnDelayChanged(TimeSpan newDelay) // Monolith - Change from string to TimeSpan
private void OnDelayChanged(string newDelay)
{
if (_window == null)
return;
SendMessage(new SignalTimerDelayChangedMessage(newDelay)); // Mono
SendMessage(new SignalTimerDelayChangedMessage(_window.GetDelay()));
}
/// <summary>
@@ -57,11 +54,8 @@ public sealed class SignalTimerBoundUserInterface : BoundUserInterface
return;
_window.SetCurrentText(cast.CurrentText);
// Begin Monolith
// _window.SetCurrentDelayMinutes(cast.CurrentDelayMinutes);
// _window.SetCurrentDelaySeconds(cast.CurrentDelaySeconds);
_window.SetCurrentDelay(cast.CurrentDelay);
// End Monolith
_window.SetCurrentDelayMinutes(cast.CurrentDelayMinutes);
_window.SetCurrentDelaySeconds(cast.CurrentDelaySeconds);
_window.SetShowText(cast.ShowText);
_window.SetTriggerTime(cast.TriggerTime);
_window.SetTimerStarted(cast.TimerStarted);
@@ -7,14 +7,10 @@
</BoxContainer>
<BoxContainer Name="DelayEdit" Orientation="Horizontal">
<Label Name="CurrentDelay" Text="{Loc signal-timer-menu-delay}" />
<!-- Mono: Changed from integer MM:SS to float seconds
<LineEdit Name="CurrentDelayEditMinutes" MinWidth="32" />
<Label Name="Colon" Text=":" />
<LineEdit Name="CurrentDelayEditSeconds" MinWidth="32" />
<Label Name="DelayInfo" Text=" (mm:ss)" />
-->
<FloatSpinBox Name="CurrentDelayEdit" MinWidth="32" />
<Label Text="s" />
</BoxContainer>
<Button Name="StartTimer" Text="{Loc signal-timer-menu-start}" />
</BoxContainer>
@@ -15,11 +15,8 @@ public sealed partial class SignalTimerWindow : DefaultWindow
private const int MaxTextLength = 5;
public event Action<string>? OnCurrentTextChanged;
// Begin Monolith
// public event Action<string>? OnCurrentDelayMinutesChanged;
// public event Action<string>? OnCurrentDelaySecondsChanged;
public event Action<TimeSpan>? OnCurrentDelayChanged;
// End Monolith
public event Action<string>? OnCurrentDelayMinutesChanged;
public event Action<string>? OnCurrentDelaySecondsChanged;
private TimeSpan? _triggerTime;
@@ -33,11 +30,8 @@ public sealed partial class SignalTimerWindow : DefaultWindow
IoCManager.InjectDependencies(this);
CurrentTextEdit.OnTextChanged += e => OnCurrentTextChange(e.Text);
// Begin Monolith
// CurrentDelayEditMinutes.OnTextChanged += e => OnCurrentDelayMinutesChange(e.Text);
// CurrentDelayEditSeconds.OnTextChanged += e => OnCurrentDelaySecondsChange(e.Text);
CurrentDelayEdit.OnValueChanged += e => CurrentDelayChange(TimeSpan.FromSeconds(e.Value));
// End Monolith
CurrentDelayEditMinutes.OnTextChanged += e => OnCurrentDelayMinutesChange(e.Text);
CurrentDelayEditSeconds.OnTextChanged += e => OnCurrentDelaySecondsChange(e.Text);
StartTimer.OnPressed += _ => StartTimerWeh();
}
@@ -83,93 +77,81 @@ public sealed partial class SignalTimerWindow : DefaultWindow
OnCurrentTextChanged?.Invoke(text);
}
// Begin Monolith
// public void OnCurrentDelayMinutesChange(string text)
// {
// List<char> toRemove = new();
// foreach (var a in text)
// {
// if (!char.IsDigit(a))
// toRemove.Add(a);
// }
// foreach (var a in toRemove)
// {
// CurrentDelayEditMinutes.Text = text.Replace(a.ToString(),"");
// }
// if (CurrentDelayEditMinutes.Text == "")
// return;
// while (CurrentDelayEditMinutes.Text[0] == '0' && CurrentDelayEditMinutes.Text.Length > 2)
// {
// CurrentDelayEditMinutes.Text = CurrentDelayEditMinutes.Text.Remove(0, 1);
// }
// if (CurrentDelayEditMinutes.Text.Length > 2)
// {
// CurrentDelayEditMinutes.Text = CurrentDelayEditMinutes.Text.Remove(2);
// }
// OnCurrentDelayMinutesChanged?.Invoke(CurrentDelayEditMinutes.Text);
// }
// public void OnCurrentDelaySecondsChange(string text)
// {
// List<char> toRemove = new();
// foreach (var a in text)
// {
// if (!char.IsDigit(a))
// toRemove.Add(a);
// }
// foreach (var a in toRemove)
// {
// CurrentDelayEditSeconds.Text = text.Replace(a.ToString(), "");
// }
// if (CurrentDelayEditSeconds.Text == "")
// return;
// while (CurrentDelayEditSeconds.Text[0] == '0' && CurrentDelayEditSeconds.Text.Length > 2)
// {
// CurrentDelayEditSeconds.Text = CurrentDelayEditSeconds.Text.Remove(0, 1);
// }
// if (CurrentDelayEditSeconds.Text.Length > 2)
// {
// CurrentDelayEditSeconds.Text = CurrentDelayEditSeconds.Text.Remove(2);
// }
// OnCurrentDelaySecondsChanged?.Invoke(CurrentDelayEditSeconds.Text);
// }
public void CurrentDelayChange(TimeSpan newValue)
public void OnCurrentDelayMinutesChange(string text)
{
OnCurrentDelayChanged?.Invoke(newValue);
List<char> toRemove = new();
foreach (var a in text)
{
if (!char.IsDigit(a))
toRemove.Add(a);
}
foreach (var a in toRemove)
{
CurrentDelayEditMinutes.Text = text.Replace(a.ToString(),"");
}
if (CurrentDelayEditMinutes.Text == "")
return;
while (CurrentDelayEditMinutes.Text[0] == '0' && CurrentDelayEditMinutes.Text.Length > 2)
{
CurrentDelayEditMinutes.Text = CurrentDelayEditMinutes.Text.Remove(0, 1);
}
if (CurrentDelayEditMinutes.Text.Length > 2)
{
CurrentDelayEditMinutes.Text = CurrentDelayEditMinutes.Text.Remove(2);
}
OnCurrentDelayMinutesChanged?.Invoke(CurrentDelayEditMinutes.Text);
}
public void OnCurrentDelaySecondsChange(string text)
{
List<char> toRemove = new();
foreach (var a in text)
{
if (!char.IsDigit(a))
toRemove.Add(a);
}
foreach (var a in toRemove)
{
CurrentDelayEditSeconds.Text = text.Replace(a.ToString(), "");
}
if (CurrentDelayEditSeconds.Text == "")
return;
while (CurrentDelayEditSeconds.Text[0] == '0' && CurrentDelayEditSeconds.Text.Length > 2)
{
CurrentDelayEditSeconds.Text = CurrentDelayEditSeconds.Text.Remove(0, 1);
}
if (CurrentDelayEditSeconds.Text.Length > 2)
{
CurrentDelayEditSeconds.Text = CurrentDelayEditSeconds.Text.Remove(2);
}
OnCurrentDelaySecondsChanged?.Invoke(CurrentDelayEditSeconds.Text);
}
// End Monolith
public void SetCurrentText(string text)
{
CurrentTextEdit.Text = text;
}
// Begin Monolith
// public void SetCurrentDelayMinutes(string delay)
// {
// CurrentDelayEditMinutes.Text = delay;
// }
// public void SetCurrentDelaySeconds(string delay)
// {
// CurrentDelayEditSeconds.Text = delay;
// }
public void SetCurrentDelay(TimeSpan delay)
public void SetCurrentDelayMinutes(string delay)
{
CurrentDelayEdit.Value = (float)delay.TotalSeconds;
CurrentDelayEditMinutes.Text = delay;
}
// End Monolith
public void SetCurrentDelaySeconds(string delay)
{
CurrentDelayEditSeconds.Text = delay;
}
public void SetShowText(bool showTime)
{
TextEdit.Visible = showTime;
@@ -194,10 +176,8 @@ public sealed partial class SignalTimerWindow : DefaultWindow
public void SetHasAccess(bool hasAccess)
{
CurrentTextEdit.Editable = hasAccess;
// Begin Monolith
// CurrentDelayEditMinutes.Editable = hasAccess;
// CurrentDelayEditSeconds.Editable = hasAccess;
// End Monolith
CurrentDelayEditMinutes.Editable = hasAccess;
CurrentDelayEditSeconds.Editable = hasAccess;
StartTimer.Disabled = !hasAccess;
}
@@ -206,13 +186,10 @@ public sealed partial class SignalTimerWindow : DefaultWindow
/// </summary>
public TimeSpan GetDelay()
{
// Begin Monolith
// if (!double.TryParse(CurrentDelayEditMinutes.Text, out var minutes))
// minutes = 0;
// if (!double.TryParse(CurrentDelayEditSeconds.Text, out var seconds))
// seconds = 0;
// return TimeSpan.FromMinutes(minutes) + TimeSpan.FromSeconds(seconds);
return TimeSpan.FromSeconds(CurrentDelayEdit.Value);
// End Monolith
if (!double.TryParse(CurrentDelayEditMinutes.Text, out var minutes))
minutes = 0;
if (!double.TryParse(CurrentDelayEditSeconds.Text, out var seconds))
seconds = 0;
return TimeSpan.FromMinutes(minutes) + TimeSpan.FromSeconds(seconds);
}
}
@@ -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);
@@ -19,6 +19,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly Shared.SSDIndicator.SSDIndicatorSystem _shared = default!; // DeltaV - SSD Recency, don't want to rename the upstream class
public override void Initialize()
{
@@ -35,13 +36,26 @@ public sealed class SSDIndicatorSystem : EntitySystem
!HasComp<ActiveNPCComponent>(uid) &&
HasComp<MindExaminableComponent>(uid))
{
// Begin DeltaV Addition
// Begin DeltaV Additions
var ev = new ShowSSDIndicatorEvent();
RaiseLocalEvent(uid, ref ev);
if (ev.Hidden)
return;
// End DeltaV Addition
args.StatusIcons.Add(_prototype.Index(component.Icon));
// SSD Recency Indicator
var stage = _shared.GetStage(new Entity<SSDIndicatorComponent>(uid, component));
var icon = stage switch
{
SsdStage.VeryRecent => component.VeryRecentIcon,
SsdStage.Recent => component.RecentIcon,
SsdStage.Cryoable => component.Icon,
_ => throw new InvalidOperationException($"{ToPrettyString(uid)} has an invalid SSD stage {stage}."),
};
args.StatusIcons.Add(_prototype.Index(icon));
// End DeltaV Additions
// args.StatusIcons.Add(_prototype.Index(component.Icon)); // DeltaV - commented out. status icon now added above
}
}
}
@@ -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);
}
}
@@ -63,16 +63,16 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
private Animation GetAnimation(Vector2 endOffset, Color color)
{
var startRotation = _random.NextAngle(Angle.FromDegrees(-80), Angle.FromDegrees(80));
var startRotation = _random.NextAngle(Angle.FromDegrees(-40), Angle.FromDegrees(40));
var endRotation = Angle.Zero;
var startScale = new Vector2(0.3f, 0.3f);
var endScale = new Vector2(1f, 1f);
var rotationLength = TimeSpan.FromMilliseconds(600);
var startOffset = new Vector2();
var offsetLength = TimeSpan.FromMilliseconds(200);
var offsetLength = TimeSpan.FromMilliseconds(250);
var startColor = color.WithAlpha(color.A * 0.9f);
var startColor = color.WithAlpha(color.A * 0.7f);
var endColor = color.WithAlpha(0f);
var colorLength = rotationLength + offsetLength;
@@ -89,7 +89,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startRotation, 0f),
new AnimationTrackProperty.KeyFrame(endRotation, (float)rotationLength.TotalSeconds, Easings.OutBounce),
new AnimationTrackProperty.KeyFrame(endRotation, (float)rotationLength.TotalSeconds, Easings.OutBack),
},
},
new AnimationTrackComponentProperty()
@@ -99,7 +99,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startScale, 0f),
new AnimationTrackProperty.KeyFrame(endScale, (float)rotationLength.TotalSeconds, Easings.OutBounce),
new AnimationTrackProperty.KeyFrame(endScale, (float)rotationLength.TotalSeconds, Easings.OutBack),
},
},
new AnimationTrackComponentProperty()
@@ -109,7 +109,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startOffset, 0f),
new AnimationTrackProperty.KeyFrame(endOffset, (float)offsetLength.TotalSeconds, Easings.OutBounce),
new AnimationTrackProperty.KeyFrame(endOffset, (float)offsetLength.TotalSeconds, Easings.OutBack),
},
},
new AnimationTrackComponentProperty()
+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.
@@ -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 -4
View File
@@ -89,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);
@@ -93,7 +93,6 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
Entry("('=", "chatsan-tearfully-smiles"),
Entry("['=", "chatsan-tearfully-smiles"),
Entry("?", "chatsan-confused"), //DeltaV
Entry("!", "chatsan-surprised"), //DeltaV
Entry("…", "chatsan-sighs"), //DeltaV (note that ... doesn't work as that turns into a radio message)
];
+90 -9
View File
@@ -8,6 +8,7 @@ using Content.Server.GameTicking;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Speech.Hushing; // DeltaV
using Content.Server.Nyanotrasen.Chat;
using Content.Server.Speech.Components; // DeltaV
using Content.Server.Speech.Prototypes;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
@@ -35,6 +36,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
using Content.Shared._DV.Chat; // DeltaV - chat enhancements
namespace Content.Server.Chat.Systems;
@@ -215,7 +217,7 @@ public sealed partial class ChatSystem : SharedChatSystem
// Was there an emote in the message? If so, send it.
if (player != null && emoteStr != message && emoteStr != null)
{
SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker);
SendEntityEmote(source, emoteStr, range, nameOverride, null, ignoreActionBlocker); // DeltaV - Had to change up for SendEntityEmote
}
// This can happen if the entire string is sanitized out.
@@ -252,9 +254,18 @@ public sealed partial class ChatSystem : SharedChatSystem
case InGameICChatType.Whisper:
SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker);
break;
case InGameICChatType.Emote:
SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
break;
case InGameICChatType.Emote: // DeltaV - Emote now has different types of emotes.
var type = ProcessEmoteMessage(source, message, out var modMessage);
if (type == EmoteType.Audible || type == EmoteType.AudiblePossessive)
{
if (checkRadioPrefix && TryProcessRadioMessage(source, modMessage, out var outputMessage, out var channel, capitalize: false))
SendAudibleEntityEmote(source, outputMessage, range, nameOverride, channel, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
else
SendAudibleEntityEmote(source, modMessage, range, nameOverride, null, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
}
else
SendEntityEmote(source, modMessage, range, nameOverride, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
break; // DeltaV - End
//Nyano - Summary: case adds the telepathic chat sending ability.
case InGameICChatType.Telepathic:
_nyanoChatSystem.SendTelepathicChat(source, message, range == ChatTransmitRange.HideChat);
@@ -629,6 +640,7 @@ public sealed partial class ChatSystem : SharedChatSystem
string action,
ChatTransmitRange range,
string? nameOverride,
EmoteType? emoteType, // DeltaV
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
@@ -642,11 +654,21 @@ public sealed partial class ChatSystem : SharedChatSystem
var ent = Identity.Entity(source, EntityManager);
string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
// Begin DeltaV
string wrappedMessage;
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (emoteType == EmoteType.Possessive) // DeltaV - Emote types now get checked.
wrappedMessage = Loc.GetString("chat-manager-entity-me-possessive-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
else
wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
// End DeltaV
if (checkEmote &&
!TryEmoteChatInput(source, action))
@@ -660,6 +682,59 @@ public sealed partial class ChatSystem : SharedChatSystem
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
}
// DeltaV - Added this to differentiate between emotes that can be heard over radio and those which can't.
protected override void SendAudibleEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
RadioChannelPrototype? channel,
EmoteType? emoteType,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
EmoteType type = emoteType ?? EmoteType.Audible;
// get the entity's apparent name (if no override provided).
var ent = Identity.Entity(source, EntityManager);
string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
string wrappedMessage;
// Audible emotes use Identity.Name, since that is the status quo. Emotes like scream doesn't currently reveal identities so this won't either. [This may be changed with feedback as you can audibly emote over radios]
if (emoteType == EmoteType.AudiblePossessive)
wrappedMessage = Loc.GetString("chat-manager-entity-me-audible-possessive-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
else
wrappedMessage = Loc.GetString("chat-manager-entity-me-audible-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (checkEmote &&
!TryEmoteChatInput(source, action))
return;
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
var ev = new EntityAudiblyEmotedEvent(source, action, channel, type);
RaiseLocalEvent(source, ref ev, true);
if (!hideLog)
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
}
// DeltaV - End
// ReSharper disable once InconsistentNaming
private void SendLOOC(EntityUid source, ICommonSession player, string message, bool hideChat)
{
@@ -889,6 +964,7 @@ public sealed partial class ChatSystem : SharedChatSystem
var recipients = new Dictionary<ICommonSession, ICChatRecipientData>();
var ghostHearing = GetEntityQuery<GhostHearingComponent>();
var xforms = GetEntityQuery<TransformComponent>();
var blockListening = GetEntityQuery<BlockListeningComponent>(); // DeltaV - block listening
var transformSource = xforms.GetComponent(source);
var sourceMapId = transformSource.MapID;
@@ -904,6 +980,11 @@ public sealed partial class ChatSystem : SharedChatSystem
if (transformEntity.MapID != sourceMapId)
continue;
// Begin DeltaV - block listening
if (blockListening.HasComponent(playerEntity))
continue;
// End DeltaV - block listening
var observer = ghostHearing.HasComponent(playerEntity);
// even if they are a ghost hearer, in some situations we still need the range
@@ -971,4 +1052,4 @@ public sealed class CheckTargetedSpeechEvent : EntityEventArgs
{
public List<EntityUid> Targets = new List<EntityUid>();
}
// END Mono
// END Mono
+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).
@@ -51,11 +51,8 @@ public sealed class SignalTimerSystem : EntitySystem
if (_ui.HasUi(uid, SignalTimerUiKey.Key))
{
_ui.SetUiState(uid, SignalTimerUiKey.Key, new SignalTimerBoundUserInterfaceState(component.Label,
// Begin Monolith
// TimeSpan.FromSeconds(component.Delay).Minutes.ToString("D2"),
// TimeSpan.FromSeconds(component.Delay).Seconds.ToString("D2"),
TimeSpan.FromSeconds(component.Delay),
// End Monolith
TimeSpan.FromSeconds(component.Delay).Minutes.ToString("D2"),
TimeSpan.FromSeconds(component.Delay).Seconds.ToString("D2"),
component.CanEditLabel,
time,
active != null,
@@ -76,11 +73,8 @@ public sealed class SignalTimerSystem : EntitySystem
if (_ui.HasUi(uid, SignalTimerUiKey.Key))
{
_ui.SetUiState(uid, SignalTimerUiKey.Key, new SignalTimerBoundUserInterfaceState(signalTimer.Label,
// Begin Monolith
// TimeSpan.FromSeconds(signalTimer.Delay).Minutes.ToString("D2"),
// TimeSpan.FromSeconds(signalTimer.Delay).Seconds.ToString("D2"),
TimeSpan.FromSeconds(signalTimer.Delay),
// End Monolith
TimeSpan.FromSeconds(signalTimer.Delay).Minutes.ToString("D2"),
TimeSpan.FromSeconds(signalTimer.Delay).Seconds.ToString("D2"),
signalTimer.CanEditLabel,
TimeSpan.Zero,
false,
+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
}
}
@@ -9,6 +9,7 @@ using Content.Shared.Radio.Components;
using Content.Shared.Radio.EntitySystems;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Content.Shared._DV.Chat;
namespace Content.Server.Radio.EntitySystems;
@@ -26,6 +27,7 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
SubscribeLocalEvent<HeadsetComponent, EncryptionChannelsChangedEvent>(OnKeysChanged);
SubscribeLocalEvent<WearingHeadsetComponent, EntitySpokeEvent>(OnSpeak);
SubscribeLocalEvent<WearingHeadsetComponent, EntityAudiblyEmotedEvent>(OnAudibleEmote); // DeltaV
}
private void OnKeysChanged(EntityUid uid, HeadsetComponent component, EncryptionChannelsChangedEvent args)
@@ -48,22 +50,43 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
EnsureComp<ActiveRadioComponent>(uid).Channels = new(keyHolder.Channels);
}
private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args)
// DeltaV
// WARNING - Be very careful when modifying this method.
// The implementation right now has null forgiving operatiors in OnSpeak() and OnAudibleEmote() since this only returns true if channel is not null
private bool CheckRadioCapable(EntityUid uid, WearingHeadsetComponent headset, RadioChannelPrototype? channel)
{
if (args.Channel != null
&& TryComp(component.Headset, out EncryptionKeyHolderComponent? keys)
&& keys.Channels.Contains(args.Channel.ID))
if (channel != null
&& TryComp(headset.Headset, out EncryptionKeyHolderComponent? keys)
&& keys.Channels.Contains(channel.ID))
{
// Begin DeltaV Additions: No using headsets if you lost your hands or are cuffed
if (!TryComp<HandsComponent>(uid, out var hands) || hands.Count < 1 ||
TryComp<CuffableComponent>(uid, out var cuffable) && _cuffable.IsCuffed((uid, cuffable)))
{
_popup.PopupEntity(Loc.GetString("headset-cant-reach"), uid, uid, PopupType.SmallCaution);
return;
return false;
}
// End DeltaV Additions
_radio.SendRadioMessage(uid, args.Message, args.Channel, component.Headset);
return true;
}
return false;
}
private void OnAudibleEmote(EntityUid uid, WearingHeadsetComponent component, EntityAudiblyEmotedEvent args)
{
if (CheckRadioCapable(uid, component, args.Channel))
{
_radio.SendRadioMessage(uid, args.Message, args.Channel!, component.Headset, emType: args.Type);
args.Channel = null;
}
}
// DeltaV - End
private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args)
{
if (CheckRadioCapable(uid, component, args.Channel)) // DeltaV - Put all the radio checks into the CheckRadioCapable() method.
{
_radio.SendRadioMessage(uid, args.Message, args.Channel!, component.Headset); // DeltaV - Made the args.Channel null ignorant as CheckRadioCapable() guarantees it not null.
args.Channel = null; // prevent duplicate messages from other listeners.
}
}
@@ -1,6 +1,7 @@
using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
using Content.Server.Power.Components;
using Content.Shared._DV.Chat;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Radio;
@@ -38,6 +39,7 @@ public sealed class RadioSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<IntrinsicRadioReceiverComponent, RadioReceiveEvent>(OnIntrinsicReceive);
SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntitySpokeEvent>(OnIntrinsicSpeak);
SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntityAudiblyEmotedEvent>(OnIntrinsicAudibleEmote); // DeltaV - Robots should be allowed to emote over radio.
_exemptQuery = GetEntityQuery<TelecomExemptComponent>();
}
@@ -57,12 +59,22 @@ public sealed class RadioSystem : EntitySystem
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel);
}
// DeltaV
private void OnIntrinsicAudibleEmote(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntityAudiblyEmotedEvent args)
{
if (args.Channel != null && component.Channels.Contains(args.Channel.ID))
{
SendRadioMessage(uid, args.Message, args.Channel, uid, emType: args.Type);
}
}
// DeltaV - End
/// <summary>
/// Send radio message to all active radio listeners
/// </summary>
public void SendRadioMessage(EntityUid messageSource, string message, ProtoId<RadioChannelPrototype> channel, EntityUid radioSource, bool escapeMarkup = true)
public void SendRadioMessage(EntityUid messageSource, string message, ProtoId<RadioChannelPrototype> channel, EntityUid radioSource, bool escapeMarkup = true, EmoteType? emType = null) // DeltaV - EmoteType? added.
{
SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup);
SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, emType: emType);
}
/// <summary>
@@ -70,7 +82,7 @@ public sealed class RadioSystem : EntitySystem
/// </summary>
/// <param name="messageSource">Entity that spoke the message</param>
/// <param name="radioSource">Entity that picked up the message and will send it, e.g. headset</param>
public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true)
public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true, EmoteType? emType = null) // DeltaV - EmoteType? added.
{
// TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this.
if (!_messages.Add(message))
@@ -92,14 +104,31 @@ public sealed class RadioSystem : EntitySystem
? FormattedMessage.EscapeText(message)
: message;
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap",
("color", channel.Color),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
// DeltaV - This change is to change up how the messages are wrapped up. Basically changing the formatting depending on the emote type.
string wrappedMessage;
if (emType == EmoteType.Audible)
wrappedMessage = Loc.GetString("chat-radio-message-audible-emote-wrap",
("color", channel.Color),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
else if (emType == EmoteType.AudiblePossessive)
wrappedMessage = Loc.GetString("chat-radio-message-audible-possessive-emote-wrap",
("color", channel.Color),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
else
wrappedMessage = Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap",
("color", channel.Color),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
// DeltaV - End
// most radios are relayed to chat, so lets parse the chat message beforehand
var chat = new ChatMessage(
@@ -27,6 +27,11 @@ public sealed partial class BorgSystem
var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta))
{
// DeltaV Begin
if (!comp.Active)
return;
// DeltaV End
if (comp.NextDisable is { } nextDisable && now >= nextDisable)
DoDisable((uid, comp, chassis, meta));
@@ -83,6 +88,11 @@ public sealed partial class BorgSystem
private void OnPacketReceived(Entity<BorgTransponderComponent> ent, ref DeviceNetworkPacketEvent args)
{
// DeltaV Begin
if (!ent.Comp.Active)
return;
// DeltaV End
var payload = args.Data;
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return;
@@ -197,4 +207,17 @@ public sealed partial class BorgSystem
return true;
}
// DeltaV Begin
/// <summary>
/// DeltaV - sets if the borg transponder is active
/// </summary>
public void SetTransponderActive(Entity<BorgTransponderComponent?> ent, bool active)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Active = active;
}
// DeltaV End
}
@@ -29,12 +29,14 @@ public sealed partial class SpeechWireAction : ComponentWireAction<SpeechCompone
public override bool Cut(EntityUid user, Wire wire, SpeechComponent component)
{
_speech.SetSpeech(wire.Owner, false, component);
EntityManager.GetComponentOrNull<Components.UnblockableSpeechComponent>(user)?.Active = false; // DeltaV - we need this to override unblockable speech
return true;
}
public override bool Mend(EntityUid user, Wire wire, SpeechComponent component)
{
_speech.SetSpeech(wire.Owner, true, component);
EntityManager.GetComponentOrNull<Components.UnblockableSpeechComponent>(user)?.Active = true; // DeltaV - we need this to override unblockable speech
return true;
}
@@ -3,5 +3,9 @@ namespace Content.Server.Speech.Components
[RegisterComponent]
public sealed partial class UnblockableSpeechComponent : Component
{
// Begin DeltaV
[DataField]
public bool Active = true;
// End DeltaV
}
}
@@ -12,7 +12,7 @@ namespace Content.Server.Speech.EntitySystems
private void OnCheck(EntityUid uid, UnblockableSpeechComponent component, CheckIgnoreSpeechBlockerEvent args)
{
args.IgnoreBlocker = true;
args.IgnoreBlocker = component.Active; // DeltaV
}
}
}
@@ -0,0 +1,120 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared._DV.AccountLinking;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._DV.AccountLinking;
public sealed class LinkAccountManager : IPostInjectInit
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
private readonly Dictionary<NetUserId, TimeSpan> _lastRequest = new();
private readonly TimeSpan _minimumWait = TimeSpan.FromSeconds(0.5);
private readonly Dictionary<NetUserId, SharedPatronFull> _connected = new();
private readonly List<SharedPatron> _allPatrons = [];
private async Task LoadData(ICommonSession player, CancellationToken cancel)
{
var patron = await _db.GetPatron(player.UserId, cancel);
var linked = await _db.HasLinkedAccount(player.UserId, cancel);
cancel.ThrowIfCancellationRequested();
var tier = patron?.Tier;
var sharedTier = tier == null
? null
: new SharedPatronTier(
tier.ShowOnCredits,
tier.Name
);
_connected[player.UserId] = new SharedPatronFull(sharedTier, linked);
}
private void FinishLoad(ICommonSession player)
{
SendPatronStatus(player);
}
private void ClientDisconnected(ICommonSession player)
{
_connected.Remove(player.UserId);
}
private void SendPatronStatus(ICommonSession player)
{
var connected = _connected.GetValueOrDefault(player.UserId);
var msg = new LinkAccountStatusMsg { Patron = connected, };
_net.ServerSendMessage(msg, player.Channel);
SendPatrons(player);
}
private void OnRequest(LinkAccountRequestMsg message)
{
var user = message.MsgChannel.UserId;
var time = _timing.RealTime;
if (_lastRequest.TryGetValue(user, out var last) &&
last + _minimumWait > time)
{
return;
}
_lastRequest[user] = time;
var code = Guid.NewGuid();
_db.SetLinkingCode(user, code);
var response = new LinkAccountCodeMsg { Code = code };
_net.ServerSendMessage(response, message.MsgChannel);
}
public async Task RefreshAllPatrons()
{
var patrons = await _db.GetAllPatrons();
_allPatrons.Clear();
foreach (var patron in patrons)
{
_allPatrons.Add(new SharedPatron(patron.Player.LastSeenUserName, patron.Tier.Name));
}
}
public void SendPatronsToAll()
{
var msg = new PatronListMsg { Patrons = _allPatrons };
_net.ServerSendToAll(msg);
}
public void SendPatrons(ICommonSession player)
{
var msg = new PatronListMsg { Patrons = _allPatrons };
_net.ServerSendMessage(msg, player.Channel);
}
public SharedPatronFull? GetPatron(ICommonSession player)
{
return GetPatron(player.UserId);
}
public SharedPatronFull? GetPatron(NetUserId userId)
{
return _connected.GetValueOrDefault(userId);
}
void IPostInjectInit.PostInject()
{
_net.RegisterNetMessage<LinkAccountRequestMsg>(OnRequest);
_net.RegisterNetMessage<LinkAccountCodeMsg>();
_net.RegisterNetMessage<LinkAccountStatusMsg>();
_net.RegisterNetMessage<PatronListMsg>();
_userDb.AddOnLoadPlayer(LoadData);
_userDb.AddOnFinishLoad(FinishLoad);
_userDb.AddOnPlayerDisconnect(ClientDisconnected);
}
}
@@ -0,0 +1,43 @@
using Content.Server.GameTicking;
namespace Content.Server._DV.AccountLinking;
public sealed class LinkAccountSystem : EntitySystem
{
[Dependency] private readonly LinkAccountManager _linkAccount = default!;
public override void Initialize()
{
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
ReloadPatrons();
}
private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev)
{
switch (ev.New)
{
case GameRunLevel.InRound:
break;
case GameRunLevel.PreRoundLobby:
case GameRunLevel.PostRound:
ReloadPatrons();
break;
}
}
private async void ReloadPatrons()
{
try
{
await _linkAccount.RefreshAllPatrons();
_linkAccount.SendPatronsToAll();
}
catch (Exception e)
{
Log.Error($"Error reloading Patrons list:\n{e}");
}
}
}
@@ -1,5 +1,6 @@
using Content.Server._DV.Psionics.Systems;
using Content.Shared._DV.Psionics.Components;
using Content.Shared._DV.Traits.Assorted;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
@@ -17,10 +18,12 @@ public sealed partial class ParadoxCloneRuleSystem
private void FilterTargets(HashSet<Entity<MindComponent>> minds)
{
// TODO: use generic IMindFilter
// no picking other antags or non-crew
// no picking other antags or non-crew and entities with no paradox clone trait
minds.RemoveWhere(mind => _role.MindIsAntagonist(mind) ||
!_role.MindHasRole<JobRoleComponent>((mind, mind), out var role) ||
role?.Comp1.JobPrototype == null);
role?.Comp1.JobPrototype == null ||
(mind.Comp.OwnedEntity is { } entity && HasComp<NoParadoxCloneComponent>(entity))
);
}
private void PostClone(EntityUid mob)
@@ -0,0 +1,39 @@
using Content.Server.Wires;
using Content.Shared._DV.Silicons.Borgs;
using Content.Shared._DV.Silicons;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the cyborg's ID chip slot is active
/// </summary>
public sealed partial class BorgIdChipWireAction : ComponentWireAction<IdChipSlotComponent>
{
public override string Name { get; set; } = "wire-name-borg-id-chip";
public override Color Color { get; set; } = Color.Thistle;
public override object StatusKey => BorgWireActionKey.ChipKey;
public override StatusLightState? GetLightState(Wire wire, IdChipSlotComponent component)
{
return component.Active ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, IdChipSlotComponent component)
{
EntityManager.System<IdChipSlotSystem>()
.SetActive((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, IdChipSlotComponent component)
{
EntityManager.System<IdChipSlotSystem>()
.SetActive((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, IdChipSlotComponent component)
{
}
}
@@ -0,0 +1,40 @@
using Content.Server.Silicons.Borgs;
using Content.Server.Wires;
using Content.Shared._DV.Silicons;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the cyborg's transponder is active
/// </summary>
public sealed partial class BorgTransponderWireAction : ComponentWireAction<BorgTransponderComponent>
{
public override string Name { get; set; } = "wire-name-borg-transponder";
public override Color Color { get; set; } = Color.YellowGreen;
public override object StatusKey => BorgWireActionKey.TransponderKey;
public override StatusLightState? GetLightState(Wire wire, BorgTransponderComponent component)
{
return component.Active ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, BorgTransponderComponent component)
{
EntityManager.System<BorgSystem>()
.SetTransponderActive((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, BorgTransponderComponent component)
{
EntityManager.System<BorgSystem>()
.SetTransponderActive((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, BorgTransponderComponent component)
{
}
}
@@ -13,6 +13,7 @@ namespace Content.Server._DV.Silicons.Laws;
public sealed class SlavedBorgSystem : SharedSlavedBorgSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedSiliconLawSystem _siliconLaws = default!;
public override void Initialize()
{
@@ -24,12 +25,12 @@ public sealed class SlavedBorgSystem : SharedSlavedBorgSystem
private void OnGetSiliconLaws(Entity<SlavedBorgComponent> ent, ref GetSiliconLawsEvent args)
{
if (ent.Comp.Added || !TryComp<SiliconLawProviderComponent>(ent, out var provider))
if (ent.Comp.HasBeenAdded || !TryComp<SiliconLawProviderComponent>(ent, out var provider))
return;
if (provider.Lawset is {} lawset)
if (provider.Lawset is {} lawset && ent.Comp.ShouldBeAdded)
AddLaw(lawset, ent.Comp.Law);
ent.Comp.Added = true; // prevent opening the ui adding more law 0's
ent.Comp.HasBeenAdded = true; // prevent opening the ui adding more law 0's
}
/// <summary>
@@ -39,4 +40,30 @@ public sealed class SlavedBorgSystem : SharedSlavedBorgSystem
{
lawset.Laws.Insert(0, _proto.Index(law).ShallowClone());
}
/// <summary>
/// Sets whether the slaving is active.
/// </summary>
public void SetShouldBeAdded(Entity<SlavedBorgComponent?> ent, bool active)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (!TryComp<SiliconLawProviderComponent>(ent, out var provider) || provider.Lawset is not { } lawset)
return;
if (!ent.Comp.ShouldBeAdded && active)
{
AddLaw(lawset, ent.Comp.Law);
ent.Comp.HasBeenAdded = true;
_siliconLaws.NotifyLawsChanged(ent);
}
else if (ent.Comp.ShouldBeAdded && !active)
{
lawset.Laws.Remove(_proto.Index(ent.Comp.Law));
ent.Comp.HasBeenAdded = false;
_siliconLaws.NotifyLawsChanged(ent);
}
ent.Comp.ShouldBeAdded = active;
}
}
@@ -0,0 +1,40 @@
using Content.Server.Wires;
using Content.Shared._DV.Silicons;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the power cell slot is active
/// </summary>
public sealed partial class PowerCellSlotWireAction : ComponentWireAction<PowerCellSlotComponent>
{
public override string Name { get; set; } = "wire-name-power-cell";
public override Color Color { get; set; } = Color.BurlyWood;
public override object StatusKey => BorgWireActionKey.CellKey;
public override StatusLightState? GetLightState(Wire wire, PowerCellSlotComponent component)
{
return component.Active ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, PowerCellSlotComponent component)
{
EntityManager.System<PowerCellSystem>()
.SetSlotActive((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, PowerCellSlotComponent component)
{
EntityManager.System<PowerCellSystem>()
.SetSlotActive((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, PowerCellSlotComponent component)
{
}
}
@@ -0,0 +1,41 @@
using Content.Server._DV.Silicons.Laws;
using Content.Server.Wires;
using Content.Shared.Doors;
using Content.Shared._DV.Silicons.Laws;
using Content.Shared._DV.Silicons;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the cyborg's transponder is active
/// </summary>
public sealed partial class SlavedBorgWireAction : ComponentWireAction<SlavedBorgComponent>
{
public override string Name { get; set; } = "wire-name-slaved-borg";
public override Color Color { get; set; } = Color.Coral;
public override object StatusKey => BorgWireActionKey.SlavedKey;
public override StatusLightState? GetLightState(Wire wire, SlavedBorgComponent component)
{
return component.ShouldBeAdded ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, SlavedBorgComponent component)
{
EntityManager.System<SlavedBorgSystem>()
.SetShouldBeAdded((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, SlavedBorgComponent component)
{
EntityManager.System<SlavedBorgSystem>()
.SetShouldBeAdded((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, SlavedBorgComponent component)
{
}
}
@@ -33,37 +33,33 @@ public sealed class InteractorSystem : SharedInteractorSystem
return;
}
// Mono
for (var i = count - 1; i >= 0; i--)
var i = count - 1;
var netEnt = ent.Comp.TargetEntities[i].Item1;
var target = GetEntity(netEnt);
_constructionQuery.TryComp(target, out var construction);
var originalCount = construction?.InteractionQueue?.Count ?? 0;
if (!InteractWith(ent, target))
{
var netEnt = ent.Comp.TargetEntities[i].Item1;
var target = GetEntity(netEnt);
_constructionQuery.TryComp(target, out var construction);
var originalCount = construction?.InteractionQueue?.Count ?? 0;
if (!InteractWith(ent, target))
{
// have to remove it since user's filter was bad due to unhandled interaction
// RemoveTarget(ent, target); // Mono
Machine.Failed(ent.Owner);
continue; // Mono
}
// have to remove it since user's filter was bad due to unhandled interaction
RemoveTarget(ent, target);
Machine.Failed(ent.Owner);
return;
}
// construction supercode queues it instead of starting a doafter now, assume that queuing means it has started
var newCount = construction?.InteractionQueue?.Count ?? 0;
if (newCount > originalCount
|| HasDoAfter(ent))
{
Machine.Started(ent.Owner);
UpdateAppearance(ent, InteractorState.Active);
}
else
{
// no doafter, complete it immediately
TryRemoveTarget(ent, target);
Machine.Completed(ent.Owner);
UpdateAppearance(ent);
}
break; // Mono
// construction supercode queues it instead of starting a doafter now, assume that queuing means it has started
var newCount = construction?.InteractionQueue?.Count ?? 0;
if (newCount > originalCount
|| HasDoAfter(ent))
{
Machine.Started(ent.Owner);
UpdateAppearance(ent, InteractorState.Active);
}
else
{
// no doafter, complete it immediately
TryRemoveTarget(ent, target);
Machine.Completed(ent.Owner);
UpdateAppearance(ent);
}
}
}
@@ -1,19 +0,0 @@
using Content.Shared.DeviceLinking;
using Content.Shared.Research.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Server._Goobstation.Lathe;
/// <summary>
/// Lets a lathe produce the last made recipe, controlled by signal port.
/// The port must be added by something else e.g. AutomationSlots
/// </summary>
[RegisterComponent, Access(typeof(LatheAutomationSystem))]
public sealed partial class LatheAutomationComponent : Component
{
[ViewVariables]
public LatheRecipePrototype? LastRecipe;
[DataField]
public ProtoId<SinkPortPrototype> PrintPort = "LathePrint";
}
@@ -1,40 +0,0 @@
using Content.Shared._Goobstation.Factory;
using Content.Server.Lathe;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Lathe;
namespace Content.Server._Goobstation.Lathe;
public sealed class LatheAutomationSystem : EntitySystem
{
[Dependency] private readonly AutomationSystem _automation = default!;
[Dependency] private readonly LatheSystem _lathe = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LatheAutomationComponent, LatheStartPrintingEvent>(OnStartPrinting);
SubscribeLocalEvent<LatheAutomationComponent, SignalReceivedEvent>(OnSignalReceived);
}
private void OnStartPrinting(Entity<LatheAutomationComponent> ent, ref LatheStartPrintingEvent args)
{
ent.Comp.LastRecipe = args.Recipe;
}
private void OnSignalReceived(Entity<LatheAutomationComponent> ent, ref SignalReceivedEvent args)
{
if (!_automation.IsAutomated(ent))
return;
if (args.Port != ent.Comp.PrintPort)
return;
if (ent.Comp.LastRecipe is not {} recipe)
return;
_lathe.TryAddToQueue(ent.Owner, recipe, 1);
_lathe.TryStartProducing(ent.Owner); // Won't do anything otherwise
}
}
+44 -1
View File
@@ -1,5 +1,6 @@
using System.Collections.Frozen;
using System.Collections.Immutable;
using Content.Shared._DV.Chat; // DeltaV - chat
using Content.Shared.Chat.Prototypes;
using Content.Shared.Speech;
using Robust.Shared.Audio;
@@ -98,7 +99,7 @@ public abstract partial class SharedChatSystem
{
// not all emotes are loc'd, but for the ones that are we pass in entity
var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
SendEntityEmote(source, action, range, nameOverride, null, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker); // DeltaV
}
return didEmote;
@@ -208,6 +209,48 @@ public abstract partial class SharedChatSystem
return true;
}
// DeltaV
/// <summary>
/// Checks which type of emote the message contains and returns the type while outputting the string without the prefixes.
/// </summary>
public static EmoteType? ProcessEmoteMessage(EntityUid source, string input, out string output)
{
output = input.Trim();
EmoteType? type = null;
if (input.Length == 0)
return type;
if (!(input.StartsWith(AudibleEmotePrefix) || input.StartsWith(PossessiveEmotePrefix)))
{
type = EmoteType.Normal;
return type;
}
var emoteType = input[0];
output = input[1..].TrimStart();
if (emoteType == AudibleEmotePrefix)
{
emoteType = input[1]; // Check for if we want AudiblePossessiveEmote
type = EmoteType.Audible;
}
if (emoteType == PossessiveEmotePrefix)
{
if (type == EmoteType.Audible)
{
type = EmoteType.AudiblePossessive;
output = input[2..].TrimStart();
}
else
type = EmoteType.Possessive;
}
return type;
}
// DeltaV - End
/// <summary>
/// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
/// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player
+25 -3
View File
@@ -1,5 +1,6 @@
using System.Collections.Frozen;
using System.Text.RegularExpressions;
using Content.Shared._DV.Chat;
using Content.Shared.ActionBlocker;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Popups;
@@ -29,6 +30,8 @@ public abstract partial class SharedChatSystem : EntitySystem
public const char OOCPrefix = '[';
public const char EmotesPrefix = '@';
public const char EmotesAltPrefix = '*';
public const char AudibleEmotePrefix = '!'; // DeltaV - You may now scream audibly!
public const char PossessiveEmotePrefix = '\''; // DeltaV - You may now be possessive of things! Whatever that means.
public const char AdminPrefix = ']';
public const char WhisperPrefix = ',';
public const char TelepathicPrefix = '='; //Nyano - Summary: Adds the telepathic channel's prefix.
@@ -152,6 +155,7 @@ public abstract partial class SharedChatSystem : EntitySystem
string input,
out string output,
out RadioChannelPrototype? channel,
bool capitalize = true, // DeltaV - We might not want to capitalize the first letter if we send in emotes.
bool quiet = false)
{
output = input.Trim();
@@ -162,7 +166,7 @@ public abstract partial class SharedChatSystem : EntitySystem
if (input.StartsWith(RadioCommonPrefix))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
output = capitalize ? SanitizeMessageCapital(input[1..].TrimStart()) : input[1..].TrimStart(); // DeltaV
channel = _prototypeManager.Index<RadioChannelPrototype>(CommonChannel);
return true;
}
@@ -172,7 +176,7 @@ public abstract partial class SharedChatSystem : EntitySystem
if (input.Length < 2 || char.IsWhiteSpace(input[1]))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
output = capitalize ? SanitizeMessageCapital(input[1..].TrimStart()) : input[1..].TrimStart(); // DeltaV
if (!quiet)
_popup.PopupEntity(Loc.GetString("chat-manager-no-radio-key"), source, source);
return true;
@@ -180,7 +184,7 @@ public abstract partial class SharedChatSystem : EntitySystem
var channelKey = input[1];
channelKey = char.ToLower(channelKey);
output = SanitizeMessageCapital(input[2..].TrimStart());
output = capitalize ? SanitizeMessageCapital(input[2..].TrimStart()) : input[2..].TrimStart(); // DeltaV
if (channelKey == DefaultChannelKey)
{
@@ -314,11 +318,29 @@ public abstract partial class SharedChatSystem : EntitySystem
return rawmsg.Substring(tagStart, tagEnd - tagStart);
}
// DeltaV
protected virtual void SendAudibleEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
RadioChannelPrototype? channel,
EmoteType? emoteType,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{
}
// DeltaV - End
protected virtual void SendEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
EmoteType? emoteType,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
+53 -6
View File
@@ -17,6 +17,17 @@ namespace Content.Shared.Humanoid
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
// DeltaV - i hate this hack but https://github.com/space-wizards/RobustToolbox/issues/6576
private ISawmill _locSawmill = default!;
public override void Initialize()
{
base.Initialize();
_locSawmill = Logger.GetSawmill("loc");
}
// End DeltaV - i hate this hack
public string GetName(string species, Gender? gender = null)
{
// if they have an old species or whatever just fall back to human I guess?
@@ -47,31 +58,67 @@ namespace Content.Shared.Humanoid
return Loc.GetString("namepreset-lastfirst",
("last", GetLastName(speciesProto)), ("first", GetFirstName(speciesProto, gender)));
case SpeciesNaming.FirstLast:
// Begin DeltaV - more complex naming
default:
{
var firstId = GetFirstNameId(speciesProto, gender);
var lastId = GetLastNameId(speciesProto);
var plural = false;
var oldLevel =_locSawmill.Level;
_locSawmill.Level = LogLevel.Fatal; // this is a hack to avoid testfails because TryGetString still logs errors for not-found stuff anyways (wtf)
if (Loc.TryGetString($"{lastId}.plural", out var pluralStr))
plural = pluralStr == "true";
var last = Loc.GetString(lastId);
if (Loc.TryGetString($"{firstId}.intersperse", out var firstIntersperse, ("last", last), ("lastPlural", plural)))
{
_locSawmill.Level = oldLevel;
return firstIntersperse;
}
_locSawmill.Level = oldLevel;
return Loc.GetString("namepreset-firstlast",
("first", GetFirstName(speciesProto, gender)), ("last", GetLastName(speciesProto)));
("first", Loc.GetString(firstId)),
("last", last),
("lastPlural", plural));
}
// End DeltaV - more complex naming
}
}
// Begin DeltaV - we want IDs
public string GetFirstName(SpeciesPrototype speciesProto, Gender? gender = null)
{
return Loc.GetString(GetFirstNameId(speciesProto, gender));
}
public string GetFirstNameId(SpeciesPrototype speciesProto, Gender? gender = null)
{
switch (gender)
{
case Gender.Male:
return _random.Pick(_prototypeManager.Index(speciesProto.MaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.MaleFirstNames));
case Gender.Female:
return _random.Pick(_prototypeManager.Index(speciesProto.FemaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.FemaleFirstNames));
default:
if (_random.Prob(0.5f))
return _random.Pick(_prototypeManager.Index(speciesProto.MaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.MaleFirstNames));
else
return _random.Pick(_prototypeManager.Index(speciesProto.FemaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.FemaleFirstNames));
}
}
public string GetLastNameId(SpeciesPrototype speciesProto)
{
return _random.PickId(_prototypeManager.Index(speciesProto.LastNames));
}
public string GetLastName(SpeciesPrototype speciesProto)
{
return _random.Pick(_prototypeManager.Index(speciesProto.LastNames));
return Loc.GetString(GetLastNameId(speciesProto));
}
// End DeltaV - we want IDs
}
}
@@ -28,6 +28,11 @@ public sealed class ActivateInWorldEvent : HandledEntityEventArgs, ITargetedInte
/// </summary>
public bool WasLogged { get; set; }
/// <summary>
/// Stellar - set to false if this interaction shouldn't have an interaction particle
/// </summary>
public bool InteractionParticle = true;
public ActivateInWorldEvent(EntityUid user, EntityUid target, bool complex)
{
User = user;
@@ -1189,7 +1189,7 @@ namespace Content.Shared.Interaction
RaiseLocalEvent(used, activateMsg, true);
if (activateMsg.Handled)
{
DoContactInteraction(user, used, null, true); // Interaction particles
DoContactInteraction(user, used, null, true, interactionParticles: activateMsg.InteractionParticle); // Stellar - Interaction particles
if (!activateMsg.WasLogged)
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
@@ -15,33 +15,24 @@ public enum SignalTimerUiKey : byte
public sealed class SignalTimerBoundUserInterfaceState : BoundUserInterfaceState
{
public string CurrentText;
// Begin Monolith
// public string CurrentDelayMinutes;
// public string CurrentDelaySeconds;
public TimeSpan CurrentDelay;
// End Monolith
public string CurrentDelayMinutes;
public string CurrentDelaySeconds;
public bool ShowText;
public TimeSpan TriggerTime;
public bool TimerStarted;
public bool HasAccess;
public SignalTimerBoundUserInterfaceState(string currentText,
// Begin Monolith
// string currentDelayMinutes,
// string currentDelaySeconds,
TimeSpan currentDelay,
// End Monolith
string currentDelayMinutes,
string currentDelaySeconds,
bool showText,
TimeSpan triggerTime,
bool timerStarted,
bool hasAccess)
{
CurrentText = currentText;
// Begin Monolith
// CurrentDelayMinutes = currentDelayMinutes;
// CurrentDelaySeconds = currentDelaySeconds;
CurrentDelay = currentDelay;
// End Monolith
CurrentDelayMinutes = currentDelayMinutes;
CurrentDelaySeconds = currentDelaySeconds;
ShowText = showText;
TriggerTime = triggerTime;
TimerStarted = timerStarted;
@@ -23,5 +23,12 @@ public sealed partial class PowerCellSlotComponent : Component
[DataField, AutoNetworkedField]
public bool FitsInCharger = true;
// Begin DeltaV
/// <summary>
/// DeltaV - power cell slots
/// </summary>
[DataField, AutoNetworkedField]
public bool Active = true;
// End DeltaV
}
@@ -39,6 +39,14 @@ public sealed partial class PowerCellSystem
return false;
}
// Begin DeltaV
if (!ent.Comp.Active)
{
battery = null;
return false;
}
// End DeltaV
if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot))
{
battery = null;
@@ -153,6 +161,21 @@ public sealed partial class PowerCellSystem
[PublicAPI]
public bool HasCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
// Begin DeltaV
if (Resolve(ent, ref ent.Comp, false) && !ent.Comp.Active)
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
return false;
}
// End DeltaV
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
@@ -192,6 +215,21 @@ public sealed partial class PowerCellSystem
[PublicAPI]
public bool TryUseCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
// Begin DeltaV
if (Resolve(ent, ref ent.Comp, false) && !ent.Comp.Active)
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
return false;
}
// End DeltaV
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
@@ -247,6 +285,29 @@ public sealed partial class PowerCellSystem
return _battery.GetMaxUses(battery.Value.AsNullable(), cost);
}
// Begin DeltaV
/// <summary>
/// Sets whether a power cell slot is capable of drawing from a battery
/// </summary>
[PublicAPI]
public void SetSlotActive(Entity<PowerCellSlotComponent?> ent, bool active)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Active = active;
Dirty(ent, ent.Comp);
if (!active)
{
var emptyEv = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(ent, ref emptyEv);
}
_battery.RefreshChargeRate(ent.Owner);
}
// End DeltaV
}
// Begin DeltaV - event-based search for battery
@@ -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;
@@ -14,13 +14,21 @@ namespace Content.Shared.Random.Helpers
return random.Pick(prototype.Values);
}
/// <summary>
/// DeltaV - Randomly selects an entry from <paramref name="prototype"/> and returns the result.
/// </summary>
public static string PickId(this IRobustRandom random, LocalizedDatasetPrototype prototype)
{
var index = random.Next(prototype.Values.Count);
return prototype.Values[index];
}
/// <summary>
/// Randomly selects an entry from <paramref name="prototype"/>, attempts to localize it, and returns the result.
/// </summary>
public static string Pick(this IRobustRandom random, LocalizedDatasetPrototype prototype)
{
var index = random.Next(prototype.Values.Count);
return Loc.GetString(prototype.Values[index]);
return Loc.GetString(random.PickId(prototype)); // DeltaV - we need LocIds too
}
public static string Pick(this IWeightedRandomPrototype prototype, System.Random random)
@@ -46,4 +46,48 @@ public sealed partial class SSDIndicatorComponent : Component
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
// DeltaV - SSD Recency Additions START
/// <summary>
/// The time at which the Entity became SSD.
/// </summary>
[AutoNetworkedField, AutoPausedField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan SsdSince = TimeSpan.Zero;
/// <summary>
/// The icon displayed next to the associated entity when it is recently SSD (stage 2).
/// </summary>
[DataField]
public ProtoId<SsdIconPrototype> RecentIcon = "RecentSSDIcon";
/// <summary>
/// The icon displayed next to the associated entity when it is very recently SSD (stage 1).
/// </summary>
[DataField]
public ProtoId<SsdIconPrototype> VeryRecentIcon = "VeryRecentSSDIcon";
// DeltaV END
}
// DeltaV - SSD Recency START
// If you change this enum, remember to update `Resources/Locale/en-US/_DV/ssdIndicator/examine.ftl`
public enum SsdStage : byte
{
/// <summary>
/// Stage 1: SSD Indicator is red. They might just be recovering from a crash/timeout.
/// </summary>
VeryRecent,
/// <summary>
/// Stage 2: SSD Indicator is yellow. They've been gone for a bit, but they shouldn't be moved to cryo yet.
/// </summary>
Recent,
/// <summary>
/// Stage 3: SSD Indicator is green/default. They've been gone for a long time, they can be moved to cryo.
/// </summary>
Cryoable,
}
// DeltaV END
@@ -1,4 +1,6 @@
using Content.Shared._DV.CCVars; // DeltaV - SSD Recency
using Content.Shared.CCVar;
using Content.Shared.Examine; // DeltaV - SSD Recency
using Content.Shared.StatusEffectNew;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
@@ -21,6 +23,9 @@ public sealed class SSDIndicatorSystem : EntitySystem
private bool _icSsdSleep;
private float _icSsdSleepTime;
private TimeSpan _cryoableSsdSeconds; // DeltaV
private TimeSpan _recentSsdSeconds; // DeltaV
public override void Initialize()
{
SubscribeLocalEvent<SSDIndicatorComponent, PlayerAttachedEvent>(OnPlayerAttached);
@@ -29,8 +34,44 @@ public sealed class SSDIndicatorSystem : EntitySystem
_cfg.OnValueChanged(CCVars.ICSSDSleep, obj => _icSsdSleep = obj, true);
_cfg.OnValueChanged(CCVars.ICSSDSleepTime, obj => _icSsdSleepTime = obj, true);
_cfg.OnValueChanged(DCCVars.SsdIndicatorCryoableAfterSeconds, obj => _cryoableSsdSeconds = TimeSpan.FromSeconds(obj), true); // DeltaV - SSD Recency
_cfg.OnValueChanged(DCCVars.SsdIndicatorRecentAfterSeconds, obj => _recentSsdSeconds = TimeSpan.FromSeconds(obj), true); // DeltaV - SSD Recency
SubscribeLocalEvent<SSDIndicatorComponent, ExaminedEvent>(OnExamine); // DeltaV - SSD Recency
}
// DeltaV - SSD Recency START
private void OnExamine(Entity<SSDIndicatorComponent> ent, ref ExaminedEvent args)
{
if (!ent.Comp.IsSSD)
return;
using (args.PushGroup(nameof(SSDIndicatorComponent)))
{
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()}"));
}
}
public SsdStage GetStage(Entity<SSDIndicatorComponent> ent)
{
var curTime = _timing.CurTime;
if (ent.Comp.SsdSince + _cryoableSsdSeconds < curTime)
{
return SsdStage.Cryoable;
}
if (ent.Comp.SsdSince + _recentSsdSeconds < curTime)
{
return SsdStage.Recent;
}
return SsdStage.VeryRecent;
}
// DeltaV END
private void OnPlayerAttached(EntityUid uid, SSDIndicatorComponent component, PlayerAttachedEvent args)
{
component.IsSSD = false;
@@ -55,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);
}
@@ -77,4 +77,10 @@ public sealed partial class BorgTransponderComponent : Component
/// </summary>
[DataField]
public bool FakeDisabled;
/// <summary>
/// DeltaV - whether the transponder is active
/// </summary>
[DataField]
public bool Active = true;
}
@@ -718,8 +718,13 @@ public abstract class SharedStrippableSystem : EntitySystem
if (args.Handled || !args.Complex || args.Target == args.User)
return;
// Begin Stellar Changes - don't play an interact particle for examining the strip UI
if (TryOpenStrippingUi(args.User, (uid, component)))
{
args.Handled = true;
args.InteractionParticle = false;
}
// End Stellar Changes - don't play an interact particle for examining the strip UI
}
/// <summary>
@@ -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);
}
+15
View File
@@ -316,4 +316,19 @@ public sealed partial class DCCVars
/// </summary>
public static readonly CVarDef<bool> EsScreenshakeDisabled =
CVarDef.Create("deltav.es_screenshake.disabled", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// The total time a player has to be SSD to be considered cryoable (stage 3).
/// Default is 20 minutes. Value should be bigger than <see cref="SsdIndicatorRecentAfterSeconds"/>.
/// </summary>
public static readonly CVarDef<float> SsdIndicatorCryoableAfterSeconds =
CVarDef.Create("deltav.ssd.cryoable_after_seconds", 1200f, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// The total time a player has to be SSD to be considered recently SSD (stage 2).
/// If the player has been SSD for less than this time, they are considered "very recently" SSD (stage 1).
/// Default is 5 minutes. Value should be smaller than <see cref="SsdIndicatorCryoableAfterSeconds"/>.
/// </summary>
public static readonly CVarDef<float> SsdIndicatorRecentAfterSeconds =
CVarDef.Create("deltav.ssd.recent_after_seconds", 300f, CVar.SERVER | CVar.REPLICATED);
}
@@ -0,0 +1,6 @@
using Content.Shared.Radio;
namespace Content.Shared._DV.Chat;
[ByRefEvent]
public record struct EntityAudiblyEmotedEvent(EntityUid Source, string Message, RadioChannelPrototype? Channel, EmoteType? Type);
@@ -0,0 +1,15 @@
namespace Content.Shared._DV.Chat;
// Note for future: If you want to make this more robust to handle more types of emotes while still being able to check off audible as an option for it then it would
// probably be better to make it a struct and have audible be a flag for it and then the type of emote. This would avoid having to make two different types for audible and visual.
/// <summary>
/// Different ways of emoting. For that little extra in RP!
/// </summary>
public enum EmoteType : byte
{
Normal, // Character emotes
Audible, // Character screams
Possessive, // Character's emote
AudiblePossessive // Character's scream
}
@@ -0,0 +1,12 @@
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Silicons;
[Serializable, NetSerializable]
public enum BorgWireActionKey : byte
{
SlavedKey,
TransponderKey,
ChipKey,
CellKey,
}
@@ -9,7 +9,7 @@ namespace Content.Shared._DV.Silicons.Borgs;
/// Adds an id chip slot for a borg which will control its access.
/// Enables an id chip's access when inserted to an id chip slot.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(IdChipSlotSystem))]
[RegisterComponent, NetworkedComponent, Access(typeof(IdChipSlotSystem)), AutoGenerateComponentState]
public sealed partial class IdChipSlotComponent : Component
{
/// <summary>
@@ -35,6 +35,12 @@ public sealed partial class IdChipSlotComponent : Component
/// </summary>
[DataField(required: true)]
public EntityWhitelist Whitelist = new();
/// <summary>
/// Whether the ID chip slot is active
/// </summary>
[DataField, AutoNetworkedField]
public bool Active = true;
}
/// <summary>
@@ -73,7 +73,7 @@ public sealed class IdChipSlotSystem : EntitySystem
private void OnGetAdditionalAccess(Entity<IdChipSlotComponent> ent, ref GetAdditionalAccessEvent args)
{
if (ent.Comp.Chip is {} chip)
if (ent.Comp is { Active: true, Chip: {} chip })
args.Entities.Add(chip);
}
@@ -83,7 +83,7 @@ public sealed class IdChipSlotSystem : EntitySystem
return;
// enable its access so the borg can use it
_access.SetAccessEnabled(args.Entity, true);
_access.SetAccessEnabled(args.Entity, ent.Comp.Active);
}
private void OnChipRemoved(Entity<IdChipSlotComponent> ent, ref EntRemovedFromContainerMessage args)
@@ -105,4 +105,14 @@ public sealed class IdChipSlotSystem : EntitySystem
_container.Remove(chip, ent.Comp.Container);
_hands.TryPickupAnyHand(args.Actor, chip);
}
public void SetActive(Entity<IdChipSlotComponent?> ent, bool active)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Active = active;
_access.SetAccessEnabled(ent.Owner, active && ent.Comp.Chip is not null);
Dirty(ent);
}
}
@@ -19,8 +19,14 @@ public sealed partial class SlavedBorgComponent : Component
public ProtoId<SiliconLawPrototype> Law;
/// <summary>
/// Prevents adding the same law twice.
/// Prevents adding the same law 0 mutliple times during law updates.
/// </summary>
[DataField]
public bool Added;
public bool HasBeenAdded;
/// <summary>
/// Whether the law 0 should be added to the lawset.
/// </summary>
[DataField]
public bool ShouldBeAdded = true;
}
@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Traits.Assorted;
/// <summary>
/// This entity will not be candidate for a paradox clone.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class NoParadoxCloneComponent : Component;
@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Whitelist.Holoprojectors;
/// <summary>
/// Marker component for holoradiationblockers, used for reclaiming charges of the projector.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class HoloRadiationblockerComponent : Component;
@@ -126,7 +126,10 @@ public sealed class AutomationSystem : EntitySystem
return null;
}
public bool IsAutomated(EntityUid uid) => _automatedQuery.HasComp(uid);
public bool IsAutomated(EntityUid uid)
{
return _automatedQuery.HasComp(uid);
}
public bool HasSlot(Entity<AutomationSlotsComponent?> ent, string port, bool input)
{
@@ -95,3 +95,8 @@
license: "CC-BY-NC-3.0"
copyright: "Superposition by Amie Waters, converted to mono"
source: "https://amiewaters.bandcamp.com/track/superposition-2"
- files: ["waitingroomabovethestars.ogg", "waitingroomabovethestars_mono.ogg"]
license: "CC-BY-SA-4.0"
copyright: "by This_Is_Gray for DeltaV, converted to mono"
source: "https://github.com/DeltaV-Station/Delta-v.git"
@@ -13,3 +13,8 @@
license: "CC-BY-NC-SA-4.0"
copyright: "flying off the album TWENTY by Oliver Buckland."
source: "https://oliver-buckland.bandcamp.com/album/twenty"
- files: ["waitingroomabovethestars.ogg"]
license: "CC-BY-SA-4.0"
copyright: "by This_Is_Gray for DeltaV"
source: "https://github.com/DeltaV-Station/Delta-v.git"
+7
View File
@@ -307,5 +307,12 @@ Entries:
id: 33
time: '2026-05-28T16:56:20.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5858
- author: DisposableCrewmember42
changes:
- message: The admin overlay now shows how long people have been SSD
type: Add
id: 34
time: '2026-06-06T19:14:04.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5699
Name: DeltaVAdmin
Order: 5
+111 -116
View File
@@ -1,120 +1,4 @@
Entries:
- author: Colin-Tel, BigMaxPorter
changes:
- message: The laser carbine now does the intended 20 heat damage.
type: Fix
id: 1871
time: '2025-11-28T08:24:56.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4790
- author: Toby222
changes:
- message: Cardboard boxes are now affected by gravity
type: Fix
id: 1872
time: '2025-11-28T08:47:19.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4794
- author: Toby222
changes:
- message: Skimmer now does its one and only job again, lazy bugger
type: Fix
id: 1873
time: '2025-11-28T09:36:55.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4795
- author: Toby222
changes:
- message: Flintlock is now craftable as intended
type: Fix
id: 1874
time: '2025-11-28T09:41:03.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4796
- author: Toby222
changes:
- message: Removed the invisible, intangible bear inside all fishing crates that
was trying to steal your fish
type: Fix
id: 1875
time: '2025-11-28T09:49:21.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4797
- author: SirWarock
changes:
- message: The faithful rejoice! Your deity has chosen to to protect you as long
as you hold your religious tome!
type: Tweak
id: 1876
time: '2025-11-28T19:03:43.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4726
- author: Vapetastic-Gaming
changes:
- message: Submarine Playtest Fixes and Tweaks, it is now in current rotation, with
a minimum 80 population !!
type: Tweak
id: 1877
time: '2025-11-29T03:30:17.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4554
- author: snowywinters
changes:
- message: Now normal command turrets alert the AI to an intruder
type: Fix
id: 1878
time: '2025-11-29T19:57:59.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4803
- author: kotobdev
changes:
- message: The FG13 Xiuhcoatl is now substantially more deadly. It also costs less!
type: Tweak
id: 1879
time: '2025-11-29T22:27:08.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4749
- author: ShepardToTheStars
changes:
- message: While trying to stand-up, you can now press your Toggle Crawling keybind
to get up faster at the cost of stamina. Of course, you still still need a free
hand to do this.
type: Tweak
id: 1880
time: '2025-11-30T16:24:25.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4802
- author: TheGrimbeeper
changes:
- message: Artifacts can now be triggered by ambient radiation, not just uranium
spears
type: Fix
id: 1881
time: '2025-11-30T16:30:34.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4817
- author: pootslap
changes:
- message: Characters must now be 40 to be considered middle-aged and 70 to be considered
old.
type: Tweak
id: 1882
time: '2025-11-30T16:31:43.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4783
- author: Proxysseia
changes:
- message: Crew of legally docked foreign vessels now has rights
type: Add
id: 1883
time: '2025-11-30T16:37:35.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4763
- author: verybigman311
changes:
- message: Rejoice! Four new cocktails utilizing the long-neglected Angobitters
are now available.
type: Add
- message: You can now ask for Mliko (glorified beer foam) at your local bar.
type: Add
- message: Famous drinks such as Pilk, Moonshine, and Hooch have been given new
sprites.
type: Add
- message: Alterations have been made to the Grog, Posca, and Gin Fizz recipes.
type: Tweak
- message: Numerous beverages will now form puddles of the correct color when thrown
violently against the ground.
type: Fix
id: 1884
time: '2025-11-30T16:41:31.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/4780
- author: verybigman311
changes:
- message: Lava Fountains are now an available decoration.
@@ -4400,4 +4284,115 @@
id: 2371
time: '2026-06-02T15:16:39.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5944
- author: turtlemutt
changes:
- message: Swapped out Lighthouse security's red with the new blue.
type: Tweak
id: 2372
time: '2026-06-03T18:43:56.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5898
- author: AeraAuling
changes:
- message: Holy and Shadow have been renamed to be a bit less good and evil
type: Tweak
id: 2373
time: '2026-06-04T10:58:56.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5952
- author: sowelipililimute
changes:
- message: Interaction particles are now less energetic.
type: Tweak
- message: Interaction particles no longer spawn when opening the strip menu.
type: Fix
id: 2374
time: '2026-06-05T12:46:01.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5933
- author: sowelipililimute
changes:
- message: Dionaea now have a new name generation algorithim.
type: Tweak
id: 2375
time: '2026-06-05T13:33:57.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5836
- author: Stxcking
changes:
- message: 'Reverted and Rework Upstream Medical Borg Changes. Media provided in
PR:'
type: Tweak
- message: Medical Borg can now see Triage Icons.
type: Fix
id: 2376
time: '2026-06-05T15:05:53.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5656
- author: Stxcking
changes:
- message: Added the No Paradox Clone Trait!
type: Add
id: 2377
time: '2026-06-05T15:17:50.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5937
- author: rebe83
changes:
- message: Nuclear operatives, listening post operators, and ninjas now have randomized
appearances.
type: Tweak
id: 2378
time: '2026-06-05T15:26:25.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5922
- author: Stxcking
changes:
- message: Added RGB plushies, gay wackpack, LGBT handheld flags, and more pride
theme items to the Pride-o-Mat!
type: Tweak
id: 2379
time: '2026-06-05T15:28:44.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5946
- author: Stxcking
changes:
- message: Added Small Nuclear Reactor (7,500) and Small Gas Turbine (5,000) Flatpacks
for Purchase!
type: Add
id: 2380
time: '2026-06-05T15:32:57.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5945
- author: einknusprigestoast
changes:
- message: Added a Chameleon armored Vest for Syndicate Agents
type: Add
id: 2381
time: '2026-06-05T17:34:10.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5921
- author: sowelipililimute
changes:
- message: NT Cyborgs now have a wire panel that allow enabling/disabling the transponder,
law 0, and AI PDA access.
type: Add
id: 2382
time: '2026-06-05T21:13:49.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5948
- author: Stxcking
changes:
- message: Radiation Barrier Projectors Actually Creates Holo Radiation Barriers
Instead of Holo Wet Floor Signs !
type: Fix
id: 2383
time: '2026-06-06T09:53:19.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5787
- author: IAmNotGray
changes:
- message: Added a new jukebox song called "Waiting Room Above The Stars."
type: Add
id: 2384
time: '2026-06-06T12:07:56.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5620
- author: DisposableCrewmember42
changes:
- message: The SSD indicator now changes color depending on duration.
type: Add
- message: The examine text of SSD players now includes the SSD duration and information
on what (not) to do with them.
type: Add
id: 2385
time: '2026-06-06T19:14:04.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5699
Order: 1
+9
View File
@@ -1036,4 +1036,13 @@
id: 120
time: '2026-05-31T19:28:53.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5847
- author: biddygelson
changes:
- message: 'Academy: added secure windoors to security disposal chutes.'
type: Add
- message: 'Academy: Escape pods are now the 5x7 DV variant'
type: Tweak
id: 121
time: '2026-06-05T15:07:41.0000000+00:00'
url: https://github.com/DeltaV-Station/Delta-v/pull/5880
Order: 3
@@ -59,4 +59,7 @@ storage_limit = 3
[audio] # DeltaV
attenuation = 4 # inversedistanceclamped 1 << 2, see audioparams.cs
z_offset = -2
z_offset = -2
[discord]
discord_account_linking_message_link = "https://discord.com/channels/968983104247185448/1513638637018021978"
@@ -112,3 +112,6 @@ max_announcement_length = 512
[audio]
attenuation = 4 # inversedistanceclamped 1 << 2, see audioparams.cs
z_offset = -2
[discord]
discord_account_linking_message_link = "https://discord.com/channels/968983104247185448/1513638637018021978"

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